Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bazelversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.2.0
7.4.0
5 changes: 5 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ bazel_dep(name = "rules_oci", version = "1.7.5")
bazel_dep(name = "rules_distroless", version = "0.3.8")
bazel_dep(name = "rules_python", version = "0.35.0")

### OCI ###
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
oci.toolchains(crane_version = "v0.18.0")
use_repo(oci, "oci_crane_toolchains")

### PYTHON ###
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(
Expand Down
15,936 changes: 7,968 additions & 7,968 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions private/oci/digest.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"generate digest for oci_image and oci_image_index"

load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file")
load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path")
load("@aspect_bazel_lib//lib:jq.bzl", "jq")

# Normally we'd use the `.digest` target that rules_oci creates for every oci_image but
# we also use oci_image_index which does not have a digest target. This was fixed in
# https://github.com/bazel-contrib/rules_oci/pull/742 but it on the 2.x releases of rules_oci
# TODO: Remove this once we upgrade to rules_oci 2.x
def digest(name, image, **kwargs):
# `oci_image_rule` and `oci_image_index_rule` produce a directory as default output.
# Label for the [name]/index.json file
directory_path(
name = "_{}_index_json".format(name),
directory = image,
path = "index.json",
**kwargs
)

copy_file(
name = "_{}_index_json_cp".format(name),
src = "_{}_index_json".format(name),
out = "_{}_index.json".format(name),
**kwargs
)

# Matches the [name].digest target produced by rules_docker container_image
jq(
name = name,
args = ["--raw-output"],
srcs = ["_{}_index.json".format(name)],
filter = """.manifests[0].digest""",
out = name + ".json.sha256", # path chosen to match rules_docker for easy migration
**kwargs
)
119 changes: 64 additions & 55 deletions private/oci/sign_and_push.bzl
Original file line number Diff line number Diff line change
@@ -1,47 +1,59 @@
"rules for signing, attesting and pushing images"

load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@rules_oci//cosign:defs.bzl", "cosign_attest", "cosign_sign")
load("@rules_oci//oci:defs.bzl", "oci_push")
load("//private/pkg:oci_image_spdx.bzl", "oci_image_spdx")
load(":digest.bzl", "digest")

PUSH_AND_SIGN_CMD = """\
# Push {IMAGE}
repository="$(stamp "{REPOSITORY}")"
tag="$(stamp "{TAG}")"

[[ -n $EXPORT ]] && echo "$repository:$tag" >> $EXPORT

# Push the image by its digest
"$(realpath {PUSH_CMD})" --repository "$repository"

# Attest the sbom
GOOGLE_SERVICE_ACCOUNT_NAME="$KEYLESS" "$(realpath {ATTEST_CMD})" --repository "$repository" --yes

# Sign keyless by using an identity
GOOGLE_SERVICE_ACCOUNT_NAME="$KEYLESS" "$(realpath {SIGN_CMD})" --repository "$repository" --yes
digest="$(cat {DIGEST})"
echo "Pushing $repository@$digest"
{CRANE} push {IMAGE} "$repository@$digest"
{COSIGN} attest "$repository@$digest" --predicate "{SBOM}" --type "spdx" --yes
{COSIGN} sign "$repository@$digest" --yes
"""

# Tag the image
"$(realpath {PUSH_CMD})" --repository "$repository" --tag "$tag"
TAG_CMD = """\
# Tag {IMAGE}
from="$(stamp "{FROM}")"
to="$(stamp "{TO}")"
{CRANE} copy "$from" "$to"
"""

def _sign_and_push_impl(ctx):
cmds = []
runfiles = ctx.runfiles(files = ctx.files.targets + [ctx.version_file])

for (target, url) in ctx.attr.targets.items():
runfiles = ctx.runfiles(files = ctx.files.targets + [ctx.version_file, ctx.file._crane, ctx.file._cosign])

for (image, target) in ctx.attr.targets.items():
files = target[DefaultInfo].files.to_list()
runfiles = runfiles.merge(target[DefaultInfo].default_runfiles)
repository_and_tag = url.split(":")

all_refs = ctx.attr.refs[image]
first_ref = all_refs[0]
repository_and_tag = first_ref.split(":")
cmds.append(
PUSH_AND_SIGN_CMD.format(
ATTEST_CMD = files[0].short_path,
SIGN_CMD = files[1].short_path,
PUSH_CMD = files[2].short_path,
IMAGE = files[0].short_path,
SBOM = files[1].short_path,
DIGEST = files[2].short_path,
CRANE = ctx.file._crane.short_path,
COSIGN = ctx.file._cosign.short_path,
REPOSITORY = repository_and_tag[0],
TAG = repository_and_tag[1],
),
)

for ref in all_refs[1:]:
cmds.append(
TAG_CMD.format(
IMAGE = image,
FROM = first_ref,
TO = ref,
CRANE = ctx.file._crane.short_path,
),
)

executable = ctx.actions.declare_file("{}_sign_and_push.sh".format(ctx.label.name))
ctx.actions.expand_template(
template = ctx.file._push_tpl,
Expand All @@ -58,8 +70,11 @@ def _sign_and_push_impl(ctx):
sign_and_push = rule(
implementation = _sign_and_push_impl,
attrs = {
"targets": attr.label_keyed_string_dict(mandatory = True, cfg = "exec"),
"refs": attr.string_list_dict(mandatory = True),
"targets": attr.string_keyed_label_dict(mandatory = True, cfg = "exec"),
"_push_tpl": attr.label(default = "sign_and_push.sh.tpl", allow_single_file = True),
"_crane": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_crane_toolchains//:current_toolchain"),
"_cosign": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_cosign_toolchains//:current_toolchain"),
},
executable = True,
)
Expand All @@ -69,59 +84,53 @@ def sign_and_push_all(name, images):

Args:
name: name of the target
images: a dict where keys are fully qualified image url and values are image labels
images: a dict where keys are fully qualified image reference and values are image label
"""
image_dict = dict()
query_dict = dict()
for (idx, (url, image)) in enumerate(images.items()):
oci_push(
name = "{}_{}_push".format(name, idx),
image = image,
repository = "repository.default.local",
)

dedup_image_dict = dict()
dedup_push_dict = dict()

for (idx, (ref, image)) in enumerate(images.items()):
if image in dedup_image_dict:
dedup_image_dict[image].append(ref)
else:
dedup_image_dict[image] = [ref]

for (idx, (image, ref)) in enumerate(dedup_image_dict.items()):
oci_image_spdx(
name = "{}_{}_sbom".format(name, idx),
image = image,
)
cosign_attest(
name = "{}_{}_attest".format(name, idx),
image = image,
type = "spdx",
predicate = "{}_{}_sbom".format(name, idx),
repository = "repository.default.local",
)
cosign_sign(
name = "{}_{}_sign".format(name, idx),
digest(
name = "{}_{}_digest".format(name, idx),
image = image,
repository = "repository.default.local",
)

native.filegroup(
name = "{}_{}".format(name, idx),
srcs = [
":{}_{}_attest".format(name, idx),
":{}_{}_sign".format(name, idx),
":{}_{}_push".format(name, idx),
image,
":{}_{}_sbom".format(name, idx),
":{}_{}_digest".format(name, idx),
],
)

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

write_file(
name = name + ".query",
content = [
"{repo} {tag} {push_label} {image_label}".format(
repo = ref[0],
tag = ref[1],
push_label = ref[2],
image_label = image,
"{repo} {image}".format(
repo = refs[0],
image = image,
)
for (image, ref) in query_dict.items()
for (image, refs) in dedup_image_dict.items()
],
out = name + "_query",
)

sign_and_push(
name = name,
targets = image_dict,
targets = dedup_push_dict,
refs = dedup_image_dict,
)
8 changes: 2 additions & 6 deletions private/oci/sign_and_push.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@
set -o pipefail -o errexit -o nounset

KEYLESS="${KEYLESS:-}"
EXPORT=""

while (( $# > 0 )); do
case $1 in
(--keyless)
KEYLESS="$2"
shift
shift;;
(--export)
EXPORT="$2"
echo -n "" > $EXPORT
shift
shift;;
(*)
echo "unknown arg $1"
exit 1
Expand Down Expand Up @@ -42,6 +36,8 @@ function stamp() {
}


export GOOGLE_SERVICE_ACCOUNT_NAME="${KEYLESS}"

{{CMDS}}

echo ""
Expand Down
18 changes: 9 additions & 9 deletions private/tools/diff.bash
Original file line number Diff line number Diff line change
Expand Up @@ -176,39 +176,39 @@ stamp_origin() {
}

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

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

repo_origin=$(stamp_origin "$repo")
repo_stage=$(stamp_stage "$repo")
tag_stamped=$(stamp_origin "$tag")

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

echo ""
echo "🚧 Diffing $repo_origin:$tag_stamped against $repo_stage:$tag_stamped"
echo "🚧 Diffing $repo_origin against $repo_stage"
echo ""

bazel run $push_label -- --repository $repo_stage --tag $tag_stamped
if ! diffoci diff --pull=always --all-platforms --semantic "$repo_origin:$tag_stamped" "$repo_stage:$tag_stamped"; then
bazel build "$image_label"
crane push "$(bazel cquery --output=files $image_label)" "$repo_stage"
if ! diffoci diff --pull=always --all-platforms --semantic "$repo_origin" "$repo_stage"; then
echo ""
echo " 🔬 To reproduce: bazel run //private/tools:diff -- --only $image_label"
echo ""
echo "👎 $repo_origin:$tag_stamped and $repo_stage:$tag_stamped are different."
echo "👎 $repo_origin and $repo_stage are different."
if [[ "${SET_GITHUB_OUTPUT}" == "1" ]]; then
echo "$image_label" >> "$CHANGED_IMAGES_FILE"
fi
else
echo ""
echo "👍 $repo_origin:$tag_stamped and $repo_stage:$tag_stamped are identical."
echo "👍 $repo_origin and $repo_stage are identical."
fi
}

Expand Down
Loading