1"""Defines a rule for wrapping container pushing rules from upstream providers (e.g., rules_oci).
2
3The wrapping rule allows consistent configuration of destination repositories
4and makes information about the destination repository available to downstream
5rules via the OCIPushInfo provider.
6"""
7
8load("@aspect_bazel_lib//lib:utils.bzl", "propagate_common_rule_attributes")
9load("@rules_oci//oci:defs.bzl", _rules_oci_push = "oci_push")
10load("@io_bazel_rules_docker//container:container.bzl", _rules_docker_push = "container_push")
11load(":constants.bzl", "COMMON_TAGS")
12load("//hack/build/rules/container/sign:container_sign.bzl", "container_sign")
13load(":push_info.bzl", "OCIPushInfo")
14
15def _impl(ctx):
16 if ctx.attr.rules_docker_push and ctx.attr.rules_oci_push:
17 fail("only one of 'rules_docker_push' and 'rules_oci_push' can be specified")
18
19 digest = ctx.file.digest
20
21 # Create ref by combining the repository and digest
22 ref = ctx.actions.declare_file("{0}.ref".format(ctx.label.name))
23 ctx.actions.run_shell(
24 outputs = [ref],
25 inputs = [ctx.file.repository_file, digest],
26 command = "echo \"$(cat {repo})@$(cat {digest})\" > {ref}".format(
27 repo = ctx.file.repository_file.path,
28 digest = digest.path,
29 ref = ref.path,
30 ),
31 )
32
33 # Resolve the pusher binary we will be wrapping and the required runfiles
34 # that will be forwarded by this rule.
35 wrapped_pusher = None
36 runfiles = None
37
38 # Create pusher binary which simply invokes the wrapped pusher. This is
39 # needed because Bazel won't allow us to provide another rule's executable
40 # as our own.
41 pusher = ctx.actions.declare_file(ctx.label.name)
42
43 if ctx.attr.rules_docker_push != None:
44 wrapped_pusher = ctx.executable.rules_docker_push
45 runfiles = ctx.runfiles(files = [ctx.file.repository_file])
46 runfiles = runfiles.merge(ctx.attr.rules_docker_push[DefaultInfo].default_runfiles)
47
48 # rules_docker needs a special wrapper to ensure that we can update
49 # the hardcoded "registry" field.
50 # Use a hardcoded tag because I'm tired of rules_docker and pusha/leaf
51 # will handle tagging post-push for us.
52 ctx.actions.write(
53 output = pusher,
54 content = """#!/usr/bin/env bash
55{pusher} --dst=$(cat {repo}):dev "$@"
56""".format(
57 pusher = wrapped_pusher.short_path,
58 repo = ctx.file.repository_file.short_path,
59 ),
60 is_executable = True,
61 )
62 else:
63 wrapped_pusher = ctx.executable.rules_oci_push
64 runfiles = ctx.attr.rules_oci_push[DefaultInfo].default_runfiles
65 ctx.actions.write(
66 output = pusher,
67 content = """#!/usr/bin/env bash
68{pusher} "$@"
69""".format(
70 pusher = wrapped_pusher.short_path,
71 ),
72 is_executable = True,
73 )
74
75 return [
76 DefaultInfo(
77 files = depset([ctx.file.repository_file, ref, digest]),
78 executable = pusher,
79 runfiles = runfiles,
80 ),
81 OCIPushInfo(
82 repo = ctx.file.repository_file,
83 digest = digest,
84 ref = ref,
85 # Have to provide our pusher if rules_docker, due to registry override
86 pusher = wrapped_pusher if ctx.attr.rules_docker_push == None else pusher,
87 runfiles = runfiles,
88 ),
89 ]
90
91# container_push rule wrapper definition. Consumers interact with the rule via
92# the public macro
93_container_push2 = rule(
94 doc = """container_push wraps upstream container pushing rules to provide a
95consistent surface area for controlling the destination repository and accessing
96that information from rules that depend on it.
97""",
98 implementation = _impl,
99 toolchains = [
100 "@rules_oci//cosign:toolchain_type",
101 ],
102 attrs = {
103 "from_third_party": attr.bool(
104 doc = "Flag to signify if container push is pushing a third party image",
105 default = False,
106 mandatory = False,
107 ),
108 "repository_file": attr.label(
109 doc = "File containing full OCI repository to push container to",
110 allow_single_file = True,
111 mandatory = True,
112 ),
113 "digest": attr.label(
114 doc = "File containing digest for the container to push.",
115 allow_single_file = True,
116 mandatory = False,
117 ),
118 # TODO: once rules_docker is dropped entirely, these attrs can be condensed
119 # into a single "wrapped_push" attr, or we can use Bazel 7 rule extension
120 # APIs.
121 "rules_docker_push": attr.label(
122 doc = "rules_docker container_push target. Mutually exclusive with rules_oci_push.",
123 executable = True,
124 cfg = "exec",
125 ),
126 "rules_oci_push": attr.label(
127 doc = "rules_oci container_push target. Mutually exclusive with rules_docker_push.",
128 executable = True,
129 cfg = "target",
130 ),
131 },
132 executable = True,
133)
134
135def container_push2(
136 image,
137 image_name,
138 repository_file,
139 digest = None,
140 name = "container_push",
141 rules_docker = False,
142 tags = [],
143 tag = None,
144 from_third_party = False,
145 **kwargs):
146 """Creates standardized container_push rule for consistent registry config.
147
148 It instantiates a pushing rule that is wrapped by our own implementation,
149 so that we can forward the pushing binary and all of the information
150 required to push it or replace container references in rules that depend
151 on this.
152
153 Args:
154 name: name of the returned container_push target
155 image: image target to push i.e. @ubuntu
156 image_name: image name i.e. library/ubuntu
157 repository_file: label to a file containing an OCI repository, which is
158 combined with the 'image_name' to create the full OCI
159 repository string
160 digest: digest of the image if set explicitly
161 tags: list of tags to add to Bazel target
162 tag: image tag, defaults to --define value image-tag
163 from_third_party:
164 a boolean flag used to signify if this container push
165 is generated by a third party pull image. Used to distinguish
166 which container pushes should be run when publishing all
167 third party sourced container images.
168 rules_docker: whether or not this container_push target will be using
169 rules_docker or not
170 **kwargs: Additional key/value pairs to pass to produced targets
171 """
172
173 forwarded_args = propagate_common_rule_attributes(kwargs)
174
175 # Generate full repository string from build config setting and image name.
176 full_repository_file = "{0}.repo".format(name)
177 native.genrule(
178 name = "{0}_repo".format(name),
179 srcs = [
180 repository_file,
181 ],
182 outs = [full_repository_file],
183 cmd = "echo \"$$(cat $(SRCS))/{image}\" > $@".format(image = image_name),
184 **forwarded_args
185 )
186
187 if rules_docker:
188 # Massage repo file into what rules_docker expects: everything after
189 # the registry URL.
190 rules_docker_repo_file = "_{0}_rules_docker.repo".format(name)
191 native.genrule(
192 name = "_{0}_rules_docker_repo".format(name),
193 srcs = [
194 repository_file,
195 ],
196 outs = [rules_docker_repo_file],
197 cmd = "echo \"$$(cat $(SRCS) | cut -d'/' -f 2-)/{image}\" > $@".format(image = image_name),
198 )
199
200 rules_docker_push_name = "_rules_docker_{0}".format(name)
201 _rules_docker_push(
202 name = rules_docker_push_name,
203 format = "Docker",
204 image = image,
205 tags = COMMON_TAGS + tags,
206 # The registry value will be overriden from the repositories file
207 # in our container_push rule
208 registry = "us-east1-docker.pkg.dev",
209 # Will be replaced by repository_file but still a mandatory attr :facepalm:
210 repository = "placeholder",
211 repository_file = rules_docker_repo_file,
212 tag = tag if tag != None else "dev",
213 stamp = "@io_bazel_rules_docker//stamp:never",
214 **forwarded_args
215 )
216
217 _container_push2(
218 name = name,
219 rules_docker_push = ":_rules_docker_" + name,
220 digest = digest or rules_docker_push_name + ".digest",
221 repository_file = full_repository_file,
222 tags = tags,
223 **forwarded_args
224 )
225
226 return
227
228 rules_oci_push_name = "_rules_oci_{0}".format(name)
229 _rules_oci_push(
230 remote_tags = [tag] if tag != None else ["dev"],
231 name = rules_oci_push_name,
232 image = image,
233 repository_file = full_repository_file,
234 **forwarded_args
235 )
236
237 _container_push2(
238 name = name,
239 rules_oci_push = ":" + rules_oci_push_name,
240 digest = digest or image + ".digest",
241 repository_file = full_repository_file,
242 tags = tags,
243 from_third_party = from_third_party if from_third_party else None,
244 **forwarded_args
245 )
246
247 container_sign(
248 name = name.replace("push", "sign"),
249 container_push = ":{0}".format(name),
250 **forwarded_args
251 )
View as plain text