Skip to content

Commit 945a0c3

Browse files
authored
Merge pull request #1738 from GoogleContainerTools/push_dedup
refactor: deduplicate images before pushing and signing
2 parents e072745 + 2535a58 commit 945a0c3

File tree

7 files changed

+8085
-8039
lines changed

7 files changed

+8085
-8039
lines changed

.bazelversion

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.2.0
1+
7.4.0

MODULE.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ bazel_dep(name = "rules_oci", version = "1.7.5")
1414
bazel_dep(name = "rules_distroless", version = "0.3.8")
1515
bazel_dep(name = "rules_python", version = "0.35.0")
1616

17+
### OCI ###
18+
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
19+
oci.toolchains(crane_version = "v0.18.0")
20+
use_repo(oci, "oci_crane_toolchains")
21+
1722
### PYTHON ###
1823
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
1924
python.toolchain(

MODULE.bazel.lock

Lines changed: 7968 additions & 7968 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

private/oci/digest.bzl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"generate digest for oci_image and oci_image_index"
2+
3+
load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
4+
load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path")
5+
load("@aspect_bazel_lib//lib:jq.bzl", "jq")
6+
7+
# Normally we'd use the `.digest` target that rules_oci creates for every oci_image but
8+
# we also use oci_image_index which does not have a digest target. This was fixed in
9+
# https://github.com/bazel-contrib/rules_oci/pull/742 but it on the 2.x releases of rules_oci
10+
# TODO: Remove this once we upgrade to rules_oci 2.x
11+
def digest(name, image, **kwargs):
12+
# `oci_image_rule` and `oci_image_index_rule` produce a directory as default output.
13+
# Label for the [name]/index.json file
14+
directory_path(
15+
name = "_{}_index_json".format(name),
16+
directory = image,
17+
path = "index.json",
18+
**kwargs
19+
)
20+
21+
copy_file(
22+
name = "_{}_index_json_cp".format(name),
23+
src = "_{}_index_json".format(name),
24+
out = "_{}_index.json".format(name),
25+
**kwargs
26+
)
27+
28+
# Matches the [name].digest target produced by rules_docker container_image
29+
jq(
30+
name = name,
31+
args = ["--raw-output"],
32+
srcs = ["_{}_index.json".format(name)],
33+
filter = """.manifests[0].digest""",
34+
out = name + ".json.sha256", # path chosen to match rules_docker for easy migration
35+
**kwargs
36+
)

private/oci/sign_and_push.bzl

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,59 @@
11
"rules for signing, attesting and pushing images"
22

33
load("@bazel_skylib//rules:write_file.bzl", "write_file")
4-
load("@rules_oci//cosign:defs.bzl", "cosign_attest", "cosign_sign")
5-
load("@rules_oci//oci:defs.bzl", "oci_push")
64
load("//private/pkg:oci_image_spdx.bzl", "oci_image_spdx")
5+
load(":digest.bzl", "digest")
76

87
PUSH_AND_SIGN_CMD = """\
8+
# Push {IMAGE}
99
repository="$(stamp "{REPOSITORY}")"
10-
tag="$(stamp "{TAG}")"
11-
12-
[[ -n $EXPORT ]] && echo "$repository:$tag" >> $EXPORT
13-
14-
# Push the image by its digest
15-
"$(realpath {PUSH_CMD})" --repository "$repository"
16-
17-
# Attest the sbom
18-
GOOGLE_SERVICE_ACCOUNT_NAME="$KEYLESS" "$(realpath {ATTEST_CMD})" --repository "$repository" --yes
19-
20-
# Sign keyless by using an identity
21-
GOOGLE_SERVICE_ACCOUNT_NAME="$KEYLESS" "$(realpath {SIGN_CMD})" --repository "$repository" --yes
10+
digest="$(cat {DIGEST})"
11+
echo "Pushing $repository@$digest"
12+
{CRANE} push {IMAGE} "$repository@$digest"
13+
{COSIGN} attest "$repository@$digest" --predicate "{SBOM}" --type "spdx" --yes
14+
{COSIGN} sign "$repository@$digest" --yes
15+
"""
2216

23-
# Tag the image
24-
"$(realpath {PUSH_CMD})" --repository "$repository" --tag "$tag"
17+
TAG_CMD = """\
18+
# Tag {IMAGE}
19+
from="$(stamp "{FROM}")"
20+
to="$(stamp "{TO}")"
21+
{CRANE} copy "$from" "$to"
2522
"""
2623

2724
def _sign_and_push_impl(ctx):
2825
cmds = []
29-
runfiles = ctx.runfiles(files = ctx.files.targets + [ctx.version_file])
3026

31-
for (target, url) in ctx.attr.targets.items():
27+
runfiles = ctx.runfiles(files = ctx.files.targets + [ctx.version_file, ctx.file._crane, ctx.file._cosign])
28+
29+
for (image, target) in ctx.attr.targets.items():
3230
files = target[DefaultInfo].files.to_list()
33-
runfiles = runfiles.merge(target[DefaultInfo].default_runfiles)
34-
repository_and_tag = url.split(":")
31+
32+
all_refs = ctx.attr.refs[image]
33+
first_ref = all_refs[0]
34+
repository_and_tag = first_ref.split(":")
3535
cmds.append(
3636
PUSH_AND_SIGN_CMD.format(
37-
ATTEST_CMD = files[0].short_path,
38-
SIGN_CMD = files[1].short_path,
39-
PUSH_CMD = files[2].short_path,
37+
IMAGE = files[0].short_path,
38+
SBOM = files[1].short_path,
39+
DIGEST = files[2].short_path,
40+
CRANE = ctx.file._crane.short_path,
41+
COSIGN = ctx.file._cosign.short_path,
4042
REPOSITORY = repository_and_tag[0],
4143
TAG = repository_and_tag[1],
4244
),
4345
)
4446

47+
for ref in all_refs[1:]:
48+
cmds.append(
49+
TAG_CMD.format(
50+
IMAGE = image,
51+
FROM = first_ref,
52+
TO = ref,
53+
CRANE = ctx.file._crane.short_path,
54+
),
55+
)
56+
4557
executable = ctx.actions.declare_file("{}_sign_and_push.sh".format(ctx.label.name))
4658
ctx.actions.expand_template(
4759
template = ctx.file._push_tpl,
@@ -58,8 +70,11 @@ def _sign_and_push_impl(ctx):
5870
sign_and_push = rule(
5971
implementation = _sign_and_push_impl,
6072
attrs = {
61-
"targets": attr.label_keyed_string_dict(mandatory = True, cfg = "exec"),
73+
"refs": attr.string_list_dict(mandatory = True),
74+
"targets": attr.string_keyed_label_dict(mandatory = True, cfg = "exec"),
6275
"_push_tpl": attr.label(default = "sign_and_push.sh.tpl", allow_single_file = True),
76+
"_crane": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_crane_toolchains//:current_toolchain"),
77+
"_cosign": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_cosign_toolchains//:current_toolchain"),
6378
},
6479
executable = True,
6580
)
@@ -69,59 +84,53 @@ def sign_and_push_all(name, images):
6984
7085
Args:
7186
name: name of the target
72-
images: a dict where keys are fully qualified image url and values are image labels
87+
images: a dict where keys are fully qualified image reference and values are image label
7388
"""
74-
image_dict = dict()
75-
query_dict = dict()
76-
for (idx, (url, image)) in enumerate(images.items()):
77-
oci_push(
78-
name = "{}_{}_push".format(name, idx),
79-
image = image,
80-
repository = "repository.default.local",
81-
)
89+
90+
dedup_image_dict = dict()
91+
dedup_push_dict = dict()
92+
93+
for (idx, (ref, image)) in enumerate(images.items()):
94+
if image in dedup_image_dict:
95+
dedup_image_dict[image].append(ref)
96+
else:
97+
dedup_image_dict[image] = [ref]
98+
99+
for (idx, (image, ref)) in enumerate(dedup_image_dict.items()):
82100
oci_image_spdx(
83101
name = "{}_{}_sbom".format(name, idx),
84102
image = image,
85103
)
86-
cosign_attest(
87-
name = "{}_{}_attest".format(name, idx),
88-
image = image,
89-
type = "spdx",
90-
predicate = "{}_{}_sbom".format(name, idx),
91-
repository = "repository.default.local",
92-
)
93-
cosign_sign(
94-
name = "{}_{}_sign".format(name, idx),
104+
digest(
105+
name = "{}_{}_digest".format(name, idx),
95106
image = image,
96-
repository = "repository.default.local",
97107
)
108+
98109
native.filegroup(
99110
name = "{}_{}".format(name, idx),
100111
srcs = [
101-
":{}_{}_attest".format(name, idx),
102-
":{}_{}_sign".format(name, idx),
103-
":{}_{}_push".format(name, idx),
112+
image,
113+
":{}_{}_sbom".format(name, idx),
114+
":{}_{}_digest".format(name, idx),
104115
],
105116
)
106117

107-
image_dict[":{}_{}".format(name, idx)] = url
108-
query_dict[image] = url.split(":") + [":{}_{}_push".format(name, idx)]
118+
dedup_push_dict[image] = "{}_{}".format(name, idx)
109119

110120
write_file(
111121
name = name + ".query",
112122
content = [
113-
"{repo} {tag} {push_label} {image_label}".format(
114-
repo = ref[0],
115-
tag = ref[1],
116-
push_label = ref[2],
117-
image_label = image,
123+
"{repo} {image}".format(
124+
repo = refs[0],
125+
image = image,
118126
)
119-
for (image, ref) in query_dict.items()
127+
for (image, refs) in dedup_image_dict.items()
120128
],
121129
out = name + "_query",
122130
)
123131

124132
sign_and_push(
125133
name = name,
126-
targets = image_dict,
134+
targets = dedup_push_dict,
135+
refs = dedup_image_dict,
127136
)

private/oci/sign_and_push.sh.tpl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,13 @@
22
set -o pipefail -o errexit -o nounset
33

44
KEYLESS="${KEYLESS:-}"
5-
EXPORT=""
65

76
while (( $# > 0 )); do
87
case $1 in
98
(--keyless)
109
KEYLESS="$2"
1110
shift
1211
shift;;
13-
(--export)
14-
EXPORT="$2"
15-
echo -n "" > $EXPORT
16-
shift
17-
shift;;
1812
(*)
1913
echo "unknown arg $1"
2014
exit 1
@@ -42,6 +36,8 @@ function stamp() {
4236
}
4337

4438

39+
export GOOGLE_SERVICE_ACCOUNT_NAME="${KEYLESS}"
40+
4541
{{CMDS}}
4642

4743
echo ""

private/tools/diff.bash

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,39 +176,39 @@ stamp_origin() {
176176
}
177177

178178
function test_image() {
179-
IFS=" " read -r repo tag push_label image_label <<<"$1"
179+
IFS=" " read -r repo image_label <<<"$1"
180180

181181
if [[ "${ONLY}" != "" && "${ONLY}" != "$image_label" ]]; then
182182
return
183183
fi
184184

185185
repo_origin=$(stamp_origin "$repo")
186186
repo_stage=$(stamp_stage "$repo")
187-
tag_stamped=$(stamp_origin "$tag")
188187

189188
if [[ "${SKIP_INDEX}" == "1" ]]; then
190-
if ! crane manifest "$repo_origin:$tag_stamped" | jq -e '.mediaType == "application/vnd.oci.image.manifest.v1+json"' > /dev/null; then
191-
echo "⏭️ Skipping image index $repo_origin:$tag_stamped "
189+
if ! crane manifest "$repo_origin" | jq -e '.mediaType == "application/vnd.oci.image.manifest.v1+json"' > /dev/null; then
190+
echo "⏭️ Skipping image index $repo_origin"
192191
return
193192
fi
194193
fi
195194

196195
echo ""
197-
echo "🚧 Diffing $repo_origin:$tag_stamped against $repo_stage:$tag_stamped"
196+
echo "🚧 Diffing $repo_origin against $repo_stage"
198197
echo ""
199198

200-
bazel run $push_label -- --repository $repo_stage --tag $tag_stamped
201-
if ! diffoci diff --pull=always --all-platforms --semantic "$repo_origin:$tag_stamped" "$repo_stage:$tag_stamped"; then
199+
bazel build "$image_label"
200+
crane push "$(bazel cquery --output=files $image_label)" "$repo_stage"
201+
if ! diffoci diff --pull=always --all-platforms --semantic "$repo_origin" "$repo_stage"; then
202202
echo ""
203203
echo " 🔬 To reproduce: bazel run //private/tools:diff -- --only $image_label"
204204
echo ""
205-
echo "👎 $repo_origin:$tag_stamped and $repo_stage:$tag_stamped are different."
205+
echo "👎 $repo_origin and $repo_stage are different."
206206
if [[ "${SET_GITHUB_OUTPUT}" == "1" ]]; then
207207
echo "$image_label" >> "$CHANGED_IMAGES_FILE"
208208
fi
209209
else
210210
echo ""
211-
echo "👍 $repo_origin:$tag_stamped and $repo_stage:$tag_stamped are identical."
211+
echo "👍 $repo_origin and $repo_stage are identical."
212212
fi
213213
}
214214

0 commit comments

Comments
 (0)