"""Defines a rule for wrapping container pushing rules from upstream providers (e.g., rules_oci). The wrapping rule allows consistent configuration of destination repositories and makes information about the destination repository available to downstream rules via the OCIPushInfo provider. """ load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes") load("@rules_oci//oci:defs.bzl", _rules_oci_push = "oci_push") load("@io_bazel_rules_docker//container:container.bzl", _rules_docker_push = "container_push") load(":constants.bzl", "COMMON_TAGS") load("//hack/build/rules/container/sign:container_sign.bzl", "container_sign") load(":push_info.bzl", "OCIPushInfo") def _impl(ctx): if ctx.attr.rules_docker_push and ctx.attr.rules_oci_push: fail("only one of 'rules_docker_push' and 'rules_oci_push' can be specified") digest = ctx.file.digest # Create ref by combining the repository and digest ref = ctx.actions.declare_file("{0}.ref".format(ctx.label.name)) ctx.actions.run_shell( outputs = [ref], inputs = [ctx.file.repository_file, digest], command = "echo \"$(cat {repo})@$(cat {digest})\" > {ref}".format( repo = ctx.file.repository_file.path, digest = digest.path, ref = ref.path, ), ) # Resolve the pusher binary we will be wrapping and the required runfiles # that will be forwarded by this rule. wrapped_pusher = None runfiles = None # Create pusher binary which simply invokes the wrapped pusher. This is # needed because Bazel won't allow us to provide another rule's executable # as our own. pusher = ctx.actions.declare_file(ctx.label.name) if ctx.attr.rules_docker_push != None: wrapped_pusher = ctx.executable.rules_docker_push runfiles = ctx.runfiles(files = [ctx.file.repository_file]) runfiles = runfiles.merge(ctx.attr.rules_docker_push[DefaultInfo].default_runfiles) # rules_docker needs a special wrapper to ensure that we can update # the hardcoded "registry" field. # Use a hardcoded tag because I'm tired of rules_docker and pusha/leaf # will handle tagging post-push for us. ctx.actions.write( output = pusher, content = """#!/usr/bin/env bash {pusher} --dst=$(cat {repo}):dev "$@" """.format( pusher = wrapped_pusher.short_path, repo = ctx.file.repository_file.short_path, ), is_executable = True, ) else: wrapped_pusher = ctx.executable.rules_oci_push runfiles = ctx.attr.rules_oci_push[DefaultInfo].default_runfiles ctx.actions.write( output = pusher, content = """#!/usr/bin/env bash {pusher} "$@" """.format( pusher = wrapped_pusher.short_path, ), is_executable = True, ) return [ DefaultInfo( files = depset([ctx.file.repository_file, ref, digest]), executable = pusher, runfiles = runfiles, ), OCIPushInfo( repo = ctx.file.repository_file, digest = digest, ref = ref, # Have to provide our pusher if rules_docker, due to registry override pusher = wrapped_pusher if ctx.attr.rules_docker_push == None else pusher, runfiles = runfiles, ), ] # container_push rule wrapper definition. Consumers interact with the rule via # the public macro _container_push2 = rule( doc = """container_push wraps upstream container pushing rules to provide a consistent surface area for controlling the destination repository and accessing that information from rules that depend on it. """, implementation = _impl, toolchains = [ "@rules_oci//cosign:toolchain_type", ], attrs = { "from_third_party": attr.bool( doc = "Flag to signify if container push is pushing a third party image", default = False, mandatory = False, ), "repository_file": attr.label( doc = "File containing full OCI repository to push container to", allow_single_file = True, mandatory = True, ), "digest": attr.label( doc = "File containing digest for the container to push.", allow_single_file = True, mandatory = False, ), # TODO: once rules_docker is dropped entirely, these attrs can be condensed # into a single "wrapped_push" attr, or we can use Bazel 7 rule extension # APIs. "rules_docker_push": attr.label( doc = "rules_docker container_push target. Mutually exclusive with rules_oci_push.", executable = True, cfg = "exec", ), "rules_oci_push": attr.label( doc = "rules_oci container_push target. Mutually exclusive with rules_docker_push.", executable = True, cfg = "target", ), }, executable = True, ) def container_push2( image, image_name, repository_file, digest = None, name = "container_push", rules_docker = False, tags = [], tag = None, from_third_party = False, **kwargs): """Creates standardized container_push rule for consistent registry config. It instantiates a pushing rule that is wrapped by our own implementation, so that we can forward the pushing binary and all of the information required to push it or replace container references in rules that depend on this. Args: name: name of the returned container_push target image: image target to push i.e. @ubuntu image_name: image name i.e. library/ubuntu repository_file: label to a file containing an OCI repository, which is combined with the 'image_name' to create the full OCI repository string digest: digest of the image if set explicitly tags: list of tags to add to Bazel target tag: image tag, defaults to --define value image-tag from_third_party: a boolean flag used to signify if this container push is generated by a third party pull image. Used to distinguish which container pushes should be run when publishing all third party sourced container images. rules_docker: whether or not this container_push target will be using rules_docker or not **kwargs: Additional key/value pairs to pass to produced targets """ forwarded_args = propagate_common_rule_attributes(kwargs) # Generate full repository string from build config setting and image name. full_repository_file = "{0}.repo".format(name) native.genrule( name = "{0}_repo".format(name), srcs = [ repository_file, ], outs = [full_repository_file], cmd = "echo \"$$(cat $(SRCS))/{image}\" > $@".format(image = image_name), **forwarded_args ) if rules_docker: # Massage repo file into what rules_docker expects: everything after # the registry URL. rules_docker_repo_file = "_{0}_rules_docker.repo".format(name) native.genrule( name = "_{0}_rules_docker_repo".format(name), srcs = [ repository_file, ], outs = [rules_docker_repo_file], cmd = "echo \"$$(cat $(SRCS) | cut -d'/' -f 2-)/{image}\" > $@".format(image = image_name), ) rules_docker_push_name = "_rules_docker_{0}".format(name) _rules_docker_push( name = rules_docker_push_name, format = "Docker", image = image, tags = COMMON_TAGS + tags, # The registry value will be overriden from the repositories file # in our container_push rule registry = "us-east1-docker.pkg.dev", # Will be replaced by repository_file but still a mandatory attr :facepalm: repository = "placeholder", repository_file = rules_docker_repo_file, tag = tag if tag != None else "dev", stamp = "@io_bazel_rules_docker//stamp:never", **forwarded_args ) _container_push2( name = name, rules_docker_push = ":_rules_docker_" + name, digest = digest or rules_docker_push_name + ".digest", repository_file = full_repository_file, tags = tags, **forwarded_args ) return rules_oci_push_name = "_rules_oci_{0}".format(name) _rules_oci_push( remote_tags = [tag] if tag != None else ["dev"], name = rules_oci_push_name, image = image, repository_file = full_repository_file, **forwarded_args ) _container_push2( name = name, rules_oci_push = ":" + rules_oci_push_name, digest = digest or image + ".digest", repository_file = full_repository_file, tags = tags, from_third_party = from_third_party if from_third_party else None, **forwarded_args ) container_sign( name = name.replace("push", "sign"), container_push = ":{0}".format(name), **forwarded_args )