pull/31845/merge
Maxime Grenu 2 days ago committed by GitHub
commit badc65a4ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,77 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"fmt"
"os"
"strconv"
"time"
chart "helm.sh/helm/v4/internal/chart/v3"
)
// ParseSourceDateEpoch reads the SOURCE_DATE_EPOCH environment variable and
// returns the corresponding time. It returns the zero time when the variable
// is not set or is set to the empty string. An error is returned when the
// value cannot be parsed or is negative.
//
// SOURCE_DATE_EPOCH is a standardised environment variable for reproducible
// builds; see https://reproducible-builds.org/docs/source-date-epoch/
func ParseSourceDateEpoch() (time.Time, error) {
v, ok := os.LookupEnv("SOURCE_DATE_EPOCH")
if !ok || v == "" {
return time.Time{}, nil
}
epoch, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("invalid SOURCE_DATE_EPOCH %q: %w", v, err)
}
if epoch < 0 {
return time.Time{}, fmt.Errorf("invalid SOURCE_DATE_EPOCH %q: negative value", v)
}
return time.Unix(epoch, 0), nil
}
// ApplySourceDateEpoch overrides the ModTime on the chart and all of its
// entries to t, ensuring reproducible archives regardless of the original
// timestamps. It recurses into dependencies.
// When t is the zero time this is a no-op.
func ApplySourceDateEpoch(c *chart.Chart, t time.Time) {
if t.IsZero() {
return
}
c.ModTime = t
if c.Lock != nil {
c.Lock.Generated = t
}
if c.Schema != nil {
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() {
ApplySourceDateEpoch(dep, t)
}
}

@ -0,0 +1,267 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"os"
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/internal/chart/v3"
)
func TestParseSourceDateEpoch(t *testing.T) {
tests := []struct {
name string
value string
set bool
want time.Time
wantErr bool
}{
{
name: "not set",
set: false,
want: time.Time{},
},
{
name: "valid epoch",
value: "1700000000",
set: true,
want: time.Unix(1700000000, 0),
},
{
name: "invalid string",
value: "not-a-number",
set: true,
wantErr: true,
},
{
name: "negative value",
value: "-1",
set: true,
wantErr: true,
},
{
name: "zero",
value: "0",
set: true,
want: time.Unix(0, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.set {
t.Setenv("SOURCE_DATE_EPOCH", tt.value)
} else {
prevVal, wasSet := os.LookupEnv("SOURCE_DATE_EPOCH")
os.Unsetenv("SOURCE_DATE_EPOCH")
t.Cleanup(func() {
if wasSet {
os.Setenv("SOURCE_DATE_EPOCH", prevVal)
}
})
}
got, err := ParseSourceDateEpoch()
if (err != nil) != tt.wantErr {
t.Errorf("ParseSourceDateEpoch() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !got.Equal(tt.want) {
t.Errorf("ParseSourceDateEpoch() = %v, want %v", got, tt.want)
}
})
}
}
func TestApplySourceDateEpoch(t *testing.T) {
epoch := time.Unix(1700000000, 0)
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
Templates: []*common.File{
{Name: "templates/test.yaml"},
},
Files: []*common.File{
{Name: "README.md"},
},
}
ApplySourceDateEpoch(c, epoch)
if !c.ModTime.Equal(epoch) {
t.Errorf("Chart.ModTime = %v, want %v", c.ModTime, epoch)
}
for _, f := range c.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("Template %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
for _, f := range c.Files {
if !f.ModTime.Equal(epoch) {
t.Errorf("File %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
}
func TestApplySourceDateEpochOverridesExisting(t *testing.T) {
epoch := time.Unix(1700000000, 0)
existing := time.Unix(1600000000, 0)
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
ModTime: existing,
Templates: []*common.File{
{Name: "templates/test.yaml", ModTime: existing},
},
Files: []*common.File{
{Name: "README.md", ModTime: existing},
},
}
ApplySourceDateEpoch(c, epoch)
if !c.ModTime.Equal(epoch) {
t.Errorf("Chart.ModTime = %v, want epoch %v", c.ModTime, epoch)
}
for _, f := range c.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("Template %s ModTime = %v, want epoch %v", f.Name, f.ModTime, epoch)
}
}
for _, f := range c.Files {
if !f.ModTime.Equal(epoch) {
t.Errorf("File %s ModTime = %v, want epoch %v", f.Name, f.ModTime, epoch)
}
}
}
func TestApplySourceDateEpochZeroNoop(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
}
ApplySourceDateEpoch(c, time.Time{})
if !c.ModTime.IsZero() {
t.Errorf("Chart.ModTime = %v, want zero", c.ModTime)
}
}
func TestApplySourceDateEpochDependencies(t *testing.T) {
epoch := time.Unix(1700000000, 0)
existing := time.Unix(1600000000, 0)
dep := &chart.Chart{
Metadata: &chart.Metadata{
Name: "dep",
Version: "0.1.0",
},
Templates: []*common.File{
{Name: "templates/dep.yaml"},
},
}
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "parent",
Version: "1.0.0",
},
ModTime: existing,
Templates: []*common.File{
{Name: "templates/main.yaml"},
},
}
c.AddDependency(dep)
ApplySourceDateEpoch(c, epoch)
// Parent chart had an existing ModTime, but it should be overridden.
if !c.ModTime.Equal(epoch) {
t.Errorf("parent Chart.ModTime = %v, want epoch %v", c.ModTime, epoch)
}
// Dependency had a zero ModTime, so it should be stamped.
if !dep.ModTime.Equal(epoch) {
t.Errorf("dep Chart.ModTime = %v, want %v", dep.ModTime, epoch)
}
for _, f := range dep.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("dep Template %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
}
func TestSaveWithSourceDateEpoch(t *testing.T) {
// End-to-end: parse SOURCE_DATE_EPOCH, apply to a chart with zero
// ModTimes, save as a tar archive, and verify every tar entry carries
// exactly the expected timestamp.
const epochStr = "1700000000"
want := time.Unix(1700000000, 0)
t.Setenv("SOURCE_DATE_EPOCH", epochStr)
epoch, err := ParseSourceDateEpoch()
if err != nil {
t.Fatalf("ParseSourceDateEpoch() error: %v", err)
}
c := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV3,
Name: "epoch-test",
Version: "0.1.0",
},
Values: map[string]any{"key": "value"},
Schema: []byte(`{"title": "Values"}`),
Files: []*common.File{{Name: "README.md", Data: []byte("# test")}},
Templates: []*common.File{{Name: "templates/test.yaml", Data: []byte("apiVersion: v1")}},
}
ApplySourceDateEpoch(c, epoch)
tmp := t.TempDir()
where, err := Save(c, tmp)
if err != nil {
t.Fatalf("Save() error: %v", err)
}
headers, err := retrieveAllHeadersFromTar(where)
if err != nil {
t.Fatalf("failed to read tar: %v", err)
}
if len(headers) == 0 {
t.Fatal("archive contains no entries")
}
for _, h := range headers {
if !h.ModTime.Equal(want) {
t.Errorf("tar entry %q ModTime = %v, want %v", h.Name, h.ModTime, want)
}
}
}

@ -109,6 +109,13 @@ func (p *Package) Run(path string, _ map[string]any) (string, error) {
}
}
// Apply SOURCE_DATE_EPOCH for reproducible builds if set.
epoch, err := chartutil.ParseSourceDateEpoch()
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: %v\n", err)
}
chartutil.ApplySourceDateEpoch(ch, epoch)
var dest string
if p.Destination == "." {
// Save to the current working directory.

@ -0,0 +1,77 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"fmt"
"os"
"strconv"
"time"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// ParseSourceDateEpoch reads the SOURCE_DATE_EPOCH environment variable and
// returns the corresponding time. It returns the zero time when the variable
// is not set or is set to the empty string. An error is returned when the
// value cannot be parsed or is negative.
//
// SOURCE_DATE_EPOCH is a standardised environment variable for reproducible
// builds; see https://reproducible-builds.org/docs/source-date-epoch/
func ParseSourceDateEpoch() (time.Time, error) {
v, ok := os.LookupEnv("SOURCE_DATE_EPOCH")
if !ok || v == "" {
return time.Time{}, nil
}
epoch, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("invalid SOURCE_DATE_EPOCH %q: %w", v, err)
}
if epoch < 0 {
return time.Time{}, fmt.Errorf("invalid SOURCE_DATE_EPOCH %q: negative value", v)
}
return time.Unix(epoch, 0), nil
}
// ApplySourceDateEpoch overrides the ModTime on the chart and all of its
// entries to t, ensuring reproducible archives regardless of the original
// timestamps. It recurses into dependencies.
// When t is the zero time this is a no-op.
func ApplySourceDateEpoch(c *chart.Chart, t time.Time) {
if t.IsZero() {
return
}
c.ModTime = t
if c.Lock != nil {
c.Lock.Generated = t
}
if c.Schema != nil {
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() {
ApplySourceDateEpoch(dep, t)
}
}

@ -0,0 +1,269 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"os"
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
func TestParseSourceDateEpoch(t *testing.T) {
tests := []struct {
name string
value string
set bool
want time.Time
wantErr bool
}{
{
name: "not set",
set: false,
want: time.Time{},
},
{
name: "valid epoch",
value: "1700000000",
set: true,
want: time.Unix(1700000000, 0),
},
{
name: "invalid string",
value: "not-a-number",
set: true,
wantErr: true,
},
{
name: "negative value",
value: "-1",
set: true,
wantErr: true,
},
{
name: "zero",
value: "0",
set: true,
want: time.Unix(0, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.set {
t.Setenv("SOURCE_DATE_EPOCH", tt.value)
} else {
prevVal, wasSet := os.LookupEnv("SOURCE_DATE_EPOCH")
os.Unsetenv("SOURCE_DATE_EPOCH")
t.Cleanup(func() {
if wasSet {
os.Setenv("SOURCE_DATE_EPOCH", prevVal)
}
})
}
got, err := ParseSourceDateEpoch()
if (err != nil) != tt.wantErr {
t.Errorf("ParseSourceDateEpoch() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !got.Equal(tt.want) {
t.Errorf("ParseSourceDateEpoch() = %v, want %v", got, tt.want)
}
})
}
}
func TestApplySourceDateEpoch(t *testing.T) {
epoch := time.Unix(1700000000, 0)
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
Templates: []*common.File{
{Name: "templates/test.yaml"},
},
Files: []*common.File{
{Name: "README.md"},
},
}
ApplySourceDateEpoch(c, epoch)
if !c.ModTime.Equal(epoch) {
t.Errorf("Chart.ModTime = %v, want %v", c.ModTime, epoch)
}
for _, f := range c.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("Template %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
for _, f := range c.Files {
if !f.ModTime.Equal(epoch) {
t.Errorf("File %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
}
func TestApplySourceDateEpochOverridesExisting(t *testing.T) {
epoch := time.Unix(1700000000, 0)
existing := time.Unix(1600000000, 0)
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
ModTime: existing,
Templates: []*common.File{
{Name: "templates/test.yaml", ModTime: existing},
},
Files: []*common.File{
{Name: "README.md", ModTime: existing},
},
}
ApplySourceDateEpoch(c, epoch)
if !c.ModTime.Equal(epoch) {
t.Errorf("Chart.ModTime = %v, want epoch %v", c.ModTime, epoch)
}
for _, f := range c.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("Template %s ModTime = %v, want epoch %v", f.Name, f.ModTime, epoch)
}
}
for _, f := range c.Files {
if !f.ModTime.Equal(epoch) {
t.Errorf("File %s ModTime = %v, want epoch %v", f.Name, f.ModTime, epoch)
}
}
}
func TestApplySourceDateEpochZeroNoop(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test",
Version: "0.1.0",
},
}
ApplySourceDateEpoch(c, time.Time{})
if !c.ModTime.IsZero() {
t.Errorf("Chart.ModTime = %v, want zero", c.ModTime)
}
}
func TestApplySourceDateEpochDependencies(t *testing.T) {
epoch := time.Unix(1700000000, 0)
existing := time.Unix(1600000000, 0)
dep := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "dep",
Version: "0.1.0",
},
Templates: []*common.File{
{Name: "templates/dep.yaml"},
},
}
c := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "parent",
Version: "1.0.0",
},
ModTime: existing,
Templates: []*common.File{
{Name: "templates/main.yaml"},
},
}
c.AddDependency(dep)
ApplySourceDateEpoch(c, epoch)
// Parent chart had an existing ModTime, but it should be overridden.
if !c.ModTime.Equal(epoch) {
t.Errorf("parent Chart.ModTime = %v, want epoch %v", c.ModTime, epoch)
}
// Dependency had a zero ModTime, so it should be stamped.
if !dep.ModTime.Equal(epoch) {
t.Errorf("dep Chart.ModTime = %v, want %v", dep.ModTime, epoch)
}
for _, f := range dep.Templates {
if !f.ModTime.Equal(epoch) {
t.Errorf("dep Template %s ModTime = %v, want %v", f.Name, f.ModTime, epoch)
}
}
}
func TestSaveWithSourceDateEpoch(t *testing.T) {
// End-to-end: parse SOURCE_DATE_EPOCH, apply to a chart with zero
// ModTimes, save as a tar archive, and verify every tar entry carries
// exactly the expected timestamp.
const epochStr = "1700000000"
want := time.Unix(1700000000, 0)
t.Setenv("SOURCE_DATE_EPOCH", epochStr)
epoch, err := ParseSourceDateEpoch()
if err != nil {
t.Fatalf("ParseSourceDateEpoch() error: %v", err)
}
c := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "epoch-test",
Version: "0.1.0",
},
Values: map[string]any{"key": "value"},
Schema: []byte(`{"title": "Values"}`),
Files: []*common.File{{Name: "README.md", Data: []byte("# test")}},
Templates: []*common.File{{Name: "templates/test.yaml", Data: []byte("apiVersion: v1")}},
}
ApplySourceDateEpoch(c, epoch)
tmp := t.TempDir()
where, err := Save(c, tmp)
if err != nil {
t.Fatalf("Save() error: %v", err)
}
headers, err := retrieveAllHeadersFromTar(where)
if err != nil {
t.Fatalf("failed to read tar: %v", err)
}
if len(headers) == 0 {
t.Fatal("archive contains no entries")
}
for _, h := range headers {
if !h.ModTime.Equal(want) {
t.Errorf("tar entry %q ModTime = %v, want %v", h.Name, h.ModTime, want)
}
}
}

@ -304,7 +304,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
if m.Debug {
fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
}
ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath)
ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath, m.Out)
if err != nil {
saveError = err
break
@ -873,7 +873,7 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
}
// archive a dep chart from local directory and save it into destPath
func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, error) {
func tarFromLocalDir(chartpath, name, repo, version, destPath string, out io.Writer) (string, error) {
if !strings.HasPrefix(repo, "file://") {
return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
}
@ -899,6 +899,13 @@ func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, e
}
if constraint.Check(v) {
// Apply SOURCE_DATE_EPOCH for reproducible builds if set.
epoch, epochErr := chartutil.ParseSourceDateEpoch()
if epochErr != nil {
fmt.Fprintf(out, "WARNING: %v\n", epochErr)
}
chartutil.ApplySourceDateEpoch(ch, epoch)
_, err = chartutil.Save(ch, destPath)
return ch.Metadata.Version, err
}

Loading…
Cancel
Save