diff --git a/mirror/defs.bzl b/mirror/defs.bzl index d5198b36..6a49b2e2 100644 --- a/mirror/defs.bzl +++ b/mirror/defs.bzl @@ -10,7 +10,10 @@ def _replace_colon_except_last_segment(input_string): output_string = "/".join(segments) return output_string -def _mirror_image_impl(ctx): +# Common implementation for mirror_image and mirror_image_test +# Uses the following ctx attributes: src_image, digest, dst, dst_prefix +# Returns the src_image, digest, and dst_without_hash +def _impl_common(ctx): digest = ctx.attr.digest src_image = ctx.attr.src_image v = src_image.split("@", 1) @@ -39,6 +42,11 @@ def _mirror_image_impl(ctx): dst_prefix = ctx.expand_make_variables("dst_prefix", ctx.attr.dst_prefix, {}) dst_without_hash = dst_prefix.strip("/") + "/" + src_repository + return src_image, digest, dst_without_hash + +def _mirror_image_impl(ctx): + src_image, digest, dst_without_hash = _impl_common(ctx) + digest_file = ctx.actions.declare_file(ctx.label.name + ".digest") ctx.actions.write( output = digest_file, @@ -81,9 +89,6 @@ mirror_image_rule = rule( mandatory = True, doc = "The image to mirror", ), - "image_name": attr.string( - doc = "The name that could be referred in manifests. This field is deprecated and unused.", - ), "digest": attr.string( mandatory = False, doc = "The digest of the image. If not provided, it will be extracted from the src_image.", @@ -114,46 +119,63 @@ Implements GitopsPushInfo and K8sPushInfo providers so the returned image can be """, ) -def validate_image_test(name, image, digest, tags = [], **kwargs): - """ - Create a test that validates the image existance using crane validate. - Image tag will be ignored if provided and only the digest will be used. - if digest is provided as a part of the image, it will be used. - It is an error to provide both digest and image with digest if they do not match. - """ - src_image = image - v = src_image.split("@", 1) - s = v[0] - if len(v) > 1: - # If the image has a digest, use that. - if digest and v[1] != digest: - fail("digest mismatch: %s != %s" % (v[1], digest)) - digest = v[1] - else: - # If the image does not have a digest, use the one provided. - src_image = s + "@" + digest - - if not digest: - fail("digest must be provided as an attribute to mirror_image or in the src_image") +def _validate_mirror_impl(ctx): + src_image, digest, dst_without_hash = _impl_common(ctx) - native.sh_test( - name = name, - size = "small", - srcs = ["@rules_gitops//mirror:validate_image.sh"], - data = [ - "@rules_gitops//vendor/github.com/google/go-containerregistry/cmd/crane:crane", - ], - args = [ - src_image, - ], - tags = ["requires-network"] + tags, - env = { - "CRANE_BIN": "$(location @rules_gitops//vendor/github.com/google/go-containerregistry/cmd/crane:crane)", + ctx.actions.expand_template( + template = ctx.file._validate_image_script, + output = ctx.outputs.executable, + substitutions = { + "{crane_tool}": ctx.executable.crane_tool.short_path, + "{src_image}": src_image, + "{digest}": digest, + "{dst_image}": dst_without_hash, }, - **kwargs + is_executable = True, ) -def mirror_image(name, src_image, digest, tags = [], **kwargs): - visibility = kwargs.pop("visibility", None) - mirror_image_rule(name = name, src_image = src_image, digest = digest, tags = tags, visibility = visibility, **kwargs) - validate_image_test(name = name + "_validate_src", image = src_image, digest = digest, visibility = visibility, tags = tags) + runfiles = ctx.runfiles(files = [ctx.file._validate_image_script]).merge(ctx.attr.crane_tool[DefaultInfo].default_runfiles) + + return DefaultInfo( + runfiles = runfiles, + executable = ctx.outputs.executable, + ) + +validate_mirror_test = rule( + implementation = _validate_mirror_impl, + test = True, + attrs = { + "src_image": attr.string( + mandatory = True, + doc = "The image to mirror", + ), + "digest": attr.string( + mandatory = False, + doc = "The digest of the image. If not provided, it will be extracted from the src_image.", + ), + "dst_prefix": attr.string( + doc = "The prefix of the destination image, should include the registry and repository. Either dst_prefix or dst_image must be specified.", + ), + "dst": attr.string( + doc = "The destination image location, should include the registry and repository. Either dst_prefix or dst_image must be specified.", + ), + "crane_tool": attr.label( + default = Label("//vendor/github.com/google/go-containerregistry/cmd/crane:crane"), + executable = True, + cfg = "exec", + ), + "_validate_image_script": attr.label( + default = ":validate_image.sh", + allow_single_file = True, + ), + }, + executable = True, + doc = """Validate a mirrored image. It checks if at least one of remote or local image exists. +""", +) + +def mirror_image(name, image_name = None, push_timeout = "30s", **kwargs): + if image_name: + fail("image_name is deprecated and unused") + mirror_image_rule(name = name, push_timeout = push_timeout, **kwargs) + validate_mirror_test(name = name + "_validate_src", **kwargs) diff --git a/mirror/mirror_image.sh b/mirror/mirror_image.sh index f1fc1dd2..3e87f418 100644 --- a/mirror/mirror_image.sh +++ b/mirror/mirror_image.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -x set -eu function guess_runfiles() { diff --git a/mirror/tests/BUILD.bazel b/mirror/tests/BUILD.bazel index 051b4606..e5ee512a 100644 --- a/mirror/tests/BUILD.bazel +++ b/mirror/tests/BUILD.bazel @@ -202,3 +202,25 @@ sh_test( }, tags = ["exclusive"], # this test starts a registry on fixed port 1338 ) + +sh_test( + name = "image_mirror_verify_test", + srcs = ["image_mirror_test.sh"], + data = [ + ":image_mirror", + ":image_mirror_validate_src", + ":push_image", + "//vendor/github.com/google/go-containerregistry/cmd/crane", + "//vendor/github.com/google/go-containerregistry/cmd/registry", + ], + env = { + "LOCAL": "localhost:1338/mirror/localhost1338/image@sha256:b812c0570a7c369b2863c64e22760dc1b1dbc025a739f02db376bac62862f4cc", + "REMOTE": "localhost:1338/image@sha256:b812c0570a7c369b2863c64e22760dc1b1dbc025a739f02db376bac62862f4cc", + "PUSH_IMAGE": "$(location :push_image)", + "IMAGE_MIRROR": "$(location :image_mirror)", + "IMAGE_MIRROR_VALIDATE_SRC": "$(location :image_mirror_validate_src)", + "CRANE_BIN": "$(location //vendor/github.com/google/go-containerregistry/cmd/crane)", + "REGISTRY_BIN": "$(location //vendor/github.com/google/go-containerregistry/cmd/registry)", + }, + tags = ["exclusive"], # this test starts a registry on fixed port 1338 +) diff --git a/mirror/tests/image_mirror_test.sh b/mirror/tests/image_mirror_test.sh new file mode 100755 index 00000000..211a114b --- /dev/null +++ b/mirror/tests/image_mirror_test.sh @@ -0,0 +1,111 @@ +#!/bin/bash -x +# +# This test runs the script passed as the first argument and verifies the image is pushed to the registry +# +${REGISTRY_BIN}& +registry_pid=$! +trap "kill -9 $registry_pid" EXIT + +echo verifying image $REMOTE does not exist +${CRANE_BIN} validate -v --fast --remote $REMOTE +if [ $? -eq 0 ]; then + echo "Image $REMOTE should not exist" + exit 1 +fi + +echo verifying image $LOCAL does not exist +${CRANE_BIN} validate -v --fast --remote $LOCAL +if [ $? -eq 0 ]; then + echo "Image $LOCAL should not exist" + exit 1 +fi + +#test should fail before pushing the image (no src, no dst) +echo verifying mirror image validation fails +${IMAGE_MIRROR_VALIDATE_SRC} +if [ $? -eq 0 ]; then + echo "Image verification should fail" + exit 1 +fi + +echo pushing image $SRC_IMAGE +${PUSH_IMAGE} + +echo verifying image $REMOTE exists +${CRANE_BIN} validate -v --fast --remote $REMOTE +if [ $? -ne 0 ]; then + echo "Image $REMOTE should exist" + exit 1 +fi + +echo verifying image $LOCAL does not exist +${CRANE_BIN} validate -v --fast --remote $LOCAL +if [ $? -eq 0 ]; then + echo "Image $LOCAL should not exist" + exit 1 +fi + +#test should succeed with src image only +echo verifying mirror image validation fails +${IMAGE_MIRROR_VALIDATE_SRC} +if [ $? -ne 0 ]; then + echo "Image verification should succeed" + exit 1 +fi + +echo running image mirror +${IMAGE_MIRROR} +if [ $? -ne 0 ]; then + echo "Image mirroring should succeed" + exit 1 +fi + +echo verifying image $LOCAL exists +${CRANE_BIN} validate -v --fast --remote $LOCAL +if [ $? -ne 0 ]; then + echo "Image $LOCAL should exist" + exit 1 +fi + +echo verifying image $REMOTE exists +${CRANE_BIN} validate -v --fast --remote $REMOTE +if [ $? -ne 0 ]; then + echo "Image $REMOTE should exist" + exit 1 +fi + +#test should succeed with src and dst images +echo verifying mirror image validation succeeds +${IMAGE_MIRROR_VALIDATE_SRC} +if [ $? -ne 0 ]; then + echo "Image verification should succeed" + exit 1 +fi + +echo removing image $REMOTE +${CRANE_BIN} delete $REMOTE || exit 1 + +echo verifying image $REMOTE does not exist +${CRANE_BIN} validate -v --fast --remote $REMOTE +if [ $? -eq 0 ]; then + echo "Image $REMOTE should not exist" + exit 1 +fi + +echo verifying image $LOCAL exists +${CRANE_BIN} validate -v --fast --remote $LOCAL +if [ $? -ne 0 ]; then + echo "Image $LOCAL should exist" + exit 1 +fi + +#test should succeed with dst image only +echo verifying mirror image validation succeeds +${IMAGE_MIRROR_VALIDATE_SRC} +if [ $? -ne 0 ]; then + echo "Image verification should succeed" + exit 1 +fi + + +# exit 1 \ No newline at end of file diff --git a/mirror/validate_image.sh b/mirror/validate_image.sh index 35afc6c4..40f48f30 100755 --- a/mirror/validate_image.sh +++ b/mirror/validate_image.sh @@ -1,3 +1,20 @@ #!/bin/bash -echo "Validating image $1" -$CRANE_BIN validate -v --fast --remote $1 +REMOTE="{src_image}" +LOCAL="{dst_image}@{digest}" +echo "Validating images ${REMOTE} and ${LOCAL}" +{crane_tool} validate -v --fast --remote ${REMOTE} +if [ $? -eq 0 ]; then + echo "Image ${REMOTE} exist" + exit 0 +fi + +echo "Image ${REMOTE} does not exist, checking ${LOCAL}" + +{crane_tool} validate -v --fast --remote ${LOCAL} +if [ $? -eq 0 ]; then + echo "Image ${LOCAL} exist" + exit 0 +fi + +echo "Image ${LOCAL} does not exist" +exit 1