diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 69fc9c4..0b73cd6 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1848,7 +1848,7 @@ }, "//tests:test_extensions.bzl%helm_test": { "general": { - "bzlTransitiveDigest": "f/tLmy8NcbHdWDFJfxgwSyVXuZio5xJr0+VA1G15SZs=", + "bzlTransitiveDigest": "fLUj6KqPIgj437Y/MbsFuE6HN4dod2UhituECab593Q=", "accumulatedFileDigests": {}, "envVariables": {}, "generatedRepoSpecs": { diff --git a/helm/private/helm_package.bzl b/helm/private/helm_package.bzl index 9434952..dec1f96 100644 --- a/helm/private/helm_package.bzl +++ b/helm/private/helm_package.bzl @@ -8,6 +8,7 @@ OciPushRepositoryInfo = provider( doc = "Repository and image information for a given oci_push target", fields = { "image_root": "File: The directory containing the image files for the oci_push target", + "remote_tags_file": "File (optional): The file containing remote tags (one per line) used for the oci_push target", "repository_file": "File: The file containing the repository path for the oci_push target", }, ) @@ -31,9 +32,14 @@ def _oci_push_repository_aspect_impl(target, ctx): target.label, )) + remote_tags_file = None + if hasattr(ctx.rule.file, "remote_tags") and ctx.rule.file.remote_tags: + remote_tags_file = ctx.rule.file.remote_tags + return [OciPushRepositoryInfo( repository_file = output, image_root = ctx.rule.file.image, + remote_tags_file = remote_tags_file, )] # This aspect exists because rules_oci seems reluctant to create a provider @@ -133,6 +139,12 @@ def _helm_package_impl(ctx): str(image.label).strip("@").replace("/", "_").replace(":", "_") + ".image_manifest", )) push_info = image[DefaultInfo] + + remote_tags_path = None + if image[OciPushRepositoryInfo].remote_tags_file: + remote_tags_path = image[OciPushRepositoryInfo].remote_tags_file.path + image_inputs.append(image[OciPushRepositoryInfo].remote_tags_file) + ctx.actions.write( output = single_image_manifest, content = json.encode_indent( @@ -140,6 +152,7 @@ def _helm_package_impl(ctx): label = str(image.label), repository_path = image[OciPushRepositoryInfo].repository_file.path, image_root_path = image[OciPushRepositoryInfo].image_root.path, + remote_tags_path = remote_tags_path, ), ), ) diff --git a/helm/private/packager/packager.go b/helm/private/packager/packager.go index 95f95a3..1c0c5d3 100644 --- a/helm/private/packager/packager.go +++ b/helm/private/packager/packager.go @@ -22,12 +22,14 @@ type ImageInfo struct { Label string Repository string Digest string + RemoteTag string } type ImageManifest struct { Label string `json:"label"` RepositoryPath string `json:"repository_path"` ImageRootPath string `json:"image_root_path"` + RemoteTagsPath string `json:"remote_tags_path"` } type ImageIndexManifest struct { @@ -227,6 +229,20 @@ func imageManifestToImageInfo(imageManifest ImageManifest) (ImageInfo, error) { } imageInfo.Digest = imageIndex.Manifests[0].Digest + + if imageManifest.RemoteTagsPath != "" { + remoteTagsContent, err := os.ReadFile(imageManifest.RemoteTagsPath) + if err != nil { + return imageInfo, fmt.Errorf("read remote tags file %q: %w", imageManifest.RemoteTagsPath, err) + } + + remoteTags := strings.Split(strings.Trim(string(remoteTagsContent), "\n"), "\n") + if len(remoteTags) == 1 { + // With many remote tags we can't say for sure which one should be used + imageInfo.RemoteTag = remoteTags[0] + } + } + return imageInfo, nil } @@ -243,17 +259,43 @@ func loadImageStamps(imageManifestPath string) ([]ReplacementGroup, error) { replacementGroups := []ReplacementGroup{} + isSingleImage := len(imageInfos) == 1 + for _, imageInfo := range imageInfos { + repository := imageInfo.Repository + digest := imageInfo.Digest + tag := imageInfo.RemoteTag + workspaceLabel := strings.Replace(imageInfo.Label, "@@", "@", 1) bzmodLabel := fmt.Sprintf("@%s", imageInfo.Label) - imageUrl := fmt.Sprintf("%s@%s", imageInfo.Repository, imageInfo.Digest) + imageUrl := fmt.Sprintf("%s@%s", repository, digest) + + replacements := map[string]string{ + workspaceLabel: imageUrl, + workspaceLabel + ".repository": repository, + workspaceLabel + ".digest": digest, + bzmodLabel: imageUrl, + bzmodLabel + ".repository": repository, + bzmodLabel + ".digest": digest, + } + if tag != "" { + replacements[workspaceLabel+".tag"] = tag + replacements[bzmodLabel+".tag"] = tag + } + + // in case of single image add well-known replacements for image details + if isSingleImage { + replacements["bazel.image.url"] = imageUrl + replacements["bazel.image.repository"] = repository + replacements["bazel.image.digest"] = digest + if tag != "" { + replacements["bazel.image.tag"] = tag + } + } replacementGroups = append(replacementGroups, ReplacementGroup{ - Name: imageInfo.Label, - Replacements: map[string]string{ - workspaceLabel: imageUrl, - bzmodLabel: imageUrl, - }, + Name: imageInfo.Label, + Replacements: replacements, }) } diff --git a/tests/with_image_deps/BUILD.bazel b/tests/with_image_deps/BUILD.bazel index fb709ac..d9d1066 100644 --- a/tests/with_image_deps/BUILD.bazel +++ b/tests/with_image_deps/BUILD.bazel @@ -18,6 +18,7 @@ helm_chart( images = [ ":image_a.push", "//tests/with_image_deps:image_b.push", + "//tests/with_image_deps:image_c.push", ], target_compatible_with = EXCLUDE_WINDOWS, values = "values.yaml", @@ -36,12 +37,14 @@ helm_package_regex_test( values_patterns = [ r"image_a:\s+url:\s+\"docker.io/rules_helm/test/image_a@sha256:[a-z0-9]{64}\"", r"image_b:\s+url:\s+\"docker.io/rules_helm/test/image_b@sha256:[a-z0-9]{64}\"", + r"image_c:\s+url:\s+\"docker.io/rules_helm/test/image_c:1.2.3\"", ], ) _IMAGES = [ "image_a", "image_b", + "image_c", ] [ @@ -75,3 +78,11 @@ oci_push( repository_file = ":image_b.repository.txt", target_compatible_with = EXCLUDE_WINDOWS, ) + +oci_push( + name = "image_c.push", + image = ":image_c", + remote_tags = ["1.2.3"], + repository = "docker.io/rules_helm/test/image_c", + target_compatible_with = EXCLUDE_WINDOWS, +) diff --git a/tests/with_image_deps/values.yaml b/tests/with_image_deps/values.yaml index d687a98..6983838 100644 --- a/tests/with_image_deps/values.yaml +++ b/tests/with_image_deps/values.yaml @@ -15,6 +15,8 @@ bazel_produced_images: url: "{@//tests/with_image_deps:image_a.push}" image_b: url: "{@//tests/with_image_deps:image_b.push}" + image_c: + url: "{@//tests/with_image_deps:image_c.push.repository}:{@//tests/with_image_deps:image_c.push.tag}" imagePullSecrets: [] nameOverride: ""