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
92 changes: 86 additions & 6 deletions helm/private/packager/packager.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -461,6 +462,71 @@ func sanitizeChartContent(content string) (string, error) {
return content, nil
}

// OCI image-spec pre-defined annotation key.
const ociCreatedAnnotation = "org.opencontainers.image.created"

// parseCreated accepts either epoch seconds (all digits) or RFC3339.
func parseCreated(s string) (time.Time, error) {
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
return time.Unix(n, 0).UTC(), nil
}
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return time.Time{}, err
}
return t.UTC(), nil
}

// A brace survives only when a {KEY} stamp token went unresolved; no valid
// created value (RFC3339 or epoch seconds) contains one.
func hasUnresolvedToken(s string) bool { return strings.Contains(s, "{") }

// A yaml.Node round-trip preserves Chart.yaml fields the HelmChart struct does
// not model; re-marshalling through HelmChart would drop them.
func normalizeCreatedAnnotation(content, canonical string) (string, error) {
var doc yaml.Node
if err := yaml.Unmarshal([]byte(content), &doc); err != nil {
return "", fmt.Errorf("unmarshal chart: %w", err)
}
if len(doc.Content) == 0 {
return content, nil
}

// A document node wraps the top mapping in Content[0].
top := doc.Content[0]
if top.Kind != yaml.MappingNode {
return content, nil
}

// Mapping node `.Content` is alternating key,value pairs.
var annotations *yaml.Node
for i := 0; i+1 < len(top.Content); i += 2 {
if top.Content[i].Value == "annotations" {
annotations = top.Content[i+1]
break
}
}
if annotations == nil || annotations.Kind != yaml.MappingNode {
return content, nil
}

for i := 0; i+1 < len(annotations.Content); i += 2 {
if annotations.Content[i].Value == ociCreatedAnnotation {
value := annotations.Content[i+1]
value.Value = canonical
value.Tag = "!!str"
value.Style = yaml.DoubleQuotedStyle
break
}
}

out, err := yaml.Marshal(&doc)
if err != nil {
return "", fmt.Errorf("marshal chart: %w", err)
}
return string(out), nil
}

func copyFile(source string, dest string) error {
srcFile, err := os.Open(source)
if err != nil {
Expand Down Expand Up @@ -958,12 +1024,6 @@ func main() {

log.SetFlags(log.LstdFlags | log.Lshortfile)

// Stamped builds embed wall-clock time into the OCI `created` annotation
// so the published manifest reflects build time.
if args.VolatileStatusFile != "" {
chartTime = time.Now().UTC()
}

cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -1047,6 +1107,26 @@ func main() {
log.Fatal(err)
}

// oci_digest merges this annotation over its mtime-derived value into the OCI
// manifest, so the resolved value also fixes the published digest. Canonicalize
// it to RFC3339 to keep the tgz mtimes, metadata.json, and manifest in
// agreement. chartTime otherwise stays at the reproducible epoch-0 default.
stampedChart, err := loadChart(stampedChartContent)
if err != nil {
log.Fatal(err)
}
if raw := stampedChart.Annotations[ociCreatedAnnotation]; raw != "" {
if !hasUnresolvedToken(raw) {
if chartTime, err = parseCreated(raw); err != nil {
log.Fatalf("%s=%q is not epoch seconds or RFC3339: %v", ociCreatedAnnotation, raw, err)
}
}
stampedChartContent, err = normalizeCreatedAnnotation(stampedChartContent, chartTime.Format(time.RFC3339))
if err != nil {
log.Fatalf("normalize %s: %v", ociCreatedAnnotation, err)
}
}

// Create a directory in which to run helm package
helmDir, err := installHelmContent(dir, args.Package, stampedChartContent, stampedValuesContent, stampedSchemaContent, args.TemplatesManifest, args.FilesManifest, args.CrdsManifest, args.DepsManifest)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions tests/created/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
charts
5 changes: 5 additions & 0 deletions tests/created/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
load(":created_test.bzl", "created_test_suite")

created_test_suite(
name = "created_test_suite",
)
1 change: 1 addition & 0 deletions tests/created/created.digest.oci_digest.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:abef8a69dbd58aad23804d65a6a9726601ee208b55b82c329e861309c04d3f06
142 changes: 142 additions & 0 deletions tests/created/created_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""helm_package derives the OCI `org.opencontainers.image.created` annotation
from the chart's own annotation, canonicalized to RFC3339, defaulting to
reproducible epoch-0 when it is absent or a `{KEY}` stamp token is unresolved.
"""

load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//helm:defs.bzl", "helm_chart", "helm_package")
load("//helm:helm_package_info.bzl", "HelmPackageInfo")

def _helm_pkg_metadata_impl(ctx):
return DefaultInfo(files = depset([ctx.attr.chart[HelmPackageInfo].metadata]))

_helm_pkg_metadata = rule(
implementation = _helm_pkg_metadata_impl,
doc = "Extracts the `metadata.json` sidecar from a `helm_package` target.",
attrs = {
"chart": attr.label(
doc = "The `helm_package` target to parse metadata from.",
providers = [HelmPackageInfo],
mandatory = True,
),
},
)

def _chart_json(created = None):
content = {
"apiVersion": "v2",
"appVersion": "1.16.0",
"description": "A Helm chart for testing the created annotation.",
"icon": "https://helm.sh/img/helm.svg",
"name": "created",
"type": "application",
"version": "0.1.0",
}
if created != None:
content["annotations"] = {"org.opencontainers.image.created": created}
return json.encode(content)

def _metadata_golden(name, created):
write_file(
name = "{}.expected_metadata".format(name),
out = "{}.expected_metadata.json".format(name),
content = """\
{{
"created": "{}",
"name": "created",
"version": "0.1.0"
}}
""".format(created).splitlines(),
newline = "unix",
)

def created_test_suite(name):
"""Declares the created-annotation test targets.

Args:
name: Name for the wrapping test_suite target.
"""

tests = []

# variant -> (annotation value, stamp, expected canonical `created`)
variants = {
"default.no_stamp": (None, 0, "1970-01-01T00:00:00Z"),
"default.stamp": (None, 1, "1970-01-01T00:00:00Z"),
"literal_epoch": ("1751328000", 0, "2025-07-01T00:00:00Z"),
"literal_rfc3339": ("2026-07-01T00:00:00Z", 0, "2026-07-01T00:00:00Z"),
"token.no_stamp": ("{STABLE_SOURCE_DATE_EPOCH}", 0, "1970-01-01T00:00:00Z"),
"token.stamp": ("{STABLE_SOURCE_DATE_EPOCH}", 1, "2009-02-13T23:31:30Z"),
}

for variant, (annotation, stamp, expected) in variants.items():
helm_package(
name = "created.{}".format(variant),
chart_json = _chart_json(annotation),
templates = native.glob(["templates/**"]),
values = "values.yaml",
stamp = stamp,
)

_helm_pkg_metadata(
name = "created.{}.metadata".format(variant),
chart = ":created.{}".format(variant),
)

_metadata_golden("created.{}".format(variant), expected)

diff_test(
name = "created.{}.metadata_test".format(variant),
file1 = "created.{}.expected_metadata".format(variant),
file2 = "created.{}.metadata".format(variant),
)
tests.append("created.{}.metadata_test".format(variant))

# Assert the packaged Chart.yaml carries canonical RFC3339, not the raw epoch
# `1234567890`. The tar path uses the chart `name:` (`created`).
native.genrule(
name = "created.token.stamp.chart_created_line",
srcs = [":created.token.stamp"],
outs = ["created.token.stamp.chart_created_line.txt"],
cmd = "tar -xzOf $(location :created.token.stamp) created/Chart.yaml | grep org.opencontainers.image.created > $@",
)

write_file(
name = "created.token.stamp.expected_chart_created_line",
out = "created.token.stamp.expected_chart_created_line.txt",
content = [
' org.opencontainers.image.created: "2009-02-13T23:31:30Z"',
"",
],
newline = "unix",
)

diff_test(
name = "created.token.stamp.normalization_test",
file1 = "created.token.stamp.expected_chart_created_line",
file2 = "created.token.stamp.chart_created_line",
)
tests.append("created.token.stamp.normalization_test")

# A stamped chart yields a byte-identical OCI digest across builds: the
# immutable-re-push guarantee.
helm_chart(
name = "created.digest",
chart_json = _chart_json("{STABLE_SOURCE_DATE_EPOCH}"),
templates = native.glob(["templates/**"]),
values = "values.yaml",
stamp = 1,
)

diff_test(
name = "created.digest_test",
file1 = "created.digest.oci_digest.golden",
file2 = ":created.digest.oci_digest",
)
tests.append("created.digest_test")

native.test_suite(
name = name,
tests = tests,
)
6 changes: 6 additions & 0 deletions tests/created/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-created
data:
data: {{ .Values.data }}
2 changes: 2 additions & 0 deletions tests/created/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Values for the `created` annotation test chart.
data: hello
20 changes: 2 additions & 18 deletions tests/version_stamp/version_stamp_unit_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,7 @@ load("//helm:defs.bzl", "helm_lint_test", "helm_package", "helm_template_test")
load("//helm:helm_package_info.bzl", "HelmPackageInfo")

def _helm_pkg_metadata_impl(ctx):
src = ctx.attr.chart[HelmPackageInfo].metadata
if not ctx.attr.stamp:
return DefaultInfo(files = depset([src]))

# Stamped builds embed time.Now() into `created`, so strip it before
# diffing — the rest of the metadata is deterministic and worth pinning.
out = ctx.actions.declare_file(ctx.label.name + ".filtered.json")
ctx.actions.run_shell(
inputs = [src],
outputs = [out],
command = "printf '%s' \"$(grep -v created {})\" > {}".format(src.path, out.path),
)
return DefaultInfo(files = depset([out]))
return DefaultInfo(files = depset([ctx.attr.chart[HelmPackageInfo].metadata]))

_helm_pkg_metadata = rule(
implementation = _helm_pkg_metadata_impl,
Expand All @@ -29,10 +17,6 @@ _helm_pkg_metadata = rule(
providers = [HelmPackageInfo],
mandatory = True,
),
"stamp": attr.bool(
doc = "Whether to stamp the metadata with build time.",
default = False,
),
},
)

Expand Down Expand Up @@ -60,7 +44,6 @@ def version_stamp_test_suite(name):
_helm_pkg_metadata(
name = "version_stamp.{}.metadata".format(name),
chart = ":version_stamp.{}".format(name),
stamp = stamp_value,
)

helm_lint_test(
Expand Down Expand Up @@ -97,6 +80,7 @@ def version_stamp_test_suite(name):
out = "version_stamp.stamp.expected_metadata.json",
content = """\
{
"created": "1970-01-01T00:00:00Z",
"name": "version-stamp",
"version": "0.1.0+stable-volatile"
}
Expand Down
4 changes: 4 additions & 0 deletions tools/workspace_status.bat
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ ECHO OFF

echo STABLE_STAMP_VALUE stable
echo VOLATILE_STAMP_VALUE volatile
REM Fixed stand-in for a commit-derived source-date epoch (real builds:
REM `git log -1 --format=%%ct`). STABLE_ puts it in the stable status file,
REM part of the action cache key, so a given commit stays reproducible.
echo STABLE_SOURCE_DATE_EPOCH 1234567890
4 changes: 4 additions & 0 deletions tools/workspace_status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ set -euo pipefail

echo STABLE_STAMP_VALUE "stable"
echo VOLATILE_STAMP_VALUE "volatile"
# Fixed stand-in for a commit-derived source-date epoch (real builds: `git log
# -1 --format=%ct`). The STABLE_ prefix puts it in the stable status file, part
# of the action cache key, so a given commit stays reproducible.
echo STABLE_SOURCE_DATE_EPOCH "1234567890"
Loading