fix(package): make mod-time stamping a Chart method and stamp Lock.Generated

stampModTimes lived in pkg/action as a free function operating on
*chart.Chart from outside the package. Moved it to chart.Chart.StampModTimes
in pkg/chart/v2, which is also where it can reach the private dependencies
field directly instead of going through the exported Dependencies() accessor.

While moving it, also stamp Lock.Generated when Lock is set: chartutil.Save
uses Lock.Generated as the tar entry modtime for Chart.lock, so without this
the lock file entry kept its original timestamp and broke reproducibility
for any chart with a Chart.lock.

Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
pull/32173/head
Ilya Kiselev 2 weeks ago
parent b0c3f438f3
commit cd6bf78a20

@ -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)
}
}

@ -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")
}

@ -0,0 +1,3 @@
dependencies: []
digest: sha256:0000000000000000000000000000000000000000000000000000000000000
generated: "2016-01-01T00:00:00Z"

@ -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

@ -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 }

Loading…
Cancel
Save