Skip to content

Commit

Permalink
Implement new container_push (#915)
Browse files Browse the repository at this point in the history
* Implement new pusher

* Fix type error

* Fix styling to resolve comments

* Enhanced error messages
  • Loading branch information
xiaohegong authored and k8s-ci-robot committed Jun 24, 2019
1 parent 574278c commit 5647f4f
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 0 deletions.
42 changes: 42 additions & 0 deletions container/go/cmd/pusher/BUILD
Original file line number Diff line number Diff line change
@@ -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"],
)
98 changes: 98 additions & 0 deletions container/go/cmd/pusher/pusher.go
Original file line number Diff line number Diff line change
@@ -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 <dst>.
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)
}
168 changes: 168 additions & 0 deletions container/new_push.bzl
Original file line number Diff line number Diff line change
@@ -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 <tag> 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,
)

0 comments on commit 5647f4f

Please sign in to comment.