Skip to content
This repository has been archived by the owner on Feb 6, 2024. It is now read-only.

Add support for the images={} attribute. #8

Merged
merged 2 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ k8s_object(

# A template of a Kubernetes Deployment object yaml.
template = ":deployment.yaml",

# An optional collection of docker_build images to publish
# when this target is bazel run. The digest of the published
# image is substituted as a part of the resolution process.
images = {
"gcr.io/rules_k8s/server:dev": "//server:image"
},
)
```

Expand All @@ -102,9 +109,53 @@ load("@k8s_deploy//:defaults.bzl", "k8s_deploy")
k8s_deploy(
name = "dev",
template = ":deployment.yaml",
images = {
"gcr.io/rules_k8s/server:dev": "//server:image"
},
)
```

## Usage

This single target exposes a collection of actions. We will follow the `:dev`
target from the example above.

### Build

Build builds all of the constituent elements, and makes the template available
as `{name}.yaml`. If `template` is a generated input, it will be built.
Likewise, any `docker_build` images referenced from the `images={}` attribute
will be built.

```shell
bazel build :dev
```

### Resolve

Deploying with tags, especially in production, is a bad practice because they
are mutable. If a tag changes, it can lead to inconsistent versions of your app
running after auto-scaling or auto-healing events. Thankfully in v2 of the
Docker Registry, digests were introduced. Deploying by digest provides
cryptographic guarantees of consistency across the replicas of a deployment.

You can "resolve" your resource `template` by running:

```shell
bazel run :dev
```

The resolved `template` will be printed to `STDOUT`.

This command will publish any `images = {}` present in your rule, substituting
those exact digests into the yaml template, and for other images resolving the
tags to digests by reaching out to the appropriate registry. Any images that
cannot be found or accessed are left unresolved.

**This process only supports fully-qualified tag names.** This means you must
always specify tag and registry domain names (no implicit `:latest`).


<a name="k8s_object"></a>
## k8s_object

Expand Down Expand Up @@ -146,6 +197,16 @@ A rule for interacting with Kubernetes objects.
<p>The yaml or json for a Kubernetes object.</p>
</td>
</tr>
<tr>
<td><code>images</code></td>
<td>
<p><code>string to label dictionary; required</code></p>
<p>When this target is <code>bazel run</code> the images
referenced by label will be published to the tag key.</p>
<p>The published digests of these images will be substituted
directly, so as to avoid a race in the resolution process</p>
</td>
</tr>
</tbody>
</table>

Expand Down
14 changes: 7 additions & 7 deletions examples/hello-grpc/cc/server/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ cc_image(
deps = ["//examples/hello-grpc/proto"],
)

load("@io_bazel_rules_docker//docker:docker.bzl", "docker_push")
load("@k8s_deploy//:defaults.bzl", "k8s_deploy")

docker_push(
name = "push",
image = ":server",
registry = "us.gcr.io",
repository = "rules_k8s/examples/hello-grpc",
tag = "cc",
k8s_deploy(
name = "staging",
images = {
"us.gcr.io/rules_k8s/hello-grpc:staging": ":server",
},
template = "//examples/hello-grpc:deployment.yaml",
)
71 changes: 67 additions & 4 deletions k8s/object.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
# limitations under the License.
"""An implementation of k8s_object for interacting with an object of kind."""

load(
"@io_bazel_rules_docker//docker:layers.bzl",
_get_layers = "get_from_target",
_layer_tools = "tools",
)
load(
"@io_bazel_rules_docker//docker:label.bzl",
_string_to_label = "string_to_label",
)

def _impl(ctx):
"""Core implementation of k8s_object."""

Expand All @@ -23,25 +33,68 @@ def _impl(ctx):
substitutions = {},
)

all_inputs = []
image_specs = []
if ctx.attr.images:
# Compute the set of layers from the image_targets.
image_target_dict = _string_to_label(
ctx.attr.image_targets, ctx.attr.image_target_strings)
image_files_dict = _string_to_label(
ctx.files.image_targets, ctx.attr.image_target_strings)

# Walk the collection of images passed and for each key/value pair
# collect the parts to pass to the resolver as --image_spec arguments.
# Each images entry results in a single --image_spec argument.
# As part of this walk, we also collect all of the image's input files
# to include as runfiles, so they are accessible to be pushed.
for tag in ctx.attr.images:
target = ctx.attr.images[tag]
image = _get_layers(ctx, image_target_dict[target], image_files_dict[target])

image_spec = {"name": tag}
if image.get("legacy"):
image_spec["tarball"] = image["legacy"]
all_inputs += [image["legacy"]]

blobsums = image.get("blobsum", [])
image_spec["digest"] = ",".join([f.short_path for f in blobsums])
all_inputs += blobsums

blobs = image.get("zipped_layer", [])
image_spec["layer"] = ",".join([f.short_path for f in blobs])
all_inputs += blobs

image_spec["config"] = image["config"].short_path
all_inputs += [image["config"]]

image_specs += [";".join([
"%s=%s" % (k, v)
for (k, v) in image_spec.items()
])]

ctx.action(
command = """cat > {resolve_script} <<"EOF"
#!/bin/bash -e
{resolver} --template {yaml}
{resolver} --template {yaml} {images}
EOF""".format(
resolver = ctx.executable._resolver.short_path,
yaml = ctx.outputs.yaml.short_path,
images = " ".join([
# Quote the parameter otherwise semi-colons complete the command.
"--image_spec='%s'" % spec
for spec in image_specs
]),
resolve_script = ctx.outputs.executable.path,
),
inputs = [],
outputs = [ctx.outputs.executable],
mnemonic = "ResolveScript"
)


return struct(runfiles = ctx.runfiles(files = [
ctx.executable._resolver,
ctx.outputs.yaml,
]))
] + all_inputs))

_common_attrs = {
# TODO(mattmoor): Add cluster / namespace once we have executable friends.
Expand All @@ -64,7 +117,10 @@ _k8s_object = rule(
single_file = True,
mandatory = True,
),
# TODO(mattmoor): images
"images": attr.string_dict(),
# Implicit dependencies.
"image_targets": attr.label_list(allow_files = True),
"image_target_strings": attr.string_list(),
} + _common_attrs,
executable = True,
outputs = {
Expand All @@ -81,6 +137,13 @@ def k8s_object(name, **kwargs):
namespace: the namespace within the cluster.
kind: the object kind.
template: the yaml template to instantiate.
images: a dictionary from fully-qualified tag to label.
"""
for reserved in ["image_targets", "image_target_strings", "resolved"]:
if reserved in kwargs:
fail("reserved for internal use by docker_bundle macro", attr=reserved)

kwargs["image_targets"] = list(set(kwargs.get("images", {}).values()))
kwargs["image_target_strings"] = list(set(kwargs.get("images", {}).values()))

_k8s_object(name=name, **kwargs)
65 changes: 59 additions & 6 deletions k8s/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from containerregistry.client import docker_creds
from containerregistry.client import docker_name
from containerregistry.client.v2_2 import docker_image as v2_2_image
from containerregistry.client.v2_2 import docker_session as v2_2_session
from containerregistry.tools import patched
from containerregistry.transport import transport_pool

Expand All @@ -34,6 +35,13 @@
'--template', action='store',
help='The template file to resolve.')

parser.add_argument(
'--image_spec', action='append',
help='Associative lists of the constitutent elements of a FromDisk image.')

_THREADS = 32
_DOCUMENT_DELIMITER = '---\n'


def Resolve(input, tag_to_digest):
"""Translate tag references within the input yaml into digests."""
Expand Down Expand Up @@ -72,8 +80,8 @@ def TagToDigest(tag, overrides, transport):
return str(overrides[tag])

def fully_qualify_digest(digest):
return str(docker_name.Digest('{registry}/{repo}@{digest}'.format(
registry=tag.registry, repo=tag.repository, digest=digest)))
return docker_name.Digest('{registry}/{repo}@{digest}'.format(
registry=tag.registry, repo=tag.repository, digest=digest))

# Resolve the tag to digest using the standard
# Docker keychain logic.
Expand All @@ -91,8 +99,48 @@ def fully_qualify_digest(digest):
return digest


_THREADS = 32
_DOCUMENT_DELIMITER = '---\n'
def Publish(transport,
name=None, tarball=None, config=None, digest=None, layer=None):
if not name:
raise Exception('Expected "name" kwarg')

if not config and (layer or digest):
raise Exception(
name + ': Using "layer" or "digest" requires "config" to be specified.')

if config:
with open(config, 'r') as reader:
config = reader.read()
elif tarball:
with v2_2_image.FromTarball(tarball) as base:
config = base.config_file()
else:
raise Exception(name + ': Either "config" or "tarball" must be specified.')

if digest or layer:
digest = digest.split(',')
layer = layer.split(',')
if len(digest) != len(layer):
raise Exception(
name + ': "digest" and "layer" must have matching lengths.')
else:
digest = []
layer = []

name = docker_name.Tag(name)

# Resolve the appropriate credential to use based on the standard Docker
# client logic.
creds = docker_creds.DefaultKeychain.Resolve(name)

with v2_2_session.Push(name, creds, transport, threads=_THREADS) as session:
with v2_2_image.FromDisk(config, zip(digest or [], layer or []),
legacy_base=tarball) as v2_2_img:
session.upload(v2_2_img)

return (name, docker_name.Digest('{repository}@{digest}'.format(
repository=name.as_repository(),
digest=v2_2_img.digest())))


def main():
Expand All @@ -101,8 +149,13 @@ def main():
transport = transport_pool.Http(httplib2.Http, size=_THREADS)

overrides = {}
# TODO(mattmoor): Support publishing images and using the
# published digest to override resolution (to avoid races).
# TODO(mattmoor): Execute these in a threadpool and
# aggregate the results as they complete.
for spec in args.image_spec:
parts = spec.split(';')
kwargs = dict([x.split('=', 2) for x in parts])
(tag, digest) = Publish(transport, **kwargs)
overrides[tag] = digest

with open(args.template, 'r') as f:
inputs = f.read()
Expand Down
Loading