Add docker_build for building Docker image using Bazel

docker_build is a Skylark rule that describe a docker image. You can
replace Dockerfile by a docker_build to use Bazel's incrementality model.

--
MOS_MIGRATED_REVID=99160762
diff --git a/tools/build_defs/docker/BUILD b/tools/build_defs/docker/BUILD
new file mode 100644
index 0000000..3b6ebd9
--- /dev/null
+++ b/tools/build_defs/docker/BUILD
@@ -0,0 +1,79 @@
+licenses(["notice"])  # Apache 2.0
+
+TEST_DATA = [
+    "//tools/build_defs/docker/testdata:base_with_entrypoint",
+    "//tools/build_defs/docker/testdata:base_with_volume",
+    "//tools/build_defs/docker/testdata:derivative_with_cmd",
+    "//tools/build_defs/docker/testdata:derivative_with_shadowed_cmd",
+    "//tools/build_defs/docker/testdata:derivative_with_volume",
+    "//tools/build_defs/docker/testdata:files_base",
+    "//tools/build_defs/docker/testdata:files_with_files_base",
+    "//tools/build_defs/docker/testdata:files_with_tar_base",
+    "//tools/build_defs/docker/testdata:tar_base",
+    "//tools/build_defs/docker/testdata:tar_with_files_base",
+    "//tools/build_defs/docker/testdata:tar_with_tar_base",
+    "//tools/build_defs/docker/testdata:generated_tarball",
+    "//tools/build_defs/docker/testdata:with_env",
+    "//tools/build_defs/docker/testdata:with_double_env",
+    "//tools/build_defs/docker/testdata:link_with_files_base",
+]
+
+sh_test(
+    name = "build_test",
+    size = "medium",
+    srcs = [
+        "build_test.sh",
+    ],
+    data = TEST_DATA + ["testenv.sh"],
+    deps = [
+        "//src/test/shell:bashunit",
+    ],
+)
+
+# Used by docker_build and friends
+py_library(
+    name = "archive",
+    srcs = ["archive.py"],
+    visibility = ["//tools/build_defs/docker:__subpackages__"],
+)
+
+py_binary(
+    name = "rewrite_json",
+    srcs = ["rewrite_json.py"],
+    visibility = ["//visibility:public"],
+    deps = ["//third_party/py/gflags"],
+)
+
+py_test(
+    name = "rewrite_json_test",
+    srcs = ["rewrite_json_test.py"],
+    deps = [
+        ":rewrite_json",
+    ],
+)
+
+py_binary(
+    name = "build_layer",
+    srcs = ["build_layer.py"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":archive",
+        "//third_party/py/gflags",
+    ],
+)
+
+py_binary(
+    name = "sha256",
+    srcs = ["sha256.py"],
+    visibility = ["//visibility:public"],
+)
+
+py_binary(
+    name = "create_image",
+    srcs = ["create_image.py"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":archive",
+        "//third_party/py/gflags",
+    ],
+)
diff --git a/tools/build_defs/docker/README.md b/tools/build_defs/docker/README.md
new file mode 100644
index 0000000..6c698a8
--- /dev/null
+++ b/tools/build_defs/docker/README.md
@@ -0,0 +1,321 @@
+# Docker support for Bazel
+
+## Overview
+
+These build rules are used for building [Docker](https://www.docker.com)
+images. Such images are easy to modify and deploy system image for
+deploying application easily on cloud providers.
+
+As traditional Dockerfile-based `docker build`s effectively execute a series
+of commands inside of Docker containers, saving the intermediate results as
+layers; this approach is unsuitable for use in Bazel for a variety of reasons.
+
+The docker_build rule constructs a tarball that is compatible with
+`docker save/load`, and creates a single layer out of each BUILD rule in the chain.
+
+* [Basic Example](#basic-example)
+* [Build Rule Reference](#reference)
+  * [`docker_build`](#docker_build)
+* [Future work](#future)
+
+<a name="basic-example"></a>
+## Basic Example
+
+Consider the following BUILD file in `//third_party/debian`:
+
+```python
+filegroup(
+    name = "ca_certificates",
+    srcs = ["ca_certificates.deb"],
+)
+
+# Example when you have all your dependencies in your repository.
+# We have an example on how to fetch them from the web later in this
+# document.
+filegroup(
+    name = "openjdk-7-jre-headless",
+    srcs = ["openjdk-7-jre-headless.deb"],
+)
+
+docker_build(
+    name = "wheezy",
+    tars = ["wheezy.tar"],
+)
+```
+
+The `wheezy` target in that BUILD file roughly corresponds to the Dockerfile:
+
+```docker
+FROM scratch
+ADD wheezy.tar /
+```
+
+You can then build up subsequent layers via:
+
+```python
+docker_build(
+    name = "base",
+    base = "//third_party/debian:wheezy",
+    debs = ["//third_party/debian:ca_certificates"],
+)
+
+docker_build(
+    name = "java",
+    base = ":base",
+    debs = ["//third_party/debian:openjdk-7-jre-headless"],
+)
+```
+
+## Metadata
+
+You can set layer metadata on these same rules by simply adding (supported) arguments to the rule, for instance:
+
+```python
+docker_build(
+    name = "my-layer",
+    entrypoint = ["foo", "bar", "baz"],
+    ...
+)
+```
+
+Will have a similar effect as the Dockerfile construct:
+
+```docker
+ENTRYPOINT ["foo", "bar", "baz"]
+```
+
+For the set of supported metadata, and ways to construct layers, see here.
+
+
+### Using
+
+Suppose you have a `docker_build` target `//my/image:helloworld`:
+
+```python
+docker_build(
+    name = "helloworld",
+    ...
+)
+```
+
+You can build this with `bazel build my/image:helloworld`.
+This will produce the file `bazel-genfiles/my/image/helloworld.tar`.
+You can load this into my local Docker client by running
+`docker load -i bazel-genfiles/my/image/helloworld.tar`, or simply
+`bazel run my/image:helloworld`.
+
+
+Upon success you should be able to run `docker images` and see:
+
+```
+REPOSITORY          TAG                 IMAGE ID       ...
+blaze/my_image      helloworld          d3440d7f2bde   ...
+```
+
+You can now use this docker image with the name `bazel/my_image:helloworld` or
+tag it with another name, for example:
+`docker tag bazel/my_image:helloworld gcr.io/my-project/my-awesome-image:v0.9`
+
+__Nota Bene:__ the `docker images` command will show a really old timestamp
+because `docker_build` remove all timestamps from the build to make it
+reproducible.
+
+## Pulling images and deb files from the internet
+
+If you do not want to check in base image in your repository, you can use
+[external repositories](http://bazel.io/docs/external.html). For instance,
+you could create various layer with `external` labels:
+
+```python
+load("/tools/build_defs/docker/docker", "docker_build")
+
+docker_build(
+    name = "java",
+    base = "@docker-debian//:wheezy",
+    debs = ["@openjdk-7-jre-headless//file"],
+)
+```
+
+Using the WORKSPACE file to add the actual files:
+
+```python
+new_http_archive(
+    name = "docker-debian",
+    url = "https://codeload.github.com/tianon/docker-brew-debian/zip/e9bafb113f432c48c7e86c616424cb4b2f2c7a51",
+    build_file = "debian.BUILD",
+    type = "zip",
+    sha256 = "515d385777643ef184729375bc5cb996134b3c1dc15c53acf104749b37334f68",
+)
+
+http_file(
+   name = "openjdk-7-jre-headless",
+   url = "http://security.debian.org/debian-security/pool/updates/main/o/openjdk-7/openjdk-7-jre-headless_7u79-2.5.5-1~deb7u1_amd64.deb",
+   sha256 = "b632f0864450161d475c012dcfcc37a1243d9ebf7ff9d6292150955616d71c23",
+)
+```
+
+With the following `debian.BUILD` file:
+
+```python
+load("/tools/build_defs/docker/docker", "docker_build")
+
+# Extract .xz files
+genrule(
+    name = "wheezy_tar",
+    srcs = ["docker-brew-debian-e9bafb113f432c48c7e86c616424cb4b2f2c7a51/wheezy/rootfs.tar.xz"],
+    outs = ["wheezy_tar.tar"],
+    cmd = "cat $< | xzcat >$@",
+)
+
+docker_build(
+    name = "wheezy",
+    tars = [":wheezy_tar"],
+    visibility = ["//visibility:public"],
+)
+```
+
+<a name="reference"></a>
+## Build Rule Reference [reference]
+
+<a name="docker_build"></a>
+### `docker_build`
+
+`docker_build(name, base, data_path, directory, files, tars, debs,
+symlinks, entrypoint, cmd, env, ports, volumes)`
+
+<table>
+  <thead>
+    <tr>
+      <th>Attribute</th>
+      <th>Description</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>name</code></td>
+      <td>
+        <code>Name, required</code>
+        <p>A unique name for this rule.</p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>base</code></td>
+      <td>
+        <code>File, optional</code>
+        <p>
+            The base layers on top of which to overlay this layer, equivalent to
+            FROM.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>data_path</code></td>
+      <td>
+        <code>String, optional</code>
+        <p>Root path of the files.</p>
+        <p>
+          The directory structure from the files is preserved inside the
+          docker image but a prefix path determined by `data_path`
+          is removed from the the directory structure. This path can
+          be absolute from the workspace root if starting with a `/` or
+          relative to the rule's directory. It is set to `.` by default.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>directory</code></td>
+      <td>
+        <code>String, optional</code>
+        <p>Target directory.</p>
+        <p>
+          The directory in which to expand the specified files, defaulting to '/'.
+          Only makes sense accompanying one of files/tars/debs.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>files</code></td>
+      <td>
+        <code>List of files, optional</code>
+        <p>File to add to the layer.</p>
+        <p>
+          A list of files that should be included in the docker image.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>tars</code></td>
+      <td>
+        <code>List of files, optional</code>
+        <p>Tar file to extract in the layer.</p>
+        <p>
+          A list of tar files whose content should be in the docker image.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>debs</code></td>
+      <td>
+        <code>List of files, optional</code>
+        <p>Debian package to install.</p>
+        <p>
+          A list of debian package that will be installed in the docker image.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>symlinks</code></td>
+      <td>
+        <code>Dictionary, optional</code>
+        <p>Symlinks to create in the docker image.</p>
+        <p>
+          <code>
+          symlinks = {
+           "/path/to/link": "/path/to/target",
+           ...
+          },
+          </code>
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>entrypoint</code></td>
+      <td>
+        <code>String or string list, optional</code>
+        <p><a href="https://docs.docker.com/reference/builder/#entrypoint">List
+               of entrypoints to add in the layer.</a></p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>cmd</code></td>
+      <td>
+        <code>String or string list, optional</code>
+        <p><a href="https://docs.docker.com/reference/builder/#cmd">List
+               of commands to execute in the layer.</a></p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>ports</code></td>
+      <td>
+        <code>String list, optional</code>
+        <p><a href="https://docs.docker.com/reference/builder/#expose">List
+               of ports to expose.</a></p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>volumes</code></td>
+      <td>
+        <code>String list, optional</code>
+        <p><a href="https://docs.docker.com/reference/builder/#volumes">List
+               of volumes to mount.</a></p>
+      </td>
+    </tr>
+  </tbody>
+  </tbody>
+</table>
+
+<a name="future"></a>
+# Future work
+
+In the future, we would like to provide better integration with docker
+repositories: pull and push docker image.
diff --git a/tools/build_defs/docker/archive.py b/tools/build_defs/docker/archive.py
new file mode 100644
index 0000000..0f9c428
--- /dev/null
+++ b/tools/build_defs/docker/archive.py
@@ -0,0 +1,194 @@
+# 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.
+"""Archive manipulation library for the Docker rules."""
+
+import os
+from StringIO import StringIO
+import tarfile
+
+
+class SimpleArFile(object):
+  """A simple AR file reader.
+
+  This enable to read AR file (System V variant) as described
+  in https://en.wikipedia.org/wiki/Ar_(Unix).
+
+  The standard usage of this class is:
+
+  with SimpleArFile(filename) as ar:
+    nextFile = ar.next()
+    while nextFile:
+      print nextFile.filename
+      nextFile = ar.next()
+
+  Upon error, this class will raise a ArError exception.
+  """
+  # TODO(dmarting): We should use a standard library instead but python 2.7
+  #   does not have AR reading library.
+
+  class ArError(Exception):
+    pass
+
+  class SimpleArFileEntry(object):
+    """Represent one entry in a AR archive.
+
+    Attributes:
+      filename: the filename of the entry, as described in the archive.
+      timestamp: the timestamp of the file entry.
+      owner_id, group_id: numeric id of the user and group owning the file.
+      mode: unix permission mode of the file
+      size: size of the file
+      data: the content of the file.
+    """
+
+    def __init__(self, f):
+      self.filename = f.read(16).strip()
+      if self.filename.endswith('/'):  # SysV variant
+        self.filename = self.filename[:-1]
+      self.timestamp = int(f.read(12).strip())
+      self.owner_id = int(f.read(6).strip())
+      self.group_id = int(f.read(6).strip())
+      self.mode = int(f.read(8).strip(), 8)
+      self.size = int(f.read(10).strip())
+      if f.read(2) != '\x60\x0a':
+        raise self.ArError('Invalid AR file header')
+      self.data = f.read(self.size)
+
+  MAGIC_STRING = '!<arch>\n'
+
+  def __init__(self, filename):
+    self.filename = filename
+
+  def __enter__(self):
+    self.f = open(self.filename, 'rb')
+    if self.f.read(len(self.MAGIC_STRING)) != self.MAGIC_STRING:
+      raise self.ArError('Not a ar file: ' + self.filename)
+    return self
+
+  def __exit__(self, t, v, traceback):
+    self.f.close()
+
+  def next(self):
+    """Read the next file. Returns None when reaching the end of file."""
+    if self.f.tell() == os.fstat(self.f.fileno()).st_size:
+      return None
+    return self.SimpleArFileEntry(self.f)
+
+
+class TarFileWriter(object):
+  """A wrapper to write tar files."""
+
+  def __init__(self, name):
+    self.tar = tarfile.open(name=name, mode='w')
+
+  def __enter__(self):
+    return self
+
+  def __exit__(self, t, v, traceback):
+    self.close()
+
+  def add_file(self, name, kind=tarfile.REGTYPE, content=None, link=None,
+               file_content=None, uid=0, gid=0, uname='', gname='', mtime=0,
+               mode=None):
+    """Add a file to the current tar.
+
+    Args:
+      name: the name of the file to add.
+      kind: the type of the file to add, see tarfile.*TYPE.
+      content: a textual content to put in the file.
+      link: if the file is a link, the destination of the link.
+      file_content: file to read the content from. Provide either this
+          one or `content` to specifies a content for the file.
+      uid: owner user identifier.
+      gid: owner group identifier.
+      uname: owner user names.
+      gname: owner group names.
+      mtime: modification time to put in the archive.
+      mode: unix permission mode of the file, default 0644 (0755).
+    """
+    if not name.startswith('.') and not name.startswith('/'):
+      name = './' + name
+    tarinfo = tarfile.TarInfo(name)
+    tarinfo.mtime = mtime
+    tarinfo.uid = uid
+    tarinfo.gid = gid
+    tarinfo.uname = uname
+    tarinfo.gname = gname
+    tarinfo.type = kind
+    if mode is None:
+      tarinfo.mode = 0644 if kind == tarfile.REGTYPE else 0755
+    else:
+      tarinfo.mode = mode
+    if link:
+      tarinfo.linkname = link
+    if content:
+      tarinfo.size = len(content)
+      self.tar.addfile(tarinfo, StringIO(content))
+    elif file_content:
+      with open(file_content, 'rb') as f:
+        tarinfo.size = os.fstat(f.fileno()).st_size
+        self.tar.addfile(tarinfo, f)
+    else:
+      self.tar.addfile(tarinfo)
+
+  def add_tar(self, tar, rootuid=None, rootgid=None,
+              numeric=False, name_filter=None):
+    """Merge a tar content into the current tar, stripping timestamp.
+
+    Args:
+      tar: the name of tar to extract and put content into the current tar.
+      rootuid: user id that we will pretend is root (replaced by uid 0).
+      rootgid: group id that we will pretend is root (replaced by gid 0).
+      numeric: set to true to strip out name of owners (and just use the
+          numeric values).
+      name_filter: filter out file by names. If not none, this method will be
+          called for each file to add, given the name and should return true if
+          the file is to be added to the final tar and false otherwise.
+    """
+    compression = os.path.splitext(tar)[-1][1:]
+    if compression == 'tgz':
+      compression = 'gz'
+    elif compression == 'bzip2':
+      compression = 'bz2'
+    elif compression not in ['gz', 'bz2']:
+      # Unfortunately xz format isn't supported in py 2.7 :(
+      compression = ''
+    with tarfile.open(name=tar, mode='r:' + compression) as intar:
+      for tarinfo in intar:
+        if name_filter is None or name_filter(tarinfo.name):
+          tarinfo.mtime = 0
+          if rootuid is not None and tarinfo.uid == rootuid:
+            tarinfo.uid = 0
+            tarinfo.uname = 'root'
+          if rootgid is not None and tarinfo.gid == rootgid:
+            tarinfo.gid = 0
+            tarinfo.gname = 'root'
+          if numeric:
+            tarinfo.uname = ''
+            tarinfo.gname = ''
+          name = tarinfo.name
+          if not name.startswith('/') and not name.startswith('.'):
+            tarinfo.name = './' + name
+
+          if tarinfo.isfile():
+            self.tar.addfile(tarinfo, intar.extractfile(tarinfo.name))
+          else:
+            self.tar.addfile(tarinfo)
+
+  def close(self):
+    """Close the output tar file.
+
+    This class should not be used anymore after calling that method.
+    """
+    self.tar.close()
diff --git a/tools/build_defs/docker/build_layer.py b/tools/build_defs/docker/build_layer.py
new file mode 100644
index 0000000..b9aa431
--- /dev/null
+++ b/tools/build_defs/docker/build_layer.py
@@ -0,0 +1,167 @@
+# 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.
+"""This tool build docker layer tar file from a list of inputs."""
+
+import os
+import os.path
+import sys
+import tarfile
+import tempfile
+
+from tools.build_defs.docker import archive
+from third_party.py import gflags
+
+gflags.DEFINE_string(
+    'output', None,
+    'The output file, mandatory')
+gflags.MarkFlagAsRequired('output')
+
+gflags.DEFINE_multistring(
+    'file', [],
+    'A file to add to the layer')
+
+gflags.DEFINE_multistring(
+    'tar', [],
+    'A tar file to add to the layer')
+
+gflags.DEFINE_multistring(
+    'deb', [],
+    'A debian package to add to the layer')
+
+gflags.DEFINE_multistring(
+    'link', [],
+    'Add a symlink a inside the layer ponting to b if a:b is specified')
+gflags.RegisterValidator(
+    'link',
+    lambda l: all(value.find(':') > 0 for value in l),
+    message='--link value should contains a : separator')
+
+gflags.DEFINE_string(
+    'file_path', '',
+    'The path to strip from the files passed using the --file option')
+
+gflags.DEFINE_string(
+    'directory', None,
+    'Directory in which to store the file inside the layer')
+
+FLAGS = gflags.FLAGS
+
+
+class DockerLayer(object):
+  """A class to generates a Docker layer."""
+
+  class DebError(Exception):
+    pass
+
+  def __init__(self, output, file_path, directory):
+    self.file_path = file_path
+    self.directory = directory
+    self.output = output
+
+  def __enter__(self):
+    self.tarfile = archive.TarFileWriter(self.output)
+    return self
+
+  def __exit__(self, t, v, traceback):
+    self.tarfile.close()
+
+  def add_file(self, f):
+    """Add a file to the layer.
+
+    Args:
+       f: the file to add to the layer
+
+    `f` will be copied into the `self.directory` sub-directory of the layer.
+    Directory structure beyond `self.file_path` will be preserved (so, if
+    `f`'s path is `a/b/c/file`, `directory` is `d/e` and `file_path` is `a/b`,
+    the path of the file in the layer will be `d/e/c/file`).
+    """
+    dest = f
+    # TODO(mattmoor): Consider applying the working directory to all four
+    # options, not just files...
+    if dest.startswith(self.file_path):
+      dest = dest[len(self.file_path):]
+    if self.directory:
+      dest = self.directory + '/' + dest
+    dest = dest.lstrip('/')  # Remove leading slashes
+    self.tarfile.add_file(dest, file_content=f)
+
+  def add_tar(self, tar):
+    """Add a tar file to the layer.
+
+    All files presents in that tar will be added to the layer under
+    the same paths. The current user uid and gid will be replaced
+    by 0 (to make like we are running as root) and no user name nor
+    group names will be added to the layer.
+
+    Args:
+      tar: the tar file to add to the layer
+    """
+    self.tarfile.add_tar(tar,
+                         rootuid=os.getuid(),
+                         rootgid=os.getgid(),
+                         numeric=True)
+
+  def add_link(self, symlink, destination):
+    """Add a symbolic link pointing to `destination` in the layer.
+
+    Args:
+      symlink: the name of the symbolic link to add.
+      destination: where the symbolic link point to.
+    """
+    self.tarfile.add_file(symlink, tarfile.SYMTYPE, link=destination)
+
+  def add_deb(self, deb):
+    """Extract a debian package in the layer.
+
+    All files presents in that debian package will be added to the
+    layer under the same paths. No user name nor group names will
+    be added to the layer.
+
+    Args:
+      deb: the tar file to add to the layer
+
+    Raises:
+      DebError: if the format of the deb archive is incorrect.
+
+    This method does not support LZMA (data.tar.xz or data.tar.lzma)
+    for the data in the deb package. Using Python 3 would fix it.
+    """
+    with archive.SimpleArFile(deb) as arfile:
+      current = arfile.next()
+      while current and not current.filename.startswith('data.'):
+        current = arfile.next()
+      if not current:
+        raise self.DebError(deb + ' does not contains a data file!')
+      tmpfile = tempfile.mkstemp(suffix=os.path.splitext(current.filename)[-1])
+      with open(tmpfile[1], 'wb') as f:
+        f.write(current.data)
+      self.add_tar(tmpfile[1])
+      os.remove(tmpfile[1])
+
+
+def main(unused_argv):
+  with DockerLayer(FLAGS.output, FLAGS.file_path, FLAGS.directory) as layer:
+    for f in FLAGS.file:
+      layer.add_file(f)
+    for tar in FLAGS.tar:
+      layer.add_tar(tar)
+    for deb in FLAGS.deb:
+      layer.add_deb(deb)
+    for link in FLAGS.link:
+      l = link.split(':', 1)
+      layer.add_link(l[0], l[1])
+
+if __name__ == '__main__':
+  main(FLAGS(sys.argv))
diff --git a/tools/build_defs/docker/build_test.sh b/tools/build_defs/docker/build_test.sh
new file mode 100755
index 0000000..3cd3cfe
--- /dev/null
+++ b/tools/build_defs/docker/build_test.sh
@@ -0,0 +1,279 @@
+#!/bin/bash
+
+# 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.
+
+# Unit tests for docker_build
+
+DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+source ${DIR}/testenv.sh || { echo "testenv.sh not found!" >&2; exit 1; }
+
+readonly PLATFORM="$(uname -s | tr 'A-Z' 'a-z')"
+if [ "${PLATFORM}" = "darwin" ]; then
+  readonly MAGIC_TIMESTAMP="$(date -r 0 "+%b %e  %Y")"
+else
+  readonly MAGIC_TIMESTAMP="$(date --date=@0 "+%F %R")"
+fi
+
+function EXPECT_CONTAINS() {
+  local complete="${1}"
+  local substring="${2}"
+  local message="${3:-Expected '${substring}' not found in '${complete}'}"
+
+  echo "${complete}" | grep -Fsq -- "${substring}" \
+    || fail "$message"
+}
+
+function check_property() {
+  local property="${1}"
+  local tarball="${2}"
+  local layer="${3}"
+  local expected="${4}"
+  local test_data="${TEST_DATA_DIR}/${tarball}.tar"
+
+  local metadata="$(tar xOf "${test_data}" "./${layer}/json")"
+
+  # This would be much more accurate if we had 'jq' everywhere.
+  EXPECT_CONTAINS "${metadata}" "\"${property}\": ${expected}"
+}
+
+function check_no_property() {
+  local property="${1}"
+  local tarball="${2}"
+  local layer="${3}"
+  local test_data="${TEST_DATA_DIR}/${tarball}.tar"
+
+  tar xOf "${test_data}" "./${layer}/json" >$TEST_log
+  expect_not_log "\"${property}\":"
+}
+
+function check_size() {
+  check_property Size "${@}"
+}
+
+function check_id() {
+  check_property id "${@}"
+}
+
+function check_parent() {
+  check_property parent "${@}"
+}
+
+function check_entrypoint() {
+  check_property Entrypoint "${@}"
+}
+
+function check_cmd() {
+  check_property Cmd "${@}"
+}
+
+function check_ports() {
+  check_property ExposedPorts "${@}"
+}
+
+function check_volumes() {
+  check_property Volumes "${@}"
+}
+
+function check_env() {
+  check_property Env "${@}"
+}
+
+function check_layers() {
+  local input=${1}
+  shift 1
+  local expected_layers=(${*})
+
+  local expected_layers_sorted=(
+    $(for i in ${expected_layers[*]}; do echo $i; done | sort)
+  )
+  local test_data="${TEST_DATA_DIR}/${input}.tar"
+
+  # Verbose output for testing.
+  tar tvf "${test_data}"
+
+  local actual_layers=(
+    $(tar tvf ${test_data} | tr -s ' ' | cut -d' ' -f 4- | sort \
+      | cut -d'/' -f 2 | grep -E '^[0-9a-f]+$' | sort | uniq))
+
+  # Verbose output for testing.
+  echo Expected: ${expected_layers_sorted[@]}
+  echo Actual: ${actual_layers[@]}
+
+  check_eq "${#expected_layers[@]}" "${#actual_layers[@]}"
+
+  local index=0
+  local parent=
+  while [ "${index}" -lt "${#expected_layers[@]}" ]
+  do
+    # Check that the nth sorted layer matches
+    check_eq "${expected_layers_sorted[$index]}" "${actual_layers[$index]}"
+
+    # Grab the ordered layer and check it.
+    local layer="${expected_layers[$index]}"
+
+    # Verbose output for testing.
+    echo Checking layer: "${layer}"
+
+    local listing="$(tar xOf "${test_data}" "./${layer}/layer.tar" | tar tv)"
+
+    # Check that all files in the layer, if any, have the magic timestamp
+    check_eq "$(echo "${listing}" | grep -Fv "${MAGIC_TIMESTAMP}")" ""
+
+    check_id "${input}" "${layer}" "\"${layer}\""
+
+    # Check that the layer contains its predecessor as its parent in the JSON.
+    if [[ -n "${parent}" ]]; then
+      check_parent "${input}" "${layer}" "\"${parent}\""
+    fi
+
+    # Check that the layer's size metadata matches the layer's tarball's size.
+    local layer_size=$(tar xOf "${test_data}" "./${layer}/layer.tar" | wc -c | xargs)
+    check_size "${input}" "${layer}" "${layer_size}"
+
+    index=$((index + 1))
+    parent=$layer
+  done
+}
+
+function test_files_base() {
+  check_layers "files_base" \
+    "240dd12c02aee796394ce18eee3108475f7d544294b17fc90ec54e983601fe1b"
+}
+
+function test_files_with_files_base() {
+  check_layers "files_with_files_base" \
+    "240dd12c02aee796394ce18eee3108475f7d544294b17fc90ec54e983601fe1b" \
+    "a9fd8cab2b9831ca2a13f371c04667a7698ef3baa90f3e820c4568d774cc69ab"
+}
+
+function test_tar_base() {
+  check_layers "tar_base" \
+    "83e8285de55c00f74f45628f75aec4366b361913be486e2e96af1a7b05211094"
+
+  # Check that this layer doesn't have any entrypoint data by looking
+  # for *any* entrypoint.
+  check_no_property "Entrypoint" "tar_base" \
+    "83e8285de55c00f74f45628f75aec4366b361913be486e2e96af1a7b05211094"
+}
+
+function test_tar_with_tar_base() {
+  check_layers "tar_with_tar_base" \
+    "83e8285de55c00f74f45628f75aec4366b361913be486e2e96af1a7b05211094" \
+    "f2878819ee41f261d2ed346e92c1fc2096e9eaa51e3e1fb32c7da1a21be77029"
+}
+
+function test_files_with_tar_base() {
+  check_layers "files_with_tar_base" \
+    "83e8285de55c00f74f45628f75aec4366b361913be486e2e96af1a7b05211094" \
+    "c96f2793f6ade79f8f4a4cfe46f31752de14f3b1eae7f27aa0c7440f78f612f3"
+}
+
+function test_tar_with_files_base() {
+  check_layers "tar_with_files_base" \
+    "240dd12c02aee796394ce18eee3108475f7d544294b17fc90ec54e983601fe1b" \
+    "2f1d1cc52ab8e72bf5affcac1a68a86c7f75679bf58a2b2a6fefdbfa0d239651"
+}
+
+function test_base_with_entrypoint() {
+  check_layers "base_with_entrypoint" \
+    "3cf09865c613d49e5fa6a1f7027744e51da662139ea833f8e757f70c8f75a554"
+
+  check_entrypoint "base_with_entrypoint" \
+    "3cf09865c613d49e5fa6a1f7027744e51da662139ea833f8e757f70c8f75a554" \
+    '["/bar"]'
+
+  # Check that the base layer has a port exposed.
+  check_ports "base_with_entrypoint" \
+    "3cf09865c613d49e5fa6a1f7027744e51da662139ea833f8e757f70c8f75a554" \
+    '{"8080/tcp": {}}'
+}
+
+function test_derivative_with_shadowed_cmd() {
+  check_layers "derivative_with_shadowed_cmd" \
+    "3cf09865c613d49e5fa6a1f7027744e51da662139ea833f8e757f70c8f75a554" \
+    "46e302dc2cb5c19baaeb479e8142ab1bb12ca77b3d7a0ecd379304413e6c5b28"
+}
+
+function test_derivative_with_cmd() {
+  check_layers "derivative_with_cmd" \
+    "3cf09865c613d49e5fa6a1f7027744e51da662139ea833f8e757f70c8f75a554" \
+    "46e302dc2cb5c19baaeb479e8142ab1bb12ca77b3d7a0ecd379304413e6c5b28" \
+    "968891207e14ab79a7ab3c71c796b88a4321ec30b9a74feb1d7c92d5a47c8bc2"
+
+  check_entrypoint "derivative_with_cmd" \
+    "968891207e14ab79a7ab3c71c796b88a4321ec30b9a74feb1d7c92d5a47c8bc2" \
+    '["/bar"]'
+
+  # Check that the middle layer has our shadowed arg.
+  check_cmd "derivative_with_cmd" \
+    "46e302dc2cb5c19baaeb479e8142ab1bb12ca77b3d7a0ecd379304413e6c5b28" \
+    '["shadowed-arg"]'
+
+  # Check that our topmost layer excludes the shadowed arg.
+  check_cmd "derivative_with_cmd" \
+    "968891207e14ab79a7ab3c71c796b88a4321ec30b9a74feb1d7c92d5a47c8bc2" \
+    '["arg1", "arg2"]'
+
+  # Check that the topmost layer has the ports exposed by the bottom
+  # layer, and itself.
+  check_ports "derivative_with_cmd" \
+    "968891207e14ab79a7ab3c71c796b88a4321ec30b9a74feb1d7c92d5a47c8bc2" \
+    '{"80/tcp": {}, "8080/tcp": {}}'
+}
+
+function test_derivative_with_volume() {
+  check_layers "derivative_with_volume" \
+    "f86da639a9346bec6d3a821ad1f716a177a8ff8f71d66f8b70238ce7e7ba51b8" \
+    "839bbd055b732c784847b3ec112d88c94f3bb752147987daef916bc956f9adf0"
+
+  # Check that the topmost layer has the ports exposed by the bottom
+  # layer, and itself.
+  check_volumes "derivative_with_volume" \
+    "f86da639a9346bec6d3a821ad1f716a177a8ff8f71d66f8b70238ce7e7ba51b8" \
+    '{"/logs": {}}'
+
+  check_volumes "derivative_with_volume" \
+    "839bbd055b732c784847b3ec112d88c94f3bb752147987daef916bc956f9adf0" \
+    '{"/asdf": {}, "/blah": {}, "/logs": {}}'
+}
+
+function test_generated_tarball() {
+  check_layers "generated_tarball" \
+    "54b8328604115255cc76c12a2a51939be65c40bf182ff5a898a5fb57c38f7772"
+}
+
+function test_with_env() {
+  check_layers "with_env" \
+    "f86da639a9346bec6d3a821ad1f716a177a8ff8f71d66f8b70238ce7e7ba51b8" \
+    "80b94376a90de45256c3e94c82bc3812bc5cbd05b7d01947f29e6805e8cd7018"
+
+  check_env "with_env" \
+    "80b94376a90de45256c3e94c82bc3812bc5cbd05b7d01947f29e6805e8cd7018" \
+    '["bar=blah blah blah", "foo=/asdf"]'
+}
+
+function test_with_double_env() {
+  check_layers "with_double_env" \
+    "f86da639a9346bec6d3a821ad1f716a177a8ff8f71d66f8b70238ce7e7ba51b8" \
+    "80b94376a90de45256c3e94c82bc3812bc5cbd05b7d01947f29e6805e8cd7018" \
+    "548e1d847a1d051e3cb3af383b0ebe40d341c01c97e735ae5a78ee3e10353b93"
+
+  # Check both the aggregation and the expansion of embedded variables.
+  check_env "with_double_env" \
+    "548e1d847a1d051e3cb3af383b0ebe40d341c01c97e735ae5a78ee3e10353b93" \
+    '["bar=blah blah blah", "baz=/asdf blah blah blah", "foo=/asdf"]'
+}
+
+run_suite "build_test"
diff --git a/tools/build_defs/docker/create_image.py b/tools/build_defs/docker/create_image.py
new file mode 100644
index 0000000..b86bc11
--- /dev/null
+++ b/tools/build_defs/docker/create_image.py
@@ -0,0 +1,137 @@
+# 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.
+"""This tool creates a docker image from a layer and the various metadata."""
+
+import sys
+import tarfile
+
+from tools.build_defs.docker import archive
+from third_party.py import gflags
+
+# Hardcoded docker versions that we are claiming to be.
+DATA_FORMAT_VERSION = '1.0'
+
+gflags.DEFINE_string(
+    'output', None,
+    'The output file, mandatory')
+gflags.MarkFlagAsRequired('output')
+
+gflags.DEFINE_string(
+    'metadata', None,
+    'The JSON metadata file for this image, mandatory.')
+gflags.MarkFlagAsRequired('metadata')
+
+gflags.DEFINE_string(
+    'layer', None,
+    'The tar file for the top layer of this image, mandatory.')
+gflags.MarkFlagAsRequired('metadata')
+
+gflags.DEFINE_string(
+    'id', None,
+    'The hex identifier of this image (hexstring or @filename), mandatory.')
+gflags.MarkFlagAsRequired('id')
+
+gflags.DEFINE_string(
+    'base', None,
+    'The base image file for this image.')
+
+gflags.DEFINE_string(
+    'repository', None,
+    'The name of the repository to add this image.')
+
+gflags.DEFINE_string(
+    'name', None,
+    'The symbolic name of this image.')
+
+FLAGS = gflags.FLAGS
+
+
+def _base_name_filter(name):
+  """Do not add multiple times 'top' and 'repositories' when merging images."""
+  filter_names = ['top', 'repositories']
+  return all([not name.endswith(s) for s in filter_names])
+
+
+def create_image(output, identifier,
+                 base=None, layer=None, metadata=None,
+                 name=None, repository=None):
+  """Creates a Docker image.
+
+  Args:
+    output: the name of the docker image file to create.
+    identifier: the identifier of the top layer for this image.
+    base: a base layer (optional) to merge to current layer.
+    layer: the layer content (a tar file).
+    metadata: the json metadata file for the top layer.
+    name: symbolic name for this docker image.
+    repository: repository name for this docker image.
+  """
+  tar = archive.TarFileWriter(output)
+  # Write our id to 'top' as we are now the topmost layer.
+  tar.add_file('top', content=identifier)
+  # Each layer is encoded as a directory in the larger tarball of the form:
+  #  {id}\
+  #    layer.tar
+  #    VERSION
+  #    json
+  # Create the directory for us to now fill in.
+  tar.add_file(identifier + '/', tarfile.DIRTYPE)
+  # VERSION generally seems to contain 1.0, not entirely sure
+  # what the point of this is.
+  tar.add_file(identifier + '/VERSION', content=DATA_FORMAT_VERSION)
+  # Add the layer file
+  tar.add_file(identifier + '/layer.tar', file_content=layer)
+  # Now the json metadata
+  tar.add_file(identifier + '/json', file_content=metadata)
+  # Merge the base if any
+  if base:
+    tar.add_tar(base, name_filter=_base_name_filter)
+  # In addition to N layers of the form described above, there is
+  # a single file at the top of the image called repositories.
+  # This file contains a JSON blob of the form:
+  # {
+  #   'repo':{
+  #     'tag-name': 'top-most layer hex',
+  #     ...
+  #   },
+  #   ...
+  # }
+  if repository:
+    tar.add_file('repositories', content='\n'.join([
+        '{',
+        '  "%s": {' % repository,
+        '    "%s": "%s"' % (name, identifier),
+        '  }',
+        '}']))
+
+
+# Main program to create a docker image. It expect to be run with:
+# create_image --output=output_file \
+#              --id=@identifier \
+#              [--base=base] \
+#              --layer=layer.tar \
+#              --metadata=metadata.json \
+#              --name=myname --repository=repositoryName
+# See the gflags declaration about the flags argument details.
+def main(unused_argv):
+  identifier = FLAGS.id
+  if identifier.startswith('@'):
+    with open(identifier[1:], 'r') as f:
+      identifier = f.read()
+  create_image(FLAGS.output, identifier, FLAGS.base,
+               FLAGS.layer, FLAGS.metadata,
+               FLAGS.name, FLAGS.repository)
+
+if __name__ == '__main__':
+  main(FLAGS(sys.argv))
diff --git a/tools/build_defs/docker/docker.bzl b/tools/build_defs/docker/docker.bzl
new file mode 100644
index 0000000..6ad6d4f
--- /dev/null
+++ b/tools/build_defs/docker/docker.bzl
@@ -0,0 +1,360 @@
+# 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 _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 = [
+      "--file_path=" + data_path,
+      "--output=" + layer.path,
+      "--directory=" + ctx.attr.directory
+      ]
+  args += ["--file=" + f.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)
diff --git a/tools/build_defs/docker/rewrite_json.py b/tools/build_defs/docker/rewrite_json.py
new file mode 100644
index 0000000..a189971
--- /dev/null
+++ b/tools/build_defs/docker/rewrite_json.py
@@ -0,0 +1,225 @@
+# 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.
+"""This package manipulates Docker image layer metadata."""
+from collections import namedtuple
+import copy
+import json
+import os
+import os.path
+import sys
+import tarfile
+
+from third_party.py import gflags
+
+gflags.DEFINE_string(
+    'name', None, 'The name of the current layer')
+
+gflags.DEFINE_string(
+    'base', None, 'The parent image')
+
+gflags.DEFINE_string(
+    'output', None, 'The output file to generate')
+
+gflags.DEFINE_string(
+    'layer', None, 'The current layer tar')
+
+gflags.DEFINE_list(
+    'entrypoint', None,
+    'Override the "Entrypoint" of the previous layer')
+
+gflags.DEFINE_list(
+    'command', None,
+    'Override the "Cmd" of the previous layer')
+
+gflags.DEFINE_list(
+    'ports', None,
+    'Augment the "ExposedPorts" of the previous layer')
+
+gflags.DEFINE_list(
+    'volumes', None,
+    'Augment the "Volumes" of the previous layer')
+
+gflags.DEFINE_list(
+    'env', None,
+    'Augment the "Env" of the previous layer')
+
+FLAGS = gflags.FLAGS
+
+_MetadataOptionsT = namedtuple(
+    'MetadataOptionsT',
+    ['name', 'parent', 'size', 'entrypoint', 'cmd', 'env', 'ports', 'volumes'])
+
+
+class MetadataOptions(_MetadataOptionsT):
+  """Docker image layer metadata options."""
+
+  def __new__(cls, name=None, parent=None, size=None,
+              entrypoint=None, cmd=None, env=None,
+              ports=None, volumes=None):
+    """Constructor."""
+    return super(MetadataOptions, cls).__new__(
+        cls, name=name, parent=parent, size=size,
+        entrypoint=entrypoint, cmd=cmd, env=env,
+        ports=ports, volumes=volumes)
+
+
+_DOCKER_VERSION = '1.5.0'
+
+_PROCESSOR_ARCHITECTURE = 'amd64'
+
+_OPERATING_SYSTEM = 'linux'
+
+
+def Resolve(value, environment):
+  """Resolves environment variables embedded in the given value."""
+  outer_env = os.environ
+  try:
+    os.environ = environment
+    return os.path.expandvars(value)
+  finally:
+    os.environ = outer_env
+
+
+def RewriteMetadata(data, options):
+  """Rewrite and return a copy of the input data according to options.
+
+  Args:
+    data: The dict of Docker image layer metadata we're copying and rewriting.
+    options: The changes this layer makes to the overall image's metadata, which
+             first appears in this layer's version of the metadata
+
+  Returns:
+    A deep copy of data, which has been updated to reflect the metadata
+    additions of this layer.
+
+  Raises:
+    Exception: a required option was missing.
+  """
+  output = copy.deepcopy(data)
+
+  if not options.name:
+    raise Exception('Missing required option: name')
+  output['id'] = options.name
+
+  if options.parent:
+    output['parent'] = options.parent
+  elif data:
+    raise Exception('Expected empty input object when parent is omitted')
+
+  if options.size:
+    output['Size'] = options.size
+  elif 'Size' in output:
+    del output['Size']
+
+  if 'config' not in output:
+    output['config'] = {}
+
+  if options.entrypoint:
+    output['config']['Entrypoint'] = options.entrypoint
+  if options.cmd:
+    output['config']['Cmd'] = options.cmd
+
+  output['docker_version'] = _DOCKER_VERSION
+  output['architecture'] = _PROCESSOR_ARCHITECTURE
+  output['os'] = _OPERATING_SYSTEM
+
+  if options.env:
+    environ_dict = {}
+    # Build a dictionary of existing environment variables (used by Resolve).
+    for kv in output['config'].get('Env', []):
+      (k, v) = kv.split('=', 1)
+      environ_dict[k] = v
+    # Merge in new environment variables, resolving references.
+    for kv in options.env:
+      (k, v) = kv.split('=', 1)
+      # Resolve handles scenarios like "PATH=$PATH:...".
+      v = Resolve(v, environ_dict)
+      environ_dict[k] = v
+    output['config']['Env'] = [
+        '%s=%s' % (k, environ_dict[k]) for k in sorted(environ_dict.keys())]
+
+  if options.ports:
+    if 'ExposedPorts' not in output['config']:
+      output['config']['ExposedPorts'] = {}
+    for p in options.ports:
+      if '/' in p:
+        # The port spec has the form 80/tcp, 1234/udp
+        # so we simply use it as the key.
+        output['config']['ExposedPorts'][p] = {}
+      else:
+        # Assume tcp
+        output['config']['ExposedPorts'][p + '/tcp'] = {}
+
+  if options.volumes:
+    if 'Volumes' not in output['config']:
+      output['config']['Volumes'] = {}
+    for p in options.volumes:
+      output['config']['Volumes'][p] = {}
+
+  # TODO(mattmoor): comment, created, container_config
+
+  # container_config contains information about the container
+  # that was used to create this layer, so it shouldn't
+  # propagate from the parent to child.  This is where we would
+  # annotate information that can be extract by tools like Blubber
+  # or Quay.io's UI to gain insight into the source that generated
+  # the layer.  A Dockerfile might produce something like:
+  #   # (nop) /bin/sh -c "apt-get update"
+  # We might consider encoding the fully-qualified bazel build target:
+  #  //tools/build_defs/docker:image
+  # However, we should be sensitive to leaking data through this field.
+  if 'container_config' in output:
+    del output['container_config']
+
+  return output
+
+
+def GetTarFile(f, name):
+  """Return the content of a file inside a tar file."""
+  with tarfile.open(f, 'r') as tar:
+    tarinfo = tar.getmember(name)
+    if not tarinfo:
+      return ''
+    return tar.extractfile(tarinfo).read()
+
+
+def main(unused_argv):
+  parent = ''
+  base_json = '{}'
+  if FLAGS.base:
+    parent = GetTarFile(FLAGS.base, './top')
+    base_json = GetTarFile(FLAGS.base, './%s/json' % parent)
+  data = json.loads(base_json)
+
+  name = FLAGS.name
+  if name.startswith('@'):
+    with open(name[1:], 'r') as f:
+      name = f.read()
+
+  output = RewriteMetadata(data, MetadataOptions(
+      name=name,
+      parent=parent,
+      size=os.path.getsize(FLAGS.layer),
+      entrypoint=FLAGS.entrypoint,
+      cmd=FLAGS.command,
+      env=FLAGS.env,
+      ports=FLAGS.ports,
+      volumes=FLAGS.volumes))
+
+  with open(FLAGS.output, 'w') as fp:
+    json.dump(output, fp, sort_keys=True)
+    fp.write('\n')
+
+if __name__ == '__main__':
+  main(FLAGS(sys.argv))
diff --git a/tools/build_defs/docker/rewrite_json_test.py b/tools/build_defs/docker/rewrite_json_test.py
new file mode 100644
index 0000000..8bb711d
--- /dev/null
+++ b/tools/build_defs/docker/rewrite_json_test.py
@@ -0,0 +1,621 @@
+# 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.
+"""Testing for rewrite_json."""
+
+import unittest
+
+from tools.build_defs.docker.rewrite_json import _DOCKER_VERSION
+from tools.build_defs.docker.rewrite_json import _OPERATING_SYSTEM
+from tools.build_defs.docker.rewrite_json import _PROCESSOR_ARCHITECTURE
+from tools.build_defs.docker.rewrite_json import MetadataOptions
+from tools.build_defs.docker.rewrite_json import RewriteMetadata
+
+
+class RewriteJsonTest(unittest.TestCase):
+  """Testing for rewrite_json."""
+
+  def testNewEntrypoint(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    entrypoint = ['/bin/bash']
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': entrypoint
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, entrypoint=entrypoint, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOverrideEntrypoint(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/sh', 'does', 'not', 'matter'],
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    entrypoint = ['/bin/bash']
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': entrypoint
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, entrypoint=entrypoint, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testNewCmd(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/bash'],
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    cmd = ['/bin/bash']
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/bash'],
+            'Cmd': cmd
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, cmd=cmd, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOverrideCmd(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/bash'],
+            'Cmd': ['does', 'not', 'matter'],
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    cmd = ['does', 'matter']
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/bash'],
+            'Cmd': cmd
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, cmd=cmd, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOverrideBoth(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': ['/bin/sh'],
+            'Cmd': ['does', 'not', 'matter'],
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    entrypoint = ['/bin/bash', '-c']
+    cmd = ['my-command', 'my-arg1', 'my-arg2']
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Entrypoint': entrypoint,
+            'Cmd': cmd
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, entrypoint=entrypoint, cmd=cmd, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOverrideParent(self):
+    name = 'me!'
+    parent = 'parent'
+    # In the typical case, we expect the parent to
+    # come in as the 'id', and our grandparent to
+    # be its 'parent'.
+    in_data = {
+        'id': parent,
+        'parent': 'grandparent',
+    }
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {},
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testNewSize(self):
+    # Size is one of the few fields that, when omitted,
+    # should be removed.
+    in_data = {
+        'id': 'you',
+        'Size': '124',
+    }
+    name = 'me'
+    parent = 'blah'
+    size = '4321'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'Size': size,
+        'config': {},
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, size=size, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOmitSize(self):
+    # Size is one of the few fields that, when omitted,
+    # should be removed.
+    in_data = {
+        'id': 'you',
+        'Size': '124',
+    }
+    name = 'me'
+    parent = 'blah'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {},
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testOmitName(self):
+    # Name is required.
+    with self.assertRaises(Exception):
+      RewriteMetadata({}, MetadataOptions(name=None))
+
+  def testStripContainerConfig(self):
+    # Size is one of the few fields that, when omitted,
+    # should be removed.
+    in_data = {
+        'id': 'you',
+        'container_config': {},
+    }
+    name = 'me'
+    parent = 'blah'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {},
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testEmptyBase(self):
+    in_data = {}
+    name = 'deadbeef'
+    entrypoint = ['/bin/bash', '-c']
+    cmd = ['my-command', 'my-arg1', 'my-arg2']
+    size = '999'
+    expected = {
+        'id': name,
+        'config': {
+            'Entrypoint': entrypoint,
+            'Cmd': cmd,
+            'ExposedPorts': {
+                '80/tcp': {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+        'Size': size,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, entrypoint=entrypoint, cmd=cmd, size=size,
+        ports=['80']))
+    self.assertEquals(expected, actual)
+
+  def testOmitParentWithBase(self):
+    # Our input data should be empty when parent is omitted
+    in_data = {
+        'id': 'you',
+    }
+    with self.assertRaises(Exception):
+      RewriteMetadata(in_data, MetadataOptions(name='me'))
+
+  def testNewPort(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    port = '80'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                port + '/tcp': {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, ports=[port]))
+    self.assertEquals(expected, actual)
+
+  def testAugmentPort(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                '443/tcp': {}
+            }
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    port = '80'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                '443/tcp': {},
+                port + '/tcp': {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, ports=[port]))
+    self.assertEquals(expected, actual)
+
+  def testMultiplePorts(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    port1 = '80'
+    port2 = '8080'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                port1 + '/tcp': {},
+                port2 + '/tcp': {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, ports=[port1, port2]))
+    self.assertEquals(expected, actual)
+
+  def testPortCollision(self):
+    port = '80'
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                port + '/tcp': {}
+            }
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                port + '/tcp': {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, ports=[port]))
+    self.assertEquals(expected, actual)
+
+  def testPortWithProtocol(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    port = '80/tcp'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'ExposedPorts': {
+                port: {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, ports=[port]))
+    self.assertEquals(expected, actual)
+
+  def testNewVolume(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    volume = '/logs'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Volumes': {
+                volume: {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, volumes=[volume]))
+    self.assertEquals(expected, actual)
+
+  def testAugmentVolume(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Volumes': {
+                '/original': {}
+            }
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    volume = '/data'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Volumes': {
+                '/original': {},
+                volume: {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, volumes=[volume]))
+    self.assertEquals(expected, actual)
+
+  def testMultipleVolumes(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    volume1 = '/input'
+    volume2 = '/output'
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Volumes': {
+                volume1: {},
+                volume2: {}
+            }
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, parent=parent, volumes=[volume1, volume2]))
+    self.assertEquals(expected, actual)
+
+  def testEnv(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor'
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    env = [
+        'baz=blah',
+        'foo=bar',
+    ]
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Env': env,
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, env=env, parent=parent))
+    self.assertEquals(expected, actual)
+
+  def testEnvResolveReplace(self):
+    in_data = {
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Env': [
+                'foo=bar',
+                'baz=blah',
+                'blah=still around',
+            ],
+        }
+    }
+    name = 'deadbeef'
+    parent = 'blah'
+    env = [
+        'baz=replacement',
+        'foo=$foo:asdf',
+    ]
+    expected = {
+        'id': name,
+        'parent': parent,
+        'config': {
+            'User': 'mattmoor',
+            'WorkingDir': '/usr/home/mattmoor',
+            'Env': [
+                'baz=replacement',
+                'blah=still around',
+                'foo=bar:asdf',
+            ],
+        },
+        'docker_version': _DOCKER_VERSION,
+        'architecture': _PROCESSOR_ARCHITECTURE,
+        'os': _OPERATING_SYSTEM,
+    }
+
+    actual = RewriteMetadata(in_data, MetadataOptions(
+        name=name, env=env, parent=parent))
+    self.assertEquals(expected, actual)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/build_defs/docker/sha256.py b/tools/build_defs/docker/sha256.py
new file mode 100644
index 0000000..992062f
--- /dev/null
+++ b/tools/build_defs/docker/sha256.py
@@ -0,0 +1,27 @@
+# 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.
+"""A wrapper to have a portable SHA-256 tool."""
+
+# TODO(dmarting): instead of this tool we should make SHA-256 of artifacts
+# available in Skylark.
+import hashlib
+import sys
+
+if __name__ == "__main__":
+  if len(sys.argv) != 3:
+    print "Usage: %s input output" % sys.argv[0]
+    sys.exit(-1)
+  with open(sys.argv[2], "w") as outputfile:
+    with open(sys.argv[1], "rb") as inputfile:
+      outputfile.write(hashlib.sha256(inputfile.read()).hexdigest())
diff --git a/tools/build_defs/docker/testdata/BUILD b/tools/build_defs/docker/testdata/BUILD
new file mode 100644
index 0000000..4957110
--- /dev/null
+++ b/tools/build_defs/docker/testdata/BUILD
@@ -0,0 +1,132 @@
+package(
+    default_visibility = [
+        "//tools/build_rules/docker:__subpackages__",
+    ],
+)
+
+load("/tools/build_rules/docker/docker", "docker_build")
+
+docker_build(
+    name = "files_base",
+    files = ["foo"],
+)
+
+docker_build(
+    name = "files_with_files_base",
+    base = ":files_base",
+    files = ["bar"],
+)
+
+docker_build(
+    name = "tar_base",
+    tars = ["one.tar"],
+)
+
+docker_build(
+    name = "tar_with_tar_base",
+    base = ":tar_base",
+    tars = ["two.tar"],
+)
+
+docker_build(
+    name = "files_with_tar_base",
+    base = ":tar_base",
+    files = ["bar"],
+)
+
+docker_build(
+    name = "tar_with_files_base",
+    base = ":files_base",
+    tars = ["two.tar"],
+)
+
+# TODO(mattmoor): Test scalar entrypoint
+docker_build(
+    name = "base_with_entrypoint",
+    entrypoint = ["/bar"],
+    files = ["bar"],
+    ports = ["8080"],
+    tars = ["two.tar"],
+)
+
+# TODO(mattmoor): Test scalar cmd
+docker_build(
+    name = "derivative_with_shadowed_cmd",
+    base = ":base_with_entrypoint",
+    cmd = ["shadowed-arg"],
+    files = ["foo"],
+)
+
+docker_build(
+    name = "derivative_with_cmd",
+    base = ":derivative_with_shadowed_cmd",
+    cmd = [
+        "arg1",
+        "arg2",
+    ],
+    ports = ["80/tcp"],
+    tars = ["one.tar"],
+)
+
+docker_build(
+    name = "base_with_volume",
+    files = [
+        "bar",
+        "foo",
+    ],
+    volumes = ["/logs"],
+)
+
+docker_build(
+    name = "derivative_with_volume",
+    base = ":base_with_volume",
+    volumes = [
+        "/asdf",
+        "/blah",
+    ],
+)
+
+py_binary(
+    name = "extras_gen",
+    srcs = ["extras_gen.py"],
+    deps = ["//tools/build_rules/docker:archive"],
+)
+
+genrule(
+    name = "extras",
+    outs = ["extras.tar"],
+    cmd = "$(location :extras_gen) $@",
+    tools = [":extras_gen"],
+)
+
+docker_build(
+    name = "generated_tarball",
+    tars = [
+        ":extras",
+    ],
+)
+
+docker_build(
+    name = "with_env",
+    base = ":base_with_volume",
+    env = {
+        "foo": "/asdf",
+        "bar": "blah blah blah",
+    },
+)
+
+docker_build(
+    name = "with_double_env",
+    base = ":with_env",
+    env = {
+        "baz": "${foo} $bar",
+    },
+)
+
+docker_build(
+    name = "link_with_files_base",
+    base = ":files_base",
+    symlinks = {
+        "/usr/bin/java": "/bar",
+    },
+)
diff --git a/tools/build_defs/docker/testdata/bar b/tools/build_defs/docker/testdata/bar
new file mode 100644
index 0000000..d6d9d34
--- /dev/null
+++ b/tools/build_defs/docker/testdata/bar
@@ -0,0 +1 @@
+blah
\ No newline at end of file
diff --git a/tools/build_defs/docker/testdata/extras_gen.py b/tools/build_defs/docker/testdata/extras_gen.py
new file mode 100644
index 0000000..fc9719f
--- /dev/null
+++ b/tools/build_defs/docker/testdata/extras_gen.py
@@ -0,0 +1,39 @@
+# 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.
+"""A simple cross-platform helper to create a timestamped tar file."""
+import datetime
+import sys
+import tarfile
+
+from tools.build_defs.docker import archive
+
+if __name__ == '__main__':
+  mtime = int(datetime.datetime.now().strftime('%s'))
+  with archive.TarFileWriter(sys.argv[1]) as f:
+    f.add_file('./', tarfile.DIRTYPE,
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./usr/', tarfile.DIRTYPE,
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./usr/bin/', tarfile.DIRTYPE,
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./usr/bin/java', tarfile.SYMTYPE,
+               link='/path/to/bin/java',
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./etc/', tarfile.DIRTYPE,
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./etc/nsswitch.conf',
+               content='hosts:          files dns\n',
+               uname='root', gname='root', mtime=mtime)
+    f.add_file('./tmp/', tarfile.DIRTYPE,
+               uname='root', gname='root', mtime=mtime)
diff --git a/tools/build_defs/docker/testdata/foo b/tools/build_defs/docker/testdata/foo
new file mode 100644
index 0000000..5e40c08
--- /dev/null
+++ b/tools/build_defs/docker/testdata/foo
@@ -0,0 +1 @@
+asdf
\ No newline at end of file
diff --git a/tools/build_defs/docker/testdata/one.tar b/tools/build_defs/docker/testdata/one.tar
new file mode 100644
index 0000000..ba13e09
--- /dev/null
+++ b/tools/build_defs/docker/testdata/one.tar
Binary files differ
diff --git a/tools/build_defs/docker/testdata/two.tar b/tools/build_defs/docker/testdata/two.tar
new file mode 100644
index 0000000..1842824
--- /dev/null
+++ b/tools/build_defs/docker/testdata/two.tar
Binary files differ
diff --git a/tools/build_defs/docker/testenv.sh b/tools/build_defs/docker/testenv.sh
new file mode 100755
index 0000000..6274d4d
--- /dev/null
+++ b/tools/build_defs/docker/testenv.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# 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.
+
+# Integration test for docker, test environment.
+
+[ -z "$TEST_SRCDIR" ] && { echo "TEST_SRCDIR not set!" >&2; exit 1; }
+
+# Load the unit-testing framework
+source "${TEST_SRCDIR}/src/test/shell/unittest.bash" || \
+  { echo "Failed to source unittest.bash" >&2; exit 1; }
+
+readonly TEST_DATA_DIR="${TEST_SRCDIR}/tools/build_defs/docker/testdata"