fix(chart): normalize StampModTimes timestamp + Chart.lock test

Closes #32396.

StampModTimes now normalizes the supplied time to UTC and truncates to
whole seconds before stamping. Without this, an SDK caller passing a
local-timezone or sub-second time.Time would produce a Chart.lock
generated: field with a timezone offset or fractional seconds, making
the lock file content non-reproducible across machines even when the
same SOURCE_DATE_EPOCH value is used — defeating the purpose of the
feature.

Also add:
- testdata/charts/chart-with-lock fixture: a minimal chart with a
  Chart.lock whose generated: timestamp predates SOURCE_DATE_EPOCH.
- TestRunWithSourceDateEpochStampsLockGenerated: packages the fixture
  with SourceDateEpoch set to a non-UTC, sub-second time.Time and
  asserts that the resulting archive's Chart.lock entry has both the
  correct tar modtime and the correct (normalized) generated: value in
  the marshaled YAML.

Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
pull/32485/head
Ilya Kiselev 1 month ago
parent 7e641d30a9
commit 3c3be926aa

@ -17,9 +17,13 @@ limitations under the License.
package action
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
@ -144,3 +148,58 @@ func TestRun(t *testing.T) {
require.Equal(t, "empty-0.1.0.tgz", filename)
require.NoError(t, os.Remove(filename))
}
// TestRunWithSourceDateEpochStampsLockGenerated verifies that packaging a chart
// that has a Chart.lock stamps both the tar entry modtime and the marshaled
// generated: field in Chart.lock to the given epoch.
//
// This guards against the normalization regression where a caller supplying a
// local-timezone or sub-second time.Time would produce a non-reproducible
// generated: value even when the same SOURCE_DATE_EPOCH is used on different
// machines.
func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) {
// Use a non-local, non-UTC timezone and sub-second precision to confirm
// normalization: without UTC().Truncate(time.Second) the generated: field
// would contain a timezone offset or fractional seconds.
loc := time.FixedZone("UTC+3", 3*60*60)
rawEpoch := time.Unix(1700000000, 123456789).In(loc)
epoch := rawEpoch.UTC().Truncate(time.Second)
client := NewPackage()
client.SourceDateEpoch = &rawEpoch
filename, err := client.Run("testdata/charts/chart-with-lock", 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" {
continue
}
found = true
require.True(t, epoch.Equal(hdr.ModTime),
"Chart.lock tar modtime: got %v, want %v", hdr.ModTime, epoch)
raw, err := io.ReadAll(tr)
require.NoError(t, err)
wantGenerated := epoch.Format(time.RFC3339)
require.Contains(t, string(raw), wantGenerated,
"Chart.lock generated: field should contain normalized UTC timestamp")
}
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

@ -177,34 +177,41 @@ func (ch *Chart) CRDObjects() []CRD {
return crds
}
// StampModTimes sets timestamps on the chart (and dependencies) to epoch.
// This is used for reproducible builds via SOURCE_DATE_EPOCH.
func (ch *Chart) StampModTimes(epoch time.Time) {
ch.ModTime = epoch
// StampModTimes sets timestamps on the chart (and dependencies) to t,
// normalized to UTC and truncated to whole seconds.
//
// Normalization is required because Chart.lock's generated: field is written
// by yaml.Marshal from Lock.Generated. Without UTC/truncate, a caller
// supplying a local-zone or sub-second time.Time produces a generated: value
// with a timezone offset or fractional seconds, making the lock file content
// non-reproducible across machines even when the same SOURCE_DATE_EPOCH is used.
func (ch *Chart) StampModTimes(t time.Time) {
t = t.UTC().Truncate(time.Second)
ch.ModTime = t
if len(ch.Schema) > 0 {
ch.SchemaModTime = epoch
ch.SchemaModTime = t
}
if ch.Lock != nil {
ch.Lock.Generated = epoch
ch.Lock.Generated = t
}
for _, f := range ch.Raw {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Templates {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Files {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, dep := range ch.Dependencies() {
dep.StampModTimes(epoch)
dep.StampModTimes(t)
}
}

Loading…
Cancel
Save