| # Copyright 2015 The Bazel Authors. All rights reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Rules for manipulation Docker images.""" |
| |
| # Filetype to restrict inputs |
| tar_filetype = [".tar", ".tar.gz", ".tgz", ".tar.xz"] |
| deb_filetype = [".deb", ".udeb"] |
| |
| # Docker files are tarballs, should we allow other extensions than tar? |
| docker_filetype = tar_filetype |
| |
| # This validates the two forms of value accepted by |
| # ENTRYPOINT and CMD, turning them into a canonical |
| # python list form. |
| # |
| # The Dockerfile construct: |
| # ENTRYPOINT "/foo" |
| # Results in: |
| # "Entrypoint": [ |
| # "/bin/sh", |
| # "-c", |
| # "\"/foo\"" |
| # ], |
| # Whereas: |
| # ENTRYPOINT ["/foo", "a"] |
| # Results in: |
| # "Entrypoint": [ |
| # "/foo", |
| # "a" |
| # ], |
| # NOTE: prefacing a command with 'exec' just ends up with the former |
| def _validate_command(name, argument): |
| if type(argument) == "string": |
| return ["/bin/sh", "-c", argument] |
| elif type(argument) == "list": |
| return argument |
| elif argument: |
| fail("The %s attribute must be a string or list, if specified." % name) |
| else: |
| return None |
| |
| def _short_path_dirname(path): |
| """Returns the directory's name of the short path of an artifact.""" |
| sp = path.short_path |
| return sp[:sp.rfind("/")] |
| |
| def _dest_path(f, strip_prefix): |
| """Returns the short path of f, stripped of strip_prefix.""" |
| if not strip_prefix: |
| # If no strip_prefix was specified, use the package of the |
| # given input as the strip_prefix. |
| strip_prefix = _short_path_dirname(f) |
| if f.short_path.startswith(strip_prefix): |
| return f.short_path[len(strip_prefix):] |
| return f.short_path |
| |
| def _compute_data_path(out, data_path): |
| """Compute the relative data path prefix from the data_path attribute.""" |
| if data_path: |
| # Strip ./ from the beginning if specified. |
| # There is no way to handle .// correctly (no function that would make |
| # that possible and Skylark is not turing complete) so just consider it |
| # as an absolute path. A data_path of / should preserve the entire |
| # path up to the repository root. |
| if data_path == "/": |
| return data_path |
| if len(data_path) >= 2 and data_path[0:2] == "./": |
| data_path = data_path[2:] |
| if not data_path or data_path == ".": # Relative to current package |
| return _short_path_dirname(out) |
| elif data_path[0] == "/": # Absolute path |
| return data_path[1:] |
| else: # Relative to a sub-directory |
| return _short_path_dirname(out) + "/" + data_path |
| return data_path |
| |
| def _build_layer(ctx): |
| """Build the current layer for appending it the base layer.""" |
| # Compute the relative path |
| data_path = _compute_data_path(ctx.outputs.out, ctx.attr.data_path) |
| |
| layer = ctx.new_file(ctx.label.name + ".layer") |
| build_layer = ctx.executable.build_layer |
| args = [ |
| "--output=" + layer.path, |
| "--directory=" + ctx.attr.directory, |
| "--mode=" + ctx.attr.mode, |
| ] |
| args += ["--file=%s=%s" % (f.path, _dest_path(f, data_path)) |
| for f in ctx.files.files] |
| args += ["--tar=" + f.path for f in ctx.files.tars] |
| args += ["--deb=" + f.path for f in ctx.files.debs if f.path.endswith(".deb")] |
| args += ["--link=%s:%s" % (k, ctx.attr.symlinks[k]) |
| for k in ctx.attr.symlinks] |
| arg_file = ctx.new_file(ctx.label.name + ".layer.args") |
| ctx.file_action(arg_file, "\n".join(args)) |
| |
| ctx.action( |
| executable = build_layer, |
| arguments = ["--flagfile=" + arg_file.path], |
| inputs = ctx.files.files + ctx.files.tars + ctx.files.debs + [arg_file], |
| outputs = [layer], |
| use_default_shell_env=True, |
| mnemonic="DockerLayer" |
| ) |
| return layer |
| |
| def _sha256(ctx, artifact): |
| """Create an action to compute the SHA-256 of an artifact.""" |
| out = ctx.new_file(artifact.basename + ".sha256") |
| ctx.action( |
| executable = ctx.executable.sha256, |
| arguments = [artifact.path, out.path], |
| inputs = [artifact], |
| outputs = [out], |
| mnemonic = "SHA256") |
| return out |
| |
| def _get_base_artifact(ctx): |
| if ctx.files.base: |
| if hasattr(ctx.attr.base, "docker_layers"): |
| # The base is the first layer in docker_layers if provided. |
| return ctx.attr.base.docker_layers[0]["layer"] |
| if len(ctx.files.base) != 1: |
| fail("base attribute should be a single tar file.") |
| return ctx.files.base[0] |
| |
| def _serialize_dict(dict_value): |
| return ",".join(["%s=%s" % (k, dict_value[k]) for k in dict_value]) |
| |
| def _image_config(ctx, layer_names): |
| """Create the configuration for a new docker image.""" |
| config = ctx.new_file(ctx.label.name + ".config") |
| |
| label_file_dict = dict() |
| for i in range(len(ctx.files.label_files)): |
| fname = ctx.attr.label_file_strings[i] |
| file = ctx.files.label_files[i] |
| label_file_dict[fname] = file |
| |
| labels = dict() |
| for l in ctx.attr.labels: |
| fname = ctx.attr.labels[l] |
| if fname[0] == '@': |
| labels[l] = "@" + label_file_dict[fname[1:]].path |
| else: |
| labels[l] = fname |
| |
| args = [ |
| "--output=%s" % config.path, |
| "--entrypoint=%s" % ",".join(ctx.attr.entrypoint), |
| "--command=%s" % ",".join(ctx.attr.cmd), |
| "--labels=%s" % _serialize_dict(labels), |
| "--env=%s" % _serialize_dict(ctx.attr.env), |
| "--ports=%s" % ",".join(ctx.attr.ports), |
| "--volumes=%s" % ",".join(ctx.attr.volumes) |
| ] |
| if ctx.attr.user: |
| args += ["--user=" + ctx.attr.user] |
| if ctx.attr.workdir: |
| args += ["--workdir=" + ctx.attr.workdir] |
| |
| inputs = layer_names |
| args += ["--layer=@" + l.path for l in layer_names] |
| |
| if ctx.attr.label_files: |
| inputs += ctx.files.label_files |
| |
| base = _get_base_artifact(ctx) |
| if base: |
| args += ["--base=%s" % base.path] |
| inputs += [base] |
| |
| ctx.action( |
| executable = ctx.executable.create_image_config, |
| arguments = args, |
| inputs = inputs, |
| outputs = [config], |
| use_default_shell_env=True, |
| mnemonic = "ImageConfig") |
| return config |
| |
| def _metadata_action(ctx, layer, name, output): |
| """Generate the action to create the JSON metadata for the layer.""" |
| rewrite_tool = ctx.executable.rewrite_tool |
| |
| label_file_dict = dict() |
| for i in range(len(ctx.files.label_files)): |
| fname = ctx.attr.label_file_strings[i] |
| file = ctx.files.label_files[i] |
| label_file_dict[fname] = file |
| |
| labels = dict() |
| for l in ctx.attr.labels: |
| fname = ctx.attr.labels[l] |
| if fname[0] == '@': |
| labels[l] = "@" + label_file_dict[fname[1:]].path |
| else: |
| labels[l] = fname |
| |
| args = [ |
| "--output=%s" % output.path, |
| "--layer=%s" % layer.path, |
| "--name=@%s" % name.path, |
| "--entrypoint=%s" % ",".join(ctx.attr.entrypoint), |
| "--command=%s" % ",".join(ctx.attr.cmd), |
| "--labels=%s" % _serialize_dict(labels), |
| "--env=%s" % _serialize_dict(ctx.attr.env), |
| "--ports=%s" % ",".join(ctx.attr.ports), |
| "--volumes=%s" % ",".join(ctx.attr.volumes) |
| ] |
| if ctx.attr.workdir: |
| args += ["--workdir=" + ctx.attr.workdir] |
| inputs = [layer, rewrite_tool, name] |
| if ctx.attr.label_files: |
| inputs += ctx.files.label_files |
| base = _get_base_artifact(ctx) |
| if base: |
| args += ["--base=%s" % base.path] |
| inputs += [base] |
| if ctx.attr.user: |
| args += ["--user=" + ctx.attr.user] |
| |
| ctx.action( |
| executable = rewrite_tool, |
| arguments = args, |
| inputs = inputs, |
| outputs = [output], |
| use_default_shell_env=True, |
| mnemonic = "RewriteJSON") |
| |
| def _compute_layer_name(ctx, layer): |
| """Compute the layer's name. |
| |
| This function synthesize a version of its metadata where in place |
| of its final name, we use the SHA256 of the layer blob. |
| |
| This makes the name of the layer a function of: |
| - Its layer's SHA256 |
| - Its metadata |
| - Its parent's name. |
| Assuming the parent's name is derived by this same rigor, then |
| a simple induction proves the content addressability. |
| |
| Args: |
| ctx: Rule context. |
| layer: The layer's artifact for which to compute the name. |
| Returns: |
| The artifact that will contains the name for the layer. |
| """ |
| metadata = ctx.new_file(ctx.label.name + ".metadata-name") |
| layer_sha = _sha256(ctx, layer) |
| _metadata_action(ctx, layer, layer_sha, metadata) |
| return _sha256(ctx, metadata) |
| |
| def _metadata(ctx, layer, name): |
| """Create the metadata for the new docker image.""" |
| metadata = ctx.new_file(ctx.label.name + ".metadata") |
| _metadata_action(ctx, layer, name, metadata) |
| return metadata |
| |
| def _create_image(ctx, layers, id, config, name, metadata): |
| """Create the new image.""" |
| args = [ |
| "--output=" + ctx.outputs.layer.path, |
| "--id=@" + id.path, |
| "--config=" + config.path, |
| ] |
| |
| args += ["--layer=@%s=%s" % (l["name"].path, l["layer"].path) for l in layers] |
| inputs = [id, config] + [l["name"] for l in layers] + [l["layer"] for l in layers] |
| |
| if name: |
| args += ["--legacy_id=@" + name.path] |
| inputs += [name] |
| |
| if metadata: |
| args += ["--metadata=" + metadata.path] |
| inputs += [metadata] |
| |
| # If we have been provided a base image, add it. |
| if ctx.attr.base and not hasattr(ctx.attr.base, "docker_layers"): |
| legacy_base = _get_base_artifact(ctx) |
| if legacy_base: |
| args += ["--legacy_base=%s" % legacy_base.path] |
| inputs += [legacy_base] |
| |
| base = _get_base_artifact(ctx) |
| if base: |
| args += ["--base=%s" % base.path] |
| inputs += [base] |
| ctx.action( |
| executable = ctx.executable.create_image, |
| arguments = args, |
| inputs = inputs, |
| outputs = [ctx.outputs.layer], |
| mnemonic = "CreateImage", |
| ) |
| |
| def _assemble_image(ctx, layers, name): |
| """Create the full image from the list of layers.""" |
| layers = [l["layer"] for l in layers] |
| args = [ |
| "--output=" + ctx.outputs.out.path, |
| "--id=@" + name.path, |
| "--repository=" + _repository_name(ctx), |
| "--name=" + ctx.label.name |
| ] + ["--layer=" + l.path for l in layers] |
| inputs = [name] + layers |
| ctx.action( |
| executable = ctx.executable.join_layers, |
| arguments = args, |
| inputs = inputs, |
| outputs = [ctx.outputs.out], |
| mnemonic = "JoinLayers" |
| ) |
| |
| def _repository_name(ctx): |
| """Compute the repository name for the current rule.""" |
| return "%s/%s" % (ctx.attr.repository, ctx.label.package.replace("/", "_")) |
| |
| def reverse(lst): |
| result = [] |
| for el in lst: |
| result = [el] + result |
| return result |
| |
| def _get_runfile_path(ctx, f): |
| """Return the runfiles relative path of f.""" |
| if ctx.workspace_name: |
| return ctx.workspace_name + "/" + f.short_path |
| else: |
| return f.short_path |
| |
| def _docker_build_impl(ctx): |
| """Implementation for the docker_build rule.""" |
| layer = _build_layer(ctx) |
| layer_sha = _sha256(ctx, layer) |
| |
| config = _image_config(ctx, [layer_sha]) |
| id = _sha256(ctx, config) |
| |
| name = _compute_layer_name(ctx, layer) |
| metadata = _metadata(ctx, layer, name) |
| |
| # creating a partial image so only pass the layers that belong to it |
| image_layer = {"layer": layer, "name": layer_sha} |
| _create_image(ctx, [image_layer], id, config, name, metadata) |
| |
| # Compute the layers transitive provider. |
| # It includes the current layers, and, if they exists the layer from |
| # base docker_build rules. We do not extract the list of layer in |
| # a base tarball as they probably do not respect the convention on |
| # layer naming that our rules use. |
| layers = [ |
| {"layer": ctx.outputs.layer, "id": id, "name": name} |
| ] + getattr(ctx.attr.base, "docker_layers", []) |
| # Generate the incremental load statement |
| ctx.template_action( |
| template = ctx.file.incremental_load_template, |
| substitutions = { |
| "%{load_statements}": "\n".join([ |
| "incr_load '%s' '%s' '%s'" % (_get_runfile_path(ctx, l["name"]), |
| _get_runfile_path(ctx, l["id"]), |
| _get_runfile_path(ctx, l["layer"])) |
| # The last layer is the first in the list of layers. |
| # We reverse to load the layer from the parent to the child. |
| for l in reverse(layers)]), |
| "%{repository}": _repository_name(ctx), |
| "%{tag}": ctx.label.name, |
| }, |
| output = ctx.outputs.executable, |
| executable = True) |
| _assemble_image(ctx, reverse(layers), name) |
| runfiles = ctx.runfiles( |
| files = [l["name"] for l in layers] + |
| [l["id"] for l in layers] + |
| [l["layer"] for l in layers]) |
| return struct(runfiles = runfiles, |
| files = set([ctx.outputs.layer]), |
| docker_layers = layers) |
| |
| docker_build_ = rule( |
| implementation = _docker_build_impl, |
| attrs = { |
| "base": attr.label(allow_files=docker_filetype), |
| "data_path": attr.string(), |
| "directory": attr.string(default="/"), |
| "tars": attr.label_list(allow_files=tar_filetype), |
| "debs": attr.label_list(allow_files=deb_filetype), |
| "files": attr.label_list(allow_files=True), |
| "mode": attr.string(default="0555"), |
| "symlinks": attr.string_dict(), |
| "entrypoint": attr.string_list(), |
| "cmd": attr.string_list(), |
| "user": attr.string(), |
| "env": attr.string_dict(), |
| "labels": attr.string_dict(), |
| "ports": attr.string_list(), # Skylark doesn't support int_list... |
| "volumes": attr.string_list(), |
| "workdir": attr.string(), |
| "repository": attr.string(default="bazel"), |
| # Implicit dependencies. |
| "label_files": attr.label_list( |
| allow_files=True), |
| "label_file_strings": attr.string_list(), |
| "build_layer": attr.label( |
| default=Label("//tools/build_defs/pkg:build_tar"), |
| cfg="host", |
| executable=True, |
| allow_files=True), |
| "create_image": attr.label( |
| default=Label("//tools/build_defs/docker:create_image"), |
| cfg="host", |
| executable=True, |
| allow_files=True), |
| "incremental_load_template": attr.label( |
| default=Label("//tools/build_defs/docker:incremental_load_template"), |
| single_file=True, |
| allow_files=True), |
| "join_layers": attr.label( |
| default=Label("//tools/build_defs/docker:join_layers"), |
| cfg="host", |
| executable=True, |
| allow_files=True), |
| "rewrite_tool": attr.label( |
| default=Label("//tools/build_defs/docker:rewrite_json"), |
| cfg="host", |
| executable=True, |
| allow_files=True), |
| "create_image_config": attr.label( |
| default=Label("//tools/build_defs/docker:create_image_config"), |
| cfg="host", |
| executable=True, |
| allow_files=True), |
| "sha256": attr.label( |
| default=Label("//tools/build_defs/docker:sha256"), |
| cfg="host", |
| executable=True, |
| allow_files=True) |
| }, |
| outputs = { |
| "out": "%{name}.tar", |
| "layer": "%{name}-layer.tar", |
| }, |
| executable = True) |
| |
| # Produces a new docker image tarball compatible with 'docker load', which |
| # is a single additional layer atop 'base'. The goal is to have relatively |
| # complete support for building docker image, from the Dockerfile spec. |
| # |
| # For more information see the 'Config' section of the image specification: |
| # https://github.com/opencontainers/image-spec/blob/v0.2.0/serialization.md |
| # |
| # Only 'name' is required. All other fields have sane defaults. |
| # |
| # docker_build( |
| # name="...", |
| # visibility="...", |
| # |
| # # The base layers on top of which to overlay this layer, |
| # # equivalent to FROM. |
| # base="//another/build:rule", |
| # |
| # # The base directory of the files, defaulted to |
| # # the package of the input. |
| # # All files structure relatively to that path will be preserved. |
| # # A leading '/' mean the workspace root and this path is relative |
| # # to the current package by default. |
| # data_path="...", |
| # |
| # # The directory in which to expand the specified files, |
| # # defaulting to '/'. |
| # # Only makes sense accompanying one of files/tars/debs. |
| # directory="...", |
| # |
| # # The set of archives to expand, or packages to install |
| # # within the chroot of this layer |
| # files=[...], |
| # tars=[...], |
| # debs=[...], |
| # |
| # # The set of symlinks to create within a given layer. |
| # symlinks = { |
| # "/path/to/link": "/path/to/target", |
| # ... |
| # }, |
| # |
| # # https://docs.docker.com/reference/builder/#entrypoint |
| # entrypoint="...", or |
| # entrypoint=[...], -- exec form |
| # |
| # # https://docs.docker.com/reference/builder/#cmd |
| # cmd="...", or |
| # cmd=[...], -- exec form |
| # |
| # # https://docs.docker.com/reference/builder/#expose |
| # ports=[...], |
| # |
| # # https://docs.docker.com/reference/builder/#user |
| # # NOTE: the normal directive affects subsequent RUN, CMD, |
| # # and ENTRYPOINT |
| # user="...", |
| # |
| # # https://docs.docker.com/reference/builder/#volume |
| # volumes=[...], |
| # |
| # # https://docs.docker.com/reference/builder/#workdir |
| # # NOTE: the normal directive affects subsequent RUN, CMD, |
| # # ENTRYPOINT, ADD, and COPY, but this attribute only affects |
| # # the entry point. |
| # workdir="...", |
| # |
| # # https://docs.docker.com/reference/builder/#env |
| # env = { |
| # "var1": "val1", |
| # "var2": "val2", |
| # ... |
| # "varN": "valN", |
| # }, |
| # ) |
| def docker_build(**kwargs): |
| """Package a docker image. |
| |
| This rule generates a sequence of genrules the last of which is named 'name', |
| so the dependency graph works out properly. The output of this rule is a |
| tarball compatible with 'docker save/load' with the structure: |
| {layer-name}: |
| layer.tar |
| VERSION |
| json |
| {image-config-sha256}.json |
| ... |
| manifest.json |
| repositories |
| top # an implementation detail of our rules, not consumed by Docker. |
| This rule appends a single new layer to the tarball of this form provided |
| via the 'base' parameter. |
| |
| The images produced by this rule are always named 'bazel/tmp:latest' when |
| loaded (an internal detail). The expectation is that the images produced |
| by these rules will be uploaded using the 'docker_push' rule below. |
| |
| Args: |
| **kwargs: See above. |
| """ |
| if "cmd" in kwargs: |
| kwargs["cmd"] = _validate_command("cmd", kwargs["cmd"]) |
| for reserved in ["label_files", "label_file_strings"]: |
| if reserved in kwargs: |
| fail("reserved for internal use by docker_build macro", attr=reserved) |
| if "labels" in kwargs: |
| files = sorted(set([v[1:] for v in kwargs["labels"].values() if v[0] == '@'])) |
| kwargs["label_files"] = files |
| kwargs["label_file_strings"] = files |
| if "entrypoint" in kwargs: |
| kwargs["entrypoint"] = _validate_command("entrypoint", kwargs["entrypoint"]) |
| docker_build_(**kwargs) |