diff --git a/container/BUILD b/container/BUILD index 610d2426f..7b2e4c0b4 100644 --- a/container/BUILD +++ b/container/BUILD @@ -70,8 +70,8 @@ py_binary( srcs_version = "PY2AND3", visibility = ["//visibility:public"], deps = [ + "@bazel_source//tools/build_defs/pkg:archive", "@bazel_tools//third_party/py/gflags", - "@bazel_tools//tools/build_defs/pkg:archive", ], ) diff --git a/container/build_tar.py b/container/build_tar.py index 14c95f1e9..b243658c8 100644 --- a/container/build_tar.py +++ b/container/build_tar.py @@ -24,7 +24,7 @@ import tarfile import tempfile -from tools.build_defs.pkg import archive +from bazel_source.tools.build_defs.pkg import archive from third_party.py import gflags gflags.DEFINE_string('output', None, 'The output file, mandatory') @@ -39,6 +39,13 @@ gflags.DEFINE_string( 'mode', None, 'Force the mode on the added files (in octal).') +gflags.DEFINE_multistring( + 'empty_root_dir', + [], + 'An empty root directory to add to the layer. This will create a directory that' + 'is a peer of "root_directory". "empty_dir" creates an empty directory inside of' + '"root_directory"') + gflags.DEFINE_multistring('tar', [], 'A tar file to add to the layer') gflags.DEFINE_multistring('deb', [], 'A debian package to add to the layer') @@ -77,6 +84,10 @@ 'Specify the owner names of individual files, e.g. ' 'path/to/file=root.root.') +gflags.DEFINE_string( + 'root_directory', './', 'Default root directory is named "."' + 'Windows docker images require this be named "Files" instead of "."') + FLAGS = gflags.FLAGS @@ -98,13 +109,18 @@ def parse_pkg_name(metadata, filename): else: return os.path.basename(os.path.splitext(filename)[0]) - def __init__(self, output, directory, compression): + def __init__(self, output, directory, compression, root_directory): self.directory = directory self.output = output self.compression = compression + self.root_directory = root_directory def __enter__(self): - self.tarfile = archive.TarFileWriter(self.output, self.compression) + self.tarfile = archive.TarFileWriter( + self.output, + self.compression, + self.root_directory + ) return self def __exit__(self, t, v, traceback): @@ -188,6 +204,23 @@ def add_empty_dir(self, destpath, mode=None, ids=None, names=None): self.add_empty_file(destpath, mode=mode, ids=ids, names=names, kind=tarfile.DIRTYPE) + def add_empty_root_dir(self, destpath, mode=None, ids=None, names=None): + """Add a directory to the root of the tar file. + + Args: + destpath: the name of the directory in the layer + mode: force to set the specified mode, defaults to 644 + ids: (uid, gid) for the file to set ownership + names: (username, groupname) for the file to set ownership. + + An empty directory will be created as `destfile` in the root layer. + """ + original_root_directory = self.tarfile.root_directory + self.tarfile.root_directory = destpath + self.add_empty_dir( + destpath, mode=mode, ids=ids, names=names) + self.tarfile.root_directory = original_root_directory + def add_tar(self, tar): """Merge a tar file into the destination tar file. @@ -358,7 +391,7 @@ def main(unused_argv): ids_map[f] = (int(user), int(group)) # Add objects to the tar file - with TarFile(FLAGS.output, FLAGS.directory, FLAGS.compression) as output: + with TarFile(FLAGS.output, FLAGS.directory, FLAGS.compression, FLAGS.root_directory) as output: def file_attributes(filename): if filename[0] == '/': filename = filename[1:] @@ -375,6 +408,8 @@ def file_attributes(filename): output.add_empty_file(f, **file_attributes(f)) for f in FLAGS.empty_dir: output.add_empty_dir(f, **file_attributes(f)) + for f in FLAGS.empty_root_dir: + output.add_empty_root_dir(f, **file_attributes(f)) for tar in FLAGS.tar: output.add_tar(tar) for deb in FLAGS.deb: diff --git a/container/container.bzl b/container/container.bzl index 6c710be55..8eb5145ee 100644 --- a/container/container.bzl +++ b/container/container.bzl @@ -32,7 +32,7 @@ container = struct( ) # The release of the github.com/google/containerregistry to consume. -CONTAINERREGISTRY_RELEASE = "v0.0.28" +CONTAINERREGISTRY_RELEASE = "v0.0.30" _local_tool_build_template = """ sh_binary( @@ -69,7 +69,7 @@ def repositories(): name = "puller", urls = [("https://storage.googleapis.com/containerregistry-releases/" + CONTAINERREGISTRY_RELEASE + "/puller.par")], - sha256 = "c834a311a1d2ade959c38c262dfead3b180ba022d196c4a96453d4bfa01e83da", + sha256 = "89a7c48df0fd5fb839d452599cc054a6550c18563394d4401428ab2e094d4f0b", executable = True, ) @@ -78,7 +78,7 @@ def repositories(): name = "importer", urls = [("https://storage.googleapis.com/containerregistry-releases/" + CONTAINERREGISTRY_RELEASE + "/importer.par")], - sha256 = "19643df59bb1dc750e97991e7071c601aa2debe94f6ad72e5f23ab8ae77da46f", + sha256 = "3c1f299df498b0712386c52e1eb5499e00d58143ae10fc4b5c12bf0deffb55b6", executable = True, ) @@ -87,10 +87,18 @@ def repositories(): name = "containerregistry", urls = [("https://github.com/google/containerregistry/archive/" + CONTAINERREGISTRY_RELEASE + ".tar.gz")], - sha256 = "07b9d06e46a9838bef712116bbda7e094ede37be010c1f8c0a3f32f2eeca6384", + sha256 = "10fb9ffa1dde14c81f5c12593666bf1d9e9f53727b8cda9abeb0012d08e57fd1", strip_prefix = "containerregistry-" + CONTAINERREGISTRY_RELEASE[1:], ) + # TODO(nichow): Remove after bazel 0.17.0 is released + if "bazel_source" not in excludes: + http_archive( + name = "bazel_source", + urls = [("https://releases.bazel.build/0.17.0/rc1/bazel-0.17.0rc1-dist.zip")], + sha256 = "a9afd2b16a21085bd6c0a70a23acce30b105a8af3a7b3c92a4b83bea6b623fd8", + ) + # TODO(mattmoor): Remove all of this (copied from google/containerregistry) # once transitive workspace instantiation lands. diff --git a/container/create_image_config.py b/container/create_image_config.py index b25f43475..5123c89f0 100644 --- a/container/create_image_config.py +++ b/container/create_image_config.py @@ -31,9 +31,15 @@ parser.add_argument('--base', action='store', help='The parent image.') +parser.add_argument('--basemanifest', action='store', + help='The parent image manifest.') + parser.add_argument('--output', action='store', required=True, help='The output file to generate.') +parser.add_argument('--manifestoutput', action='store', required=False, + help='The manifest output file to generate.') + parser.add_argument('--layer', action='append', default=[], help='Layer sha256 hashes that make up this image') @@ -76,10 +82,11 @@ parser.add_argument('--null_cmd', action='store', default=False, help='If True, "Cmd" will be set to null.') -_PROCESSOR_ARCHITECTURE = 'amd64' - -_OPERATING_SYSTEM = 'linux' +parser.add_argument('--operating_system', action='store', default='linux', + choices=['linux', 'windows'], + help=('Operating system to create docker image for, e.g. {linux}')) +_PROCESSOR_ARCHITECTURE = 'amd64' def KeyValueToDict(pair): """Converts an iterable object of key=value pairs to dictionary.""" @@ -123,6 +130,12 @@ def Stamp(inp): base_json = r.read() data = json.loads(base_json) + base_manifest_json = '{}' + if args.basemanifest: + with open(args.basemanifest, 'r') as r: + base_manifest_json = r.read() + manifestdata = json.loads(base_manifest_json) + layers = [] for layer in args.layer: layers.append(utils.ExtractValue(layer)) @@ -172,7 +185,7 @@ def Stamp(inp): }, ports=args.ports, volumes=args.volumes, workdir=Stamp(args.workdir)), architecture=_PROCESSOR_ARCHITECTURE, - operating_system=_OPERATING_SYSTEM) + operating_system=args.operating_system) if ('config' in output and 'Cmd' in output['config'] and args.null_cmd == "True"): @@ -186,6 +199,10 @@ def Stamp(inp): json.dump(output, fp, sort_keys=True) fp.write('\n') + if (args.manifestoutput): + with open(args.manifestoutput, 'w') as fp: + json.dump(manifestdata, fp, sort_keys=False) + fp.write('\n') if __name__ == '__main__': main() diff --git a/container/extract_config.py b/container/extract_config.py index 066bb822a..631120bbe 100644 --- a/container/extract_config.py +++ b/container/extract_config.py @@ -27,6 +27,8 @@ parser.add_argument('--output', action='store', required=True, help='The output file to which we write the config.') +parser.add_argument('--manifestoutput', action='store', required=True, + help='The output file to which we write the manifest.') # Main program to create a docker image. It expect to be run with: # extract_config --tarball=image.tar \ @@ -37,6 +39,8 @@ def main(): with docker_image.FromTarball(args.tarball) as img: with open(args.output, 'w') as f: f.write(img.config_file()) + with open(args.manifestoutput, 'w') as f: + f.write(img.manifest()) if __name__ == '__main__': diff --git a/container/image.bzl b/container/image.bzl index dc52b6a15..e6818fd30 100644 --- a/container/image.bzl +++ b/container/image.bzl @@ -90,6 +90,12 @@ def _get_base_config(ctx, name, base): l = _get_layers(ctx, name, ctx.attr.base, base) return l.get("config") +def _get_base_manifest(ctx, name, base): + if ctx.files.base or base: + # The base is the first layer in container_parts if provided. + layer = _get_layers(ctx, name, ctx.attr.base, base) + return layer.get("manifest") + def _image_config( ctx, name, @@ -99,12 +105,15 @@ def _image_config( creation_time = None, env = None, base_config = None, + base_manifest = None, + operating_system = None, layer_name = None, workdir = None, null_entrypoint = False, null_cmd = False): """Create the configuration for a new container image.""" config = ctx.new_file(name + "." + layer_name + ".config") + manifest = ctx.new_file(name + "." + layer_name + ".manifest") label_file_dict = _string_to_label( ctx.files.label_files, @@ -121,6 +130,8 @@ def _image_config( args = [ "--output=%s" % config.path, + ] + [ + "--manifestoutput=%s" % manifest.path, ] + [ "--entrypoint=%s" % x for x in entrypoint @@ -168,6 +179,13 @@ def _image_config( args += ["--base=%s" % base_config.path] inputs += [base_config] + if base_manifest: + args += ["--basemanifest=%s" % base_manifest.path] + inputs += [base_manifest] + + if operating_system: + args += ["--operating_system=%s" % operating_system] + if ctx.attr.stamp: stamp_inputs = [ctx.info_file, ctx.version_file] args += ["--stamp-info-file=%s" % f.path for f in stamp_inputs] @@ -177,11 +195,11 @@ def _image_config( executable = ctx.executable.create_image_config, arguments = args, inputs = inputs, - outputs = [config], + outputs = [config, manifest], use_default_shell_env = True, mnemonic = "ImageConfig", ) - return config, _sha256(ctx, config) + return config, _sha256(ctx, config), manifest, _sha256(ctx, manifest) def _repository_name(ctx): """Compute the repository name for the current rule.""" @@ -211,6 +229,7 @@ def _impl( layers = None, debs = None, tars = None, + operating_system = None, output_executable = None, output_tarball = None, output_layer = None, @@ -236,6 +255,7 @@ def _impl( layers: label List, overrides ctx.attr.layers debs: File list, overrides ctx.files.debs tars: File list, overrides ctx.files.tars + operating_system: Operating system to target (e.g. linux, windows) output_executable: File to use as output for script to load docker image output_tarball: File, overrides ctx.outputs.out output_layer: File, overrides ctx.outputs.layer @@ -246,6 +266,7 @@ def _impl( name = name or ctx.label.name entrypoint = entrypoint or ctx.attr.entrypoint cmd = cmd or ctx.attr.cmd + operating_system = operating_system or ctx.attr.operating_system creation_time = creation_time or ctx.attr.creation_time output_executable = output_executable or ctx.outputs.executable output_tarball = output_tarball or ctx.outputs.out @@ -266,6 +287,7 @@ def _impl( debs = debs, tars = tars, env = env, + operating_system = operating_system, output_layer = output_layer, ) @@ -285,9 +307,13 @@ def _impl( # Get the config for the base layer config_file = _get_base_config(ctx, name, base) + # Get the manifest for the base layer + manifest_file = _get_base_manifest(ctx, name, base) + manifest_digest = None + # Generate the new config layer by layer, using the attributes specified and the diff_id for i, layer in enumerate(layers): - config_file, config_digest = _image_config( + config_file, config_digest, manifest_file, manifest_digest = _image_config( ctx, name = name, layer_names = [layer_diff_ids[i]], @@ -296,6 +322,8 @@ def _impl( creation_time = creation_time, env = layer.env, base_config = config_file, + base_manifest = manifest_file, + operating_system = operating_system, layer_name = str(i), workdir = workdir or ctx.attr.workdir, null_entrypoint = null_entrypoint, @@ -312,6 +340,10 @@ def _impl( "config": config_file, "config_digest": config_digest, + # The path to the v2.2 manifest file. + "manifest": manifest_file, + "manifest_digest": manifest_digest, + # A list of paths to the layer .tar.gz files "zipped_layer": zipped_layers, # A list of paths to the layer digests. @@ -425,13 +457,20 @@ container_image_ = rule( # python list form. # # The Dockerfile construct: -# ENTRYPOINT "/foo" +# ENTRYPOINT "/foo" for Linux: # Results in: # "Entrypoint": [ # "/bin/sh", # "-c", # "\"/foo\"" # ], +# ENTRYPOINT "foo" for Windows: +# Results in: +# "Entrypoint": [ +# "%WinDir%\system32\cmd.exe", +# "/c", +# "\"foo\"" +# ], # Whereas: # ENTRYPOINT ["/foo", "a"] # Results in: @@ -440,9 +479,12 @@ container_image_ = rule( # "a" # ], # NOTE: prefacing a command with 'exec' just ends up with the former -def _validate_command(name, argument): +def _validate_command(name, argument, operating_system): if type(argument) == type(""): - return ["/bin/sh", "-c", argument] + if (operating_system == "windows"): + return ["%WinDir%\system32\cmd.exe", "/c", argument] + else: + return ["/bin/sh", "-c", argument] elif type(argument) == type([]): return argument elif argument: @@ -556,6 +598,16 @@ def container_image(**kwargs): Args: **kwargs: See above. """ + operating_system = None + + if ("operating_system" in kwargs): + operating_system = kwargs["operating_system"] + if operating_system != "linux" and operating_system != "windows": + fail( + "invalid operating_system(%s) specified. Must be 'linux' or 'windows'" % operating_system, + attr = operating_system, + ) + reserved_attrs = [ "label_files", "label_file_strings", @@ -583,7 +635,7 @@ def container_image(**kwargs): if kwargs["cmd"] == "": kwargs["cmd"] = [] else: - kwargs["cmd"] = _validate_command("cmd", kwargs["cmd"]) + kwargs["cmd"] = _validate_command("cmd", kwargs["cmd"], operating_system) # If entrypoint is set but set to None, [] or "", # we interpret it as users want to set it to null. @@ -596,6 +648,6 @@ def container_image(**kwargs): if kwargs["entrypoint"] == "": kwargs["entrypoint"] = [] else: - kwargs["entrypoint"] = _validate_command("entrypoint", kwargs["entrypoint"]) + kwargs["entrypoint"] = _validate_command("entrypoint", kwargs["entrypoint"], operating_system) container_image_(**kwargs) diff --git a/container/import.bzl b/container/import.bzl index e80f6a691..6f71f606e 100644 --- a/container/import.bzl +++ b/container/import.bzl @@ -83,6 +83,13 @@ def _container_import_impl(ctx): blobsums += [_sha256(ctx, zipped)] diff_ids += [_sha256(ctx, unzipped)] + manifest = None + manifest_digest = None + + if (len(ctx.files.manifest) > 0): + manifest = ctx.files.manifest[0] + manifest_digest = _sha256(ctx, ctx.files.manifest[0]) + # These are the constituent parts of the Container image, which each # rule in the chain must preserve. @@ -91,6 +98,10 @@ def _container_import_impl(ctx): "config": ctx.files.config[0], "config_digest": _sha256(ctx, ctx.files.config[0]), + # The path to the optional v2.2 manifest file. + "manifest": manifest, + "manifest_digest": manifest_digest, + # A list of paths to the layer .tar.gz files "zipped_layer": zipped_layers, # A list of paths to the layer digests. @@ -122,6 +133,15 @@ def _container_import_impl(ctx): container_parts["config_digest"], ]), ) + if (len(ctx.files.manifest) > 0): + runfiles = runfiles.merge( + ctx.runfiles( + files = ([ + container_parts["manifest"], + container_parts["manifest_digest"], + ]), + ), + ) return struct( container_parts = container_parts, @@ -140,7 +160,8 @@ def _container_import_impl(ctx): container_import = rule( attrs = dict({ "config": attr.label(allow_files = [".json"]), - "layers": attr.label_list(allow_files = tar_filetype + tgz_filetype), + "manifest": attr.label(allow_files = [".json"], mandatory = False), + "layers": attr.label_list(allow_files = tar_filetype + tgz_filetype, mandatory = True), "repository": attr.string(default = "bazel"), }.items() + _hash_tools.items() + _layer_tools.items() + _zip_tools.items()), executable = True, diff --git a/container/join_layers.py b/container/join_layers.py index 989662978..a27d08c86 100644 --- a/container/join_layers.py +++ b/container/join_layers.py @@ -43,7 +43,12 @@ 'and the layer they tag. ' 'e.g. ubuntu=deadbeef,gcr.io/blah/debian=baadf00d')) -parser.add_argument('--layer', action='append', required=True, +parser.add_argument('--manifests', action='append', required=False, + help=('An associative list of fully qualified tag names ' + 'and the manifest associated' + 'e.g. ubuntu=deadbeef,gcr.io/blah/debian=baadf00d')) + +parser.add_argument('--layer', action='append', required=False, help=('Each entry is an equivalence class with 4 parts: ' 'diff_id, blob_sum, unzipped layer, zipped layer.')) @@ -54,7 +59,6 @@ help=('If stamping these layers, the list of files from ' 'which to obtain workspace information')) - class FromParts(v2_2_image.DockerImage): """This accesses a more efficient on-disk format than FromTarball. @@ -62,14 +66,18 @@ class FromParts(v2_2_image.DockerImage): compressed and uncompressed forms available. """ - def __init__(self, config_file, diffid_to_blobsum, + def __init__(self, config_file, manifest_file, diffid_to_blobsum, blobsum_to_unzipped, blobsum_to_zipped, blobsum_to_legacy): self._config = config_file + self._manifest = manifest_file self._blobsum_to_unzipped = blobsum_to_unzipped self._blobsum_to_zipped = blobsum_to_zipped self._blobsum_to_legacy = blobsum_to_legacy + self._diffid_to_blobsum = diffid_to_blobsum config = json.loads(self._config) + content = self.config_file().encode('utf-8') + self._manifest = json.dumps({ 'schemaVersion': 2, 'mediaType': docker_http.MANIFEST_SCHEMA2_MIME, @@ -79,11 +87,7 @@ def __init__(self, config_file, diffid_to_blobsum, 'digest': 'sha256:' + hashlib.sha256(content).hexdigest() }, 'layers': [ - { - 'mediaType': docker_http.LAYER_MIME, - 'size': self.blob_size(diffid_to_blobsum[diff_id]), - 'digest': diffid_to_blobsum[diff_id] - } + self.diff_id_to_layer_manifest_json(diff_id) for diff_id in config['rootfs']['diff_ids'] ] }, sort_keys=True) @@ -99,8 +103,12 @@ def config_file(self): # Could be large, do not memoize def uncompressed_blob(self, digest): """Override.""" - if digest not in self._blobsum_to_unzipped: + + if self.blobsum_to_media_type(digest) == docker_http.FOREIGN_LAYER_MIME: + return bytearray() + elif digest not in self._blobsum_to_unzipped: return self._blobsum_to_legacy[digest].uncompressed_blob(digest) + with open(self._blobsum_to_unzipped[digest], 'r') as reader: return reader.read() @@ -114,11 +122,43 @@ def blob(self, digest): def blob_size(self, digest): """Override.""" - if digest not in self._blobsum_to_zipped: + if self.blobsum_to_media_type(digest) == docker_http.FOREIGN_LAYER_MIME: + return self.blobsum_to_manifest_layer(digest)['size'] + elif digest not in self._blobsum_to_zipped: return self._blobsum_to_legacy[digest].blob_size(digest) info = os.stat(self._blobsum_to_zipped[digest]) return info.st_size + def diff_id_to_manifest_layer(self, diff_id): + return self.blobsum_to_manifest_layer(self._diffid_to_blobsum[diff_id]) + + def blobsum_to_manifest_layer(self, digest): + if self._manifest: + manifest = json.loads(self._manifest) + if 'layers' in manifest: + for layer in manifest['layers']: + if layer['digest'] == digest: + return layer + return None + + def blobsum_to_media_type(self, digest): + manifest_layer = self.blobsum_to_manifest_layer(digest) + if manifest_layer: + return manifest_layer['mediaType'] + return docker_http.LAYER_MIME + + def diff_id_to_layer_manifest_json(self, diff_id): + manifest_layer = self.diff_id_to_manifest_layer(diff_id) + + if manifest_layer: + return manifest_layer + else: + return { + 'mediaType': docker_http.LAYER_MIME, + 'size': self.blob_size(self._diffid_to_blobsum[diff_id]), + 'digest': self._diffid_to_blobsum[diff_id] + } + # __enter__ and __exit__ allow use as a context manager. def __enter__(self): return self @@ -127,7 +167,7 @@ def __exit__(self, unused_type, unused_value, unused_traceback): pass -def create_bundle(output, tag_to_config, diffid_to_blobsum, +def create_bundle(output, tag_to_config, tag_to_manifest, diffid_to_blobsum, blobsum_to_unzipped, blobsum_to_zipped, blobsum_to_legacy): """Creates a Docker image from a list of layers. @@ -147,17 +187,47 @@ def add_file(filename, contents): tag_to_image = {} for (tag, config) in six.iteritems(tag_to_config): + manifest = None + if tag in tag_to_manifest: + manifest = tag_to_manifest[tag] tag_to_image[tag] = FromParts( - config, diffid_to_blobsum, + config, manifest, diffid_to_blobsum, blobsum_to_unzipped, blobsum_to_zipped, blobsum_to_legacy) v2_2_save.multi_image_tarball(tag_to_image, tar) +def create_tag_to_file_content_map(stamp_info, tag_file_pairs): + """ + Creates a Docker image tag to file content map. + + Args: + stamp_info - Tag substitutions to make in the input tags, e.g. {BUILD_USER} + tag_file_pairs - List of input tags and file names + (e.g. ...:image=@bazel-out/...image.0.config) + """ + tag_to_file_content = {} + + if tag_file_pairs: + for entry in tag_file_pairs: + elts = entry.split('=') + if len(elts) != 2: + raise Exception('Expected associative list key=value, got: %s' % entry) + (fq_tag, filename) = elts + + formatted_tag = fq_tag.format(**stamp_info) + tag = docker_name.Tag(formatted_tag, strict=False) + file_contents = utils.ExtractValue(filename) + + # Add the mapping in one direction. + tag_to_file_content[tag] = file_contents + + return tag_to_file_content def main(): args = parser.parse_args() tag_to_config = {} + tag_to_manifest = {} stamp_info = {} diffid_to_blobsum = {} blobsum_to_unzipped = {} @@ -174,18 +244,8 @@ def main(): "using '%s'" % (key, value)) stamp_info[key] = value - for entry in args.tags: - elts = entry.split('=') - if len(elts) != 2: - raise Exception('Expected associative list key=value, got: %s' % entry) - (fq_tag, config_filename) = elts - - formatted_tag = fq_tag.format(**stamp_info) - tag = docker_name.Tag(formatted_tag, strict=False) - config_file = utils.ExtractValue(config_filename) - - # Add the mapping in one direction. - tag_to_config[tag] = config_file + tag_to_config = create_tag_to_file_content_map(stamp_info, args.tags) + tag_to_manifest = create_tag_to_file_content_map(stamp_info, args.manifests) # Do this first so that if there is overlap with the loop below it wins. blobsum_to_legacy = {} @@ -199,22 +259,44 @@ def main(): diffid_to_blobsum[diff_id] = blob_sum blobsum_to_legacy[blob_sum] = legacy_image - for entry in args.layer: - elts = entry.split('=') - if len(elts) != 4: - raise Exception('Expected associative list key=value, got: %s' % entry) - (diffid_filename, blobsum_filename, - unzipped_filename, zipped_filename) = elts - - diff_id = 'sha256:' + utils.ExtractValue(diffid_filename) - blob_sum = 'sha256:' + utils.ExtractValue(diffid_filename) - - diffid_to_blobsum[diff_id] = blob_sum - blobsum_to_unzipped[blob_sum] = unzipped_filename - blobsum_to_zipped[blob_sum] = zipped_filename + if args.layer: + for entry in args.layer: + elts = entry.split('=') + if len(elts) != 4: + raise Exception('Expected associative list key=value, got: %s' % entry) + (diffid_filename, blobsum_filename, + unzipped_filename, zipped_filename) = elts + + diff_id = 'sha256:' + utils.ExtractValue(diffid_filename) + blob_sum = 'sha256:' + utils.ExtractValue(blobsum_filename) + + diffid_to_blobsum[diff_id] = blob_sum + blobsum_to_unzipped[blob_sum] = unzipped_filename + blobsum_to_zipped[blob_sum] = zipped_filename + + # add foreign layers + # + # Windows base images distributed by Microsoft are using foreign layers. + # Foreign layers are not stored in the Docker repository like normal layers. + # Instead they include a list of URLs where the layer can be downloaded. + # This is done because Windows base images are large (2+GB). When someone + # pulls a Windows image, it downloads the foreign layers from those URLs + # instead of requesting the blob from the registry. + # When adding foreign layers through bazel, the actual layer blob is not + # present on the system. Instead the base image manifest is used to + # describe the parent image layers. + for tag, manifest_file in tag_to_manifest.items(): + manifest = json.loads(manifest_file) + if 'layers' in manifest: + config = json.loads(tag_to_config[tag]) + for i, layer in enumerate(manifest['layers']): + diff_id = config['rootfs']['diff_ids'][i] + if layer['mediaType'] == docker_http.FOREIGN_LAYER_MIME: + blob_sum = layer['digest'] + diffid_to_blobsum[diff_id] = blob_sum create_bundle( - args.output, tag_to_config, diffid_to_blobsum, + args.output, tag_to_config, tag_to_manifest, diffid_to_blobsum, blobsum_to_unzipped, blobsum_to_zipped, blobsum_to_legacy) diff --git a/container/layer.bzl b/container/layer.bzl index 14d673513..5f22c7361 100644 --- a/container/layer.bzl +++ b/container/layer.bzl @@ -74,7 +74,8 @@ def build_layer( directory = None, symlinks = None, debs = None, - tars = None): + tars = None, + operating_system = None): """Build the current layer for appending it to the base layer""" layer = output_layer build_layer_exec = ctx.executable.build_layer @@ -84,6 +85,16 @@ def build_layer( "--mode=" + ctx.attr.mode, ] + # Windows layer.tar require two separate root directories instead of just 1 + # 'Files' is the equivalent of '.' in Linux images. + # 'Hives' is unique to Windows Docker images. It is where per layer registry + # changes are stored. rules_docker doesn't support registry deltas, but the + # directory is required for compatibility on Windows. + if (operating_system == "windows"): + args += ["--root_directory=Files"] + empty_root_dirs = ["Files", "Hives"] + args += ["--empty_root_dir=%s" % f for f in empty_root_dirs or []] + args += ["--file=%s=%s" % (f.path, _magic_path(ctx, f, layer)) for f in files] args += ["--file=%s=%s" % (f.path, path) for (path, f) in file_map.items()] args += ["--empty_file=%s" % f for f in empty_files or []] @@ -122,6 +133,7 @@ def _impl( debs = None, tars = None, env = None, + operating_system = None, output_layer = None): """Implementation for the container_layer rule. @@ -135,6 +147,7 @@ def _impl( directory: str, overrides ctx.attr.directory symlinks: str Dict, overrides ctx.attr.symlinks env: str Dict, overrides ctx.attr.env + operating_system: operating system to target (e.g. linux, windows) debs: File list, overrides ctx.files.debs tars: File list, overrides ctx.files.tars output_layer: File, overrides ctx.outputs.layer @@ -146,6 +159,7 @@ def _impl( empty_dirs = empty_dirs or ctx.attr.empty_dirs directory = directory or ctx.attr.directory symlinks = symlinks or ctx.attr.symlinks + operating_system = operating_system or ctx.attr.operating_system debs = debs or ctx.files.debs tars = tars or ctx.files.tars output_layer = output_layer or ctx.outputs.layer @@ -163,6 +177,7 @@ def _impl( symlinks = symlinks, debs = debs, tars = tars, + operating_system = operating_system, ) # Generate the zipped filesystem layer, and its sha256 (aka blob sum) @@ -194,6 +209,7 @@ _layer_attrs = dict({ # Implicit/Undocumented dependencies. "empty_files": attr.string_list(), "empty_dirs": attr.string_list(), + "operating_system": attr.string(default = "linux", mandatory = False), "build_layer": attr.label( default = Label("//container:build_tar"), cfg = "host", diff --git a/container/layer_tools.bzl b/container/layer_tools.bzl index 40453bdb4..acaff674b 100644 --- a/container/layer_tools.bzl +++ b/container/layer_tools.bzl @@ -20,6 +20,7 @@ load( def _extract_layers(ctx, name, artifact): config_file = ctx.new_file(name + "." + artifact.basename + ".config") + manifest_file = ctx.new_file(name + "." + artifact.basename + ".manifest") ctx.action( executable = ctx.executable.extract_config, arguments = [ @@ -27,9 +28,11 @@ def _extract_layers(ctx, name, artifact): artifact.path, "--output", config_file.path, + "--manifestoutput", + manifest_file.path, ], inputs = [artifact], - outputs = [config_file], + outputs = [config_file, manifest_file], mnemonic = "ExtractConfig", ) return { @@ -38,6 +41,7 @@ def _extract_layers(ctx, name, artifact): # I believe we would for a checked in tarball to be usable # with docker_bundle + bazel run. "legacy": artifact, + "manifest": manifest_file, } def get_from_target(ctx, name, attr_target, file_target = None): @@ -63,8 +67,17 @@ def assemble(ctx, images, output, stamp = False): args += [ "--tags=" + tag + "=@" + image["config"].path, ] + + if image.get("manifest"): + args += [ + "--manifests=" + tag + "=@" + image["manifest"].path, + ] + inputs += [image["config"]] + if image.get("manifest"): + inputs += [image["manifest"]] + for i in range(0, len(image["diff_id"])): args += [ "--layer=" + diff --git a/container/push.bzl b/container/push.bzl index 471925da5..47b57a69e 100644 --- a/container/push.bzl +++ b/container/push.bzl @@ -56,6 +56,7 @@ def _impl(ctx): blobs = image.get("zipped_layer", []) layer_arg = " ".join(["--layer=%s" % _get_runfile_path(ctx, f) for f in blobs]) config_arg = "--config=%s" % _get_runfile_path(ctx, image["config"]) + manifest_arg = "--manifest=%s" % _get_runfile_path(ctx, image["manifest"]) ctx.template_action( template = ctx.file._tag_tpl, @@ -78,9 +79,10 @@ def _impl(ctx): ), ), "%{stamp}": stamp_arg, - "%{image}": "%s %s %s %s" % ( + "%{image}": "%s %s %s %s %s" % ( legacy_base_arg, config_arg, + manifest_arg, digest_arg, layer_arg, ), @@ -95,6 +97,7 @@ def _impl(ctx): files = [ ctx.executable._pusher, image["config"], + image["manifest"], ] + image.get("blobsum", []) + image.get("zipped_layer", []) + stamp_inputs + ([image["legacy"]] if image.get("legacy") else []) + list(ctx.attr._pusher.default_runfiles.files), diff --git a/contrib/test.bzl b/contrib/test.bzl index c55426f76..cb0af9150 100644 --- a/contrib/test.bzl +++ b/contrib/test.bzl @@ -125,13 +125,18 @@ def container_test(name, image, configs, driver = None, verbose = None, **kwargs intermediate_image_name = "%s:intermediate" % sanitized_name image_tar_name = "intermediate_bundle_%s" % name - # Give the image a predictable name when loaded - container_bundle( - name = image_tar_name, - images = { - intermediate_image_name: image, - }, - ) + if driver == "tar": + intermediate_image_name = image + image_tar_name = image + else: + # Give the image a predictable name when loaded + container_bundle( + name = image_tar_name, + images = { + intermediate_image_name: image, + }, + ) + _container_test( name = name, image_name = intermediate_image_name, diff --git a/tests/docker/BUILD b/tests/docker/BUILD index 12507c8c9..9e33ef288 100644 --- a/tests/docker/BUILD +++ b/tests/docker/BUILD @@ -13,7 +13,7 @@ # limitations under the License. package(default_visibility = ["//visibility:public"]) -load("//container:container.bzl", "container_image") +load("//container:container.bzl", "container_image", "container_import", "container_push") load("//contrib:test.bzl", "container_test") container_test( @@ -139,3 +139,24 @@ container_test( configs = ["//tests/docker/configs:set_cmd_and_entrypoint.yaml"], image = ":set_cmd_and_entrypoint_null_on_base", ) + +container_import( + name = "import_windows_base_image", + config = "windowsservercore.1803.config.json", + layers = [], + manifest = "windowsservercore.1803.manifest.json", +) + +container_image( + name = "basic_windows_image", + base = ":import_windows_base_image", + cmd = ["echo bar"], + operating_system = "windows", +) + +container_test( + name = "basic_windows_image_test", + configs = ["//tests/docker/configs:windows_image.yaml"], + driver = "tar", + image = ":basic_windows_image", +) diff --git a/tests/docker/configs/windows_image.yaml b/tests/docker/configs/windows_image.yaml new file mode 100644 index 000000000..48baa81f6 --- /dev/null +++ b/tests/docker/configs/windows_image.yaml @@ -0,0 +1,16 @@ +schemaVersion: 2.0.0 + +fileExistenceTests: +- name: 'Files' + path: '/' + shouldExist: true + permissions: 'drwx------' + +- name: 'Hives' + path: '/' + shouldExist: true + permissions: 'drwx------' + +metadataTest: + cmd: ["echo bar"] + diff --git a/tests/docker/windowsservercore.1803.config.json b/tests/docker/windowsservercore.1803.config.json new file mode 100644 index 000000000..d9369443c --- /dev/null +++ b/tests/docker/windowsservercore.1803.config.json @@ -0,0 +1,44 @@ +{ + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "c:\\windows\\system32\\cmd.exe" + ], + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "created": "2018-07-10T15:03:09.8411592-07:00", + "history": [ + { + "created": "2018-04-12T09:20:54.053Z", + "created_by": "Apply image 10.0.17134.1" + }, + { + "created": "2018-07-07T22:48:41.6201766Z", + "created_by": "Install update 10.0.17134.165" + } + ], + "os": "windows", + "os.version": "10.0.17134.165", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:70bba925263c1c193fa7bc7f79b1aa062707b09fe7774c7896b8aaf091b901a9", + "sha256:aa10afa36c78a1a1775d9bb44d90cfe5cfe3f93fd6b48cba8a2586661a0e4da6" + ] + } +} \ No newline at end of file diff --git a/tests/docker/windowsservercore.1803.manifest.json b/tests/docker/windowsservercore.1803.manifest.json new file mode 100644 index 000000000..09bfbb18f --- /dev/null +++ b/tests/docker/windowsservercore.1803.manifest.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 784, + "digest": "sha256:fc9cd8b52f1abe17698bbb770a259ad8d86af3ad9f4f9f29e11e68a4666679cc" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 1659688273, + "digest": "sha256:d9e8b01179bfc94a5bdb1810fbd76b999aa52016001ace2d3a4c4bc7065a9601", + "urls": [ + "https://go.microsoft.com/fwlink/?linkid=873595" + ] + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 493521205, + "digest": "sha256:e30fefc566f71c5dd5786e4783ff4ae3ad98804d5279c14dcf806c813fdf8f66", + "urls": [ + "https://go.microsoft.com/fwlink/?linkid=2005408" + ] + } + ] + } \ No newline at end of file