diff --git a/container/go/cmd/pusher/BUILD b/container/go/cmd/pusher/BUILD new file mode 100644 index 000000000..853659013 --- /dev/null +++ b/container/go/cmd/pusher/BUILD @@ -0,0 +1,42 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ### +# Build file for new puller binary based on go-containerregistry backend. +load("@bazel_gazelle//:def.bzl", "gazelle") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +# gazelle:prefix github.com/bazelbuild/rules_docker +gazelle(name = "gazelle") + +go_library( + name = "go_pusher", + srcs = ["pusher.go"], + importpath = "github.com/bazelbuild/rules_docker", + visibility = ["//visibility:private"], + deps = [ + "//container/go/pkg/oci:go_default_library", + "@com_github_google_go_containerregistry//pkg/authn:go_default_library", + "@com_github_google_go_containerregistry//pkg/name:go_default_library", + "@com_github_google_go_containerregistry//pkg/v1:go_default_library", + "@com_github_google_go_containerregistry//pkg/v1/remote:go_default_library", + "@com_github_google_go_containerregistry//pkg/v1/tarball:go_default_library", + "@com_github_pkg_errors//:go_default_library", + ], +) + +go_binary( + name = "pusher", + embed = [":go_pusher"], + visibility = ["//visibility:public"], +) diff --git a/container/go/cmd/pusher/pusher.go b/container/go/cmd/pusher/pusher.go new file mode 100644 index 000000000..81570b4af --- /dev/null +++ b/container/go/cmd/pusher/pusher.go @@ -0,0 +1,98 @@ +// Copyright 2015 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +////////////////////////////////////////////////////////////////////// +// This binary pushes an image to a Docker Registry. +package main + +import ( + "flag" + "log" + "os" + + "github.com/bazelbuild/rules_docker/container/go/pkg/oci" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/pkg/errors" +) + +var ( + dst = flag.String("dst", "", "The destination location including repo and digest/tag of the docker image to push. Supports fully-qualified tag or digest references.") + src = flag.String("src", "", "Path to the directory which has the image index that will be pushed.") + format = flag.String("format", "", "The format of the source image, oci|docker. The docker format should be a tarball of the image as generated by docker save.") + clientConfigDir = flag.String("client-config-dir", "", "The path to the directory where the client configuration files are located. Overiddes the value from DOCKER_CONFIG.") +) + +func main() { + flag.Parse() + log.Println("Running the Image Pusher to push images to a Docker Registry...") + + if *dst == "" { + log.Fatalln("Required option -dst was not specified.") + } + if *src == "" { + log.Fatalln("Required option -src was not specified.") + } + if *format == "" { + log.Fatalln("Required option -format was not specified.") + } + + // If the user provided a client config directory, instruct the keychain resolver + // to use it to look for the docker client config. + if *clientConfigDir != "" { + os.Setenv("DOCKER_CONFIG", *clientConfigDir) + } + + img, err := readImage(*src, *format) + if err != nil { + log.Fatalf("Error reading from %s: %v", *src, err) + } + + if err := push(*dst, img); err != nil { + log.Fatalf("Error pushing image to %s: %v", *dst, err) + } + + log.Println("Successfully pushed %s image from %s to %s", *format, *src, *dst) +} + +// push pushes the given image to the given destination. +// NOTE: This function is adapted from https://github.com/google/go-containerregistry/blob/master/pkg/crane/push.go +// with modification for option to push OCI layout or Docker tarball format . +// Push the given image to destination . +func push(dst string, img v1.Image) error { + // Push the image to dst. + ref, err := name.ParseReference(dst) + if err != nil { + return errors.Wrapf(err, "error parsing %q as an image reference", dst) + } + + if err := remote.Write(ref, img, remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil { + return errors.Wrapf(err, "unable to push image to %s", dst) + } + + return nil +} + +// readImage returns a v1.Image after reading an OCI index or a Docker tarball from src. +func readImage(src, format string) (v1.Image, error) { + if format == "oci" { + return oci.Read(src) + } + if format == "docker" { + return tarball.ImageFromPath(src, nil) + } + return nil, errors.Errorf("unknown image format %q", format) +} diff --git a/container/new_push.bzl b/container/new_push.bzl new file mode 100644 index 000000000..a68a3f257 --- /dev/null +++ b/container/new_push.bzl @@ -0,0 +1,168 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""An new implementation of container_push based on google/containerregistry using google/go-containerregistry. + +This wraps the rules_docker.container.go.cmd.pusher.pusher executable in a +Bazel rule for publishing images. +""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@io_bazel_rules_docker//container:providers.bzl", "PushInfo") +load( + "//container:layer_tools.bzl", + _layer_tools = "tools", +) +load( + "//skylib:path.bzl", + "runfile", +) + +def _get_runfile_path(ctx, f): + return "${RUNFILES}/%s" % runfile(ctx, f) + +def _impl(ctx): + """Core implementation of new_container_push.""" + + # TODO (xiaohegong): 1) Possible optimization for efficiently pushing intermediate + # representation, similar with the old python implementation, e.g., push-by-layer. + # Some of the digester arguments omitted from before: --tarball, --config, --manifest, --digest, --layer, --oci. + # 2) The old implementation outputs a {image_name}.digest for compatibility with container_digest, omitted for now. + # 3) Use and implementation of attr.stamp. + + pusher_args = [] + + # Parse and get destination registry to be pushed to + registry = ctx.expand_make_variables("registry", ctx.attr.registry, {}) + repository = ctx.expand_make_variables("repository", ctx.attr.repository, {}) + tag = ctx.expand_make_variables("tag", ctx.attr.tag, {}) + + # If a tag file is provided, override with tag value + runfiles_tag_file = [] + if ctx.file.tag_file: + tag = "$(cat {})".format(_get_runfile_path(ctx, ctx.file.tag_file)) + runfiles_tag_file = [ctx.file.tag_file] + + pusher_args += ["-dst", "{registry}/{repository}:{tag}".format( + registry = registry, + repository = repository, + tag = tag, + )] + + # Find and set src to correct paths depending the image format to be pushed + if ctx.attr.format == "oci": + found = False + for f in ctx.files.image: + if f.basename == "index.json": + pusher_args += ["-src", "{index_dir}".format( + index_dir = f.dirname, + )] + found = True + if not found: + fail("Did not find an index.json in the image attribute {} specified to {}".format(ctx.attr.image, ctx.label)) + if ctx.attr.format == "docker": + if len(ctx.files.image) == 0: + fail("Attribute image {} to {} did not contain an image tarball".format(ctx.attr.image, ctx.label)) + if len(ctx.files.image) > 1: + fail("Attribute image {} to {} had {} files. Expected exactly 1".format(ctx.attr.image, ctx.label, len(ctx.files.image))) + pusher_args += ["-src", str(ctx.files.image[0].path)] + + pusher_args += ["-format", str(ctx.attr.format)] + + # If the docker toolchain is configured to use a custom client config + # directory, use that instead + toolchain_info = ctx.toolchains["@io_bazel_rules_docker//toolchains/docker:toolchain_type"].info + if toolchain_info.client_config != "": + pusher_args += ["-client-config-dir", str(toolchain_info.client_config)] + + ctx.actions.expand_template( + template = ctx.file._tag_tpl, + substitutions = { + "%{args}": " ".join(pusher_args), + "%{container_pusher}": _get_runfile_path(ctx, ctx.executable._pusher), + }, + output = ctx.outputs.executable, + is_executable = True, + ) + + runfiles = ctx.runfiles(files = [ctx.executable._pusher] + runfiles_tag_file) + runfiles = runfiles.merge(ctx.attr._pusher[DefaultInfo].default_runfiles) + + return [ + DefaultInfo( + executable = ctx.outputs.executable, + runfiles = runfiles, + ), + PushInfo( + registry = registry, + repository = repository, + tag = tag, + ), + ] + +# Pushes a container image to a registry. +new_container_push = rule( + attrs = dicts.add({ + "format": attr.string( + default = "oci", + values = [ + "oci", + "docker", + ], + doc = "The form to push: docker or oci, default to 'oci'.", + ), + "image": attr.label( + allow_files = True, + mandatory = True, + doc = "The label of the image to push.", + ), + "registry": attr.string( + mandatory = True, + doc = "The registry to which we are pushing.", + ), + "repository": attr.string( + mandatory = True, + doc = "The name of the image.", + ), + "stamp": attr.bool( + default = False, + mandatory = False, + ), + "tag": attr.string( + default = "latest", + doc = "(optional) The tag of the image, default to 'latest'.", + ), + "tag_file": attr.label( + allow_single_file = True, + doc = "(optional) The label of the file with tag value. Overrides 'tag'.", + ), + "_digester": attr.label( + default = "@containerregistry//:digester", + cfg = "host", + executable = True, + ), + "_pusher": attr.label( + default = Label("@io_bazel_rules_docker//container/go/cmd/pusher:pusher"), + cfg = "host", + executable = True, + allow_files = True, + ), + "_tag_tpl": attr.label( + default = Label("//container:push-tag.sh.tpl"), + allow_single_file = True, + ), + }, _layer_tools), + executable = True, + toolchains = ["@io_bazel_rules_docker//toolchains/docker:toolchain_type"], + implementation = _impl, +)