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
25 changes: 22 additions & 3 deletions helm/private/oci_digest.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@ load(":providers.bzl", "HelmPackageInfo")
HelmOCIDigestInfo = provider(
doc = "OCI manifest digest for a Helm chart, computed at build time.",
fields = {
"config": "File: The raw OCI config blob (application/vnd.cncf.helm.config.v1+json)",
"digest": "File: Text file containing the OCI manifest digest (sha256:...)",
"manifest": "File: The raw OCI manifest bytes whose digest is in `digest`",
},
)

def _helm_oci_digest_impl(ctx):
pkg_info = ctx.attr.chart[HelmPackageInfo]

digest_out = ctx.actions.declare_file(ctx.label.name + ".digest")
manifest_out = ctx.actions.declare_file(ctx.label.name + ".manifest.json")
config_out = ctx.actions.declare_file(ctx.label.name + ".config.json")

args = ctx.actions.args()
args.add("-chart", pkg_info.chart)
args.add("-metadata", pkg_info.metadata)
args.add("-digest_output", digest_out)
args.add("-manifest_output", manifest_out)
args.add("-config_output", config_out)

ctx.actions.run(
executable = ctx.executable._oci_digest_tool,
arguments = [args],
inputs = [pkg_info.chart, pkg_info.metadata],
outputs = [digest_out],
outputs = [digest_out, manifest_out, config_out],
mnemonic = "HelmOCIDigest",
progress_message = "Computing OCI digest for %s" % ctx.label,
)
Expand All @@ -34,6 +40,14 @@ def _helm_oci_digest_impl(ctx):
),
HelmOCIDigestInfo(
digest = digest_out,
manifest = manifest_out,
config = config_out,
),
# Also as output groups, so the blobs are reachable from a BUILD file
# (filegroup, diff_test) and not only from a rule implementation.
OutputGroupInfo(
config = depset([config_out]),
manifest = depset([manifest_out]),
),
# Forward HelmPackageInfo so downstream rules can access
# both providers from a single target.
Expand All @@ -45,10 +59,15 @@ helm_oci_digest = rule(
doc = """Compute the OCI manifest digest for a Helm chart at build time.

This rule constructs the OCI manifest that `helm push` (Helm v4) would
create and computes its sha256 digest. The .tgz file's mtime is used for
the creation annotation, formatted in UTC to match the push side.
create and computes its sha256 digest. The creation annotation comes from
the chart's own `org.opencontainers.image.created` annotation, resolved by
the stamping pass, and is formatted in UTC to match the push side.

The digest file contains a single line: `sha256:<hex>`.

The manifest and config blobs are also emitted, so a pusher that uploads
them itself can PUT the exact bytes this digest was computed over. They are
not in the default outputs; read them off `HelmOCIDigestInfo`.
""",
attrs = {
"chart": attr.label(
Expand Down
19 changes: 19 additions & 0 deletions helm/private/oci_digest/oci_digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ func main() {
chartTgz := flag.String("chart", "", "Path to the Helm chart .tgz file")
metadataJSON := flag.String("metadata", "", "Path to the chart metadata JSON file")
digestOutput := flag.String("digest_output", "", "Path to write the manifest digest (sha256:...)")
manifestOutput := flag.String("manifest_output", "", "Optional path to write the OCI manifest bytes")
configOutput := flag.String("config_output", "", "Optional path to write the OCI config blob bytes")

flag.Parse()

Expand Down Expand Up @@ -214,4 +216,21 @@ func main() {
if err := os.WriteFile(*digestOutput, []byte(digest.String()), 0644); err != nil {
log.Fatalf("Error writing digest file: %v", err)
}

// The manifest and config blobs are written verbatim, never re-marshalled by
// a consumer: re-serializing this JSON can reorder keys or change whitespace,
// which changes the digest above and invalidates every descriptor that
// references it. A pusher that uploads the blobs itself needs these exact
// bytes to PUT.
if *manifestOutput != "" {
if err := os.WriteFile(*manifestOutput, manifestBytes, 0644); err != nil {
log.Fatalf("Error writing manifest file: %v", err)
}
}

if *configOutput != "" {
if err := os.WriteFile(*configOutput, configBlob, 0644); err != nil {
log.Fatalf("Error writing config file: %v", err)
}
}
}
26 changes: 26 additions & 0 deletions tests/simple/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ diff_test(
file1 = "simple.oci_digest.golden",
file2 = ":simple.oci_digest",
)

filegroup(
name = "simple_oci_manifest",
srcs = [":simple.oci_digest"],
output_group = "manifest",
)

filegroup(
name = "simple_oci_config",
srcs = [":simple.oci_digest"],
output_group = "config",
)

# The digest above is the sha256 of this manifest, so locking both keeps them
# honest: changing one without the other fails here.
diff_test(
name = "simple_oci_manifest_test",
file1 = "simple.oci_manifest.golden",
file2 = ":simple_oci_manifest",
)

diff_test(
name = "simple_oci_config_test",
file1 = "simple.oci_config.golden",
file2 = ":simple_oci_config",
)
1 change: 1 addition & 0 deletions tests/simple/simple.oci_config.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"simple","version":"0.1.0","description":"A Helm chart for Kubernetes","icon":"https://helm.sh/img/helm.svg","apiVersion":"v2","appVersion":"1.16.0","type":"application"}
1 change: 1 addition & 0 deletions tests/simple/simple.oci_manifest.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.helm.config.v1+json","digest":"sha256:8a48b77fb50ca4dad5251f0cf7a1faa443201a3bf572ff09eba16ce913d0a52a","size":178},"layers":[{"mediaType":"application/vnd.cncf.helm.chart.content.v1.tar+gzip","digest":"sha256:c3bce8332d61329ca952ee7c4d121dae03c86e4682951a1b0a4a66d94a92ab57","size":3856}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z","org.opencontainers.image.description":"A Helm chart for Kubernetes","org.opencontainers.image.title":"simple","org.opencontainers.image.version":"0.1.0"}}
Loading