diff --git a/pkg/action/package.go b/pkg/action/package.go index d3e8d1a52..b8ee21c68 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -129,7 +129,7 @@ func (p *Package) Run(path string, _ map[string]any) (string, error) { } if p.SourceDateEpoch != nil { - stampModTimes(ch, *p.SourceDateEpoch) + ch.StampModTimes(*p.SourceDateEpoch) } name, err := chartutil.Save(ch, dest) @@ -265,25 +265,3 @@ func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) } return os.Open(passphraseFile) } - -// stampModTimes recursively sets all file modification times in a chart to t. -// t is normalized to UTC and truncated to whole seconds before use because tar -// headers have second-level granularity and timezone-independent storage. -// This is used to produce reproducible archives when SourceDateEpoch is set. -func stampModTimes(c *chart.Chart, t time.Time) { - t = t.UTC().Truncate(time.Second) - c.ModTime = t - c.SchemaModTime = t - for _, f := range c.Raw { - f.ModTime = t - } - for _, f := range c.Templates { - f.ModTime = t - } - for _, f := range c.Files { - f.ModTime = t - } - for _, dep := range c.Dependencies() { - stampModTimes(dep, t) - } -} diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go index ccc73ab91..2f6f50330 100644 --- a/pkg/action/package_test.go +++ b/pkg/action/package_test.go @@ -232,3 +232,40 @@ func TestRunWithSourceDateEpochReproducible(t *testing.T) { second := build() require.Equal(t, first, second, "two builds with the same SOURCE_DATE_EPOCH must be byte-identical") } + +func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) { + chartPath := "testdata/charts/chart-with-lock" + epoch := time.Unix(1700000000, 0).UTC() + + client := NewPackage() + client.SourceDateEpoch = &epoch + + filename, err := client.Run(chartPath, nil) + require.NoError(t, err) + t.Cleanup(func() { os.Remove(filename) }) + + f, err := os.Open(filename) + require.NoError(t, err) + defer f.Close() + + gr, err := gzip.NewReader(f) + require.NoError(t, err) + defer gr.Close() + + found := false + tr := tar.NewReader(gr) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + if path.Base(hdr.Name) == "Chart.lock" { + found = true + // Without stamping Lock.Generated, this entry would keep the + // fixture's original 2016 timestamp instead of the epoch. + require.True(t, epoch.Equal(hdr.ModTime), "Chart.lock entry: got ModTime %v, want %v", hdr.ModTime, epoch) + } + } + require.True(t, found, "expected archive to contain a Chart.lock entry") +} diff --git a/pkg/action/testdata/charts/chart-with-lock/Chart.lock b/pkg/action/testdata/charts/chart-with-lock/Chart.lock new file mode 100644 index 000000000..824df0a40 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-lock/Chart.lock @@ -0,0 +1,3 @@ +dependencies: [] +digest: sha256:0000000000000000000000000000000000000000000000000000000000000 +generated: "2016-01-01T00:00:00Z" diff --git a/pkg/action/testdata/charts/chart-with-lock/Chart.yaml b/pkg/action/testdata/charts/chart-with-lock/Chart.yaml new file mode 100644 index 000000000..2cb1b3557 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-lock/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: chart-with-lock +version: 0.1.0 +description: Test chart with a Chart.lock, used to verify SourceDateEpoch stamping diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index 4cfc2b890..adfaab15c 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -105,6 +105,32 @@ func (ch *Chart) Root() *Chart { // Dependencies are the charts that this chart depends on. func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } +// StampModTimes recursively sets all file modification times in the chart and +// its dependencies to t. t is normalized to UTC and truncated to whole seconds +// because tar headers have second-level granularity and timezone-independent +// storage. This is used to produce reproducible archives when a build process +// supplies a fixed timestamp (e.g. via SOURCE_DATE_EPOCH). +func (ch *Chart) StampModTimes(t time.Time) { + t = t.UTC().Truncate(time.Second) + ch.ModTime = t + ch.SchemaModTime = t + if ch.Lock != nil { + ch.Lock.Generated = t + } + for _, f := range ch.Raw { + f.ModTime = t + } + for _, f := range ch.Templates { + f.ModTime = t + } + for _, f := range ch.Files { + f.ModTime = t + } + for _, dep := range ch.dependencies { + dep.StampModTimes(t) + } +} + // IsRoot determines if the chart is the root chart. func (ch *Chart) IsRoot() bool { return ch.parent == nil }