| # Copyright 2015 Google Inc. 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 = FileType([".tar", ".tar.gz", ".tgz", ".tar.xz"]) |
| deb_filetype = FileType([".deb"]) |
| |
| # 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 f.short_path.startswith(strip_prefix): |
| return f.short_path[len(strip_prefix):] |
| return f.short_path |
| |
| def _build_layer(ctx): |
| """Build the current layer for appending it the base layer.""" |
| # Compute the relative path |
| data_path = ctx.attr.data_path |
| if not data_path: |
| data_path = _short_path_dirname(ctx.outputs.out) |
| elif data_path[0] == "/": |
| data_path = data_path[1:] |
| else: # relative path |
| data_path = _short_path_dirname(ctx.outputs.out) + "/" + data_path |
| |
| layer = ctx.new_file(ctx.label.name + ".layer") |
| build_layer = ctx.executable._build_layer |
| args = [ |
| "--output=" + layer.path, |
| "--directory=" + ctx.attr.directory |
| ] |
| 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] |
| args += ["--link=%s:%s" % (k, ctx.attr.symlinks[k]) |
| for k in ctx.attr.symlinks] |
| |
| ctx.action( |
| executable = build_layer, |
| arguments = args, |
| inputs = ctx.files.files + ctx.files.tars + ctx.files.debs, |
| outputs = [layer], |
| 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_image"): |
| return ctx.attr.base.docker_image |
| if len(ctx.files.base) != 1: |
| fail("base attribute should be a single tar file.") |
| return ctx.files.base[0] |
| |
| def _metadata_action(ctx, layer, name, output): |
| """Generate the action to create the JSON metadata for the layer.""" |
| rewrite_tool = ctx.executable._rewrite_tool |
| env = ctx.attr.env |
| 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), |
| "--env=%s" % ",".join(["%s=%s" % (k, env[k]) for k in env]), |
| "--ports=%s" % ",".join(ctx.attr.ports), |
| "--volumes=%s" % ",".join(ctx.attr.volumes) |
| ] |
| inputs = [layer, rewrite_tool, name] |
| base = _get_base_artifact(ctx) |
| if base: |
| args += ["--base=%s" % base.path] |
| inputs += [base] |
| |
| ctx.action( |
| executable = rewrite_tool, |
| arguments = args, |
| inputs = inputs, |
| outputs = [output], |
| 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, layer, name, metadata): |
| """Create the new image.""" |
| create_image = ctx.executable._create_image |
| args = [ |
| "--output=" + ctx.outputs.out.path, |
| "--metadata=" + metadata.path, |
| "--layer=" + layer.path, |
| "--id=@" + name.path, |
| # We label at push time, so we only put a single name in this file: |
| # bazel/package:target => {the layer being appended} |
| # TODO(dmarting): Does the name makes sense? We could use the |
| # repositoryName/package instead. (why do we need to replace |
| # slashes?) |
| "--repository=bazel/" + ctx.label.package.replace("/", "_"), |
| "--name=" + ctx.label.name |
| ] |
| inputs = [layer, metadata, name] |
| # If we have been provided a base image, add it. |
| base = _get_base_artifact(ctx) |
| if base: |
| args += ["--base=%s" % base.path] |
| inputs += [base] |
| ctx.action( |
| executable = create_image, |
| arguments = args, |
| inputs = inputs, |
| use_default_shell_env = True, |
| outputs = [ctx.outputs.out] |
| ) |
| |
| def _docker_build_impl(ctx): |
| """Implementation for the docker_build rule.""" |
| layer = _build_layer(ctx) |
| name = _compute_layer_name(ctx, layer) |
| metadata = _metadata(ctx, layer, name) |
| _create_image(ctx, layer, name, metadata) |
| ctx.file_action( |
| content = "\n".join([ |
| "#!/bin/bash -eu", |
| "docker load -i " + ctx.outputs.out.short_path |
| ]), |
| output = ctx.outputs.executable, |
| executable = True) |
| return struct(runfiles = ctx.runfiles(files = [ctx.outputs.out]), |
| docker_image = ctx.outputs.out) |
| |
| 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), |
| "symlinks": attr.string_dict(), |
| "entrypoint": attr.string_list(), |
| "cmd": attr.string_list(), |
| "env": attr.string_dict(), |
| "ports": attr.string_list(), # Skylark doesn't support int_list... |
| "volumes": attr.string_list(), |
| # Implicit dependencies. |
| "_build_layer": attr.label( |
| default=Label("//tools/build_defs/docker:build_layer"), |
| cfg=HOST_CFG, |
| executable=True, |
| allow_files=True), |
| "_create_image": attr.label( |
| default=Label("//tools/build_defs/docker:create_image"), |
| cfg=HOST_CFG, |
| executable=True, |
| allow_files=True), |
| "_rewrite_tool": attr.label( |
| default=Label("//tools/build_defs/docker:rewrite_json"), |
| cfg=HOST_CFG, |
| executable=True, |
| allow_files=True), |
| "_sha256": attr.label( |
| default=Label("//tools/build_defs/docker:sha256"), |
| cfg=HOST_CFG, |
| executable=True, |
| allow_files=True) |
| }, |
| outputs = { |
| "out": "%{name}.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. |
| # |
| # 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 this package. |
| # # 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=[...], |
| # |
| # # TODO(mattmoor): NYI |
| # # https://docs.docker.com/reference/builder/#maintainer |
| # maintainer="...", |
| # |
| # # TODO(mattmoor): NYI |
| # # 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=[...], |
| # |
| # # TODO(mattmoor): NYI |
| # # https://docs.docker.com/reference/builder/#workdir |
| # # NOTE: the normal directive affects subsequent RUN, CMD, |
| # # ENTRYPOINT, ADD, and COPY |
| # workdir="...", |
| # |
| # # https://docs.docker.com/reference/builder/#env |
| # env = { |
| # "var1": "val1", |
| # "var2": "val2", |
| # ... |
| # "varN": "valN", |
| # }, |
| # |
| # # NOTE: Without a motivating use case, there is little reason to support: |
| # # https://docs.docker.com/reference/builder/#onbuild |
| # ) |
| 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 |
| ... |
| 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 'blaze/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"]) |
| if "entrypoint" in kwargs: |
| kwargs["entrypoint"] = _validate_command("entrypoint", kwargs["entrypoint"]) |
| docker_build_(**kwargs) |