pull/32162/merge
Lohit Kolluri 2 days ago committed by GitHub
commit 7aeba72572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -23,6 +23,7 @@ import (
"os"
"path/filepath"
"syscall"
"time"
"github.com/Masterminds/semver/v3"
"golang.org/x/term"
@ -58,6 +59,8 @@ type Package struct {
KeyFile string
CaFile string
InsecureSkipTLSVerify bool
// SourceDateEpoch, when set, normalizes chart timestamps for reproducible archives.
SourceDateEpoch *time.Time
}
const (
@ -103,6 +106,10 @@ func (p *Package) Run(path string, _ map[string]any) (string, error) {
ch.Metadata.AppVersion = p.AppVersion
}
if p.SourceDateEpoch != nil {
ch.StampModTimes(*p.SourceDateEpoch)
}
if reqs := ac.MetaDependencies(); len(reqs) > 0 {
if err := CheckDependencies(ch, reqs); err != nil {
return "", err

@ -177,6 +177,37 @@ 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
if len(ch.Schema) > 0 {
ch.SchemaModTime = epoch
}
if ch.Lock != nil {
ch.Lock.Generated = epoch
}
for _, f := range ch.Raw {
if f != nil {
f.ModTime = epoch
}
}
for _, f := range ch.Templates {
if f != nil {
f.ModTime = epoch
}
}
for _, f := range ch.Files {
if f != nil {
f.ModTime = epoch
}
}
for _, dep := range ch.Dependencies() {
dep.StampModTimes(epoch)
}
}
func hasManifestExtension(fname string) bool {
ext := filepath.Ext(fname)
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")

@ -186,6 +186,40 @@ func TestSavePreservesTimestamps(t *testing.T) {
}
}
func TestSaveWithSourceDateEpoch(t *testing.T) {
epoch := time.Unix(1609459200, 0).UTC()
tmp := t.TempDir()
c := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "ahab",
Version: "1.2.3",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
}
c.StampModTimes(epoch)
where, err := Save(c, tmp)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
allHeaders, err := retrieveAllHeadersFromTar(where)
if err != nil {
t.Fatalf("Failed to parse tar: %v", err)
}
expected := epoch.Round(time.Second)
for _, header := range allHeaders {
if !header.ModTime.Equal(expected) {
t.Fatalf("Expected SOURCE_DATE_EPOCH timestamp %v, got %v for %q", expected, header.ModTime, header.Name)
}
}
}
// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function
// as well, so we are not duplicating components of the code which iterate
// through the tar.

@ -55,6 +55,10 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
if len(args) > 0 {
chartpath = filepath.Clean(args[0])
}
sourceDateEpoch, err := sourceDateEpochFromEnv()
if err != nil {
return err
}
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
@ -72,6 +76,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
RepositoryCache: settings.RepositoryCache,
ContentCache: settings.ContentCache,
Debug: settings.Debug,
SourceDateEpoch: sourceDateEpoch,
}
if client.Verify {
man.Verify = downloader.VerifyIfPossible

@ -58,6 +58,10 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
if len(args) > 0 {
chartpath = filepath.Clean(args[0])
}
sourceDateEpoch, err := sourceDateEpochFromEnv()
if err != nil {
return err
}
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
@ -75,6 +79,7 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
RepositoryCache: settings.RepositoryCache,
ContentCache: settings.ContentCache,
Debug: settings.Debug,
SourceDateEpoch: sourceDateEpoch,
}
if client.Verify {
man.Verify = downloader.VerifyAlways

@ -297,6 +297,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
if req := ac.MetaDependencies(); len(req) > 0 {
sourceDateEpoch, err := sourceDateEpochFromEnv()
if err != nil {
return nil, err
}
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
@ -313,6 +317,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
ContentCache: settings.ContentCache,
Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
SourceDateEpoch: sourceDateEpoch,
}
if err := man.Update(); err != nil {
return nil, err

@ -59,6 +59,11 @@ func newPackageCmd(out io.Writer) *cobra.Command {
if len(args) == 0 {
return errors.New("need at least one argument, the path to the chart")
}
sourceDateEpoch, err := sourceDateEpochFromEnv()
if err != nil {
return err
}
client.SourceDateEpoch = sourceDateEpoch
if client.Sign {
if client.Key == "" {
return errors.New("--key is required for signing a package")
@ -101,6 +106,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
ContentCache: settings.ContentCache,
SourceDateEpoch: sourceDateEpoch,
}
if err := downloadManager.Update(); err != nil {

@ -84,6 +84,7 @@ Environment variables:
| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values |
| $HELM_COLOR | set color output mode. Allowed values: never, always, auto (default: never) |
| $NO_COLOR | set to any non-empty value to disable all colored output (overrides $HELM_COLOR) |
| $SOURCE_DATE_EPOCH | set a Unix timestamp for reproducible chart archives |
Helm stores cache, configuration, and data based on the following configuration order:

@ -0,0 +1,42 @@
/*
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 cmd
import (
"errors"
"fmt"
"os"
"strconv"
"time"
)
// sourceDateEpochFromEnv returns SOURCE_DATE_EPOCH when set, or nil when unset.
func sourceDateEpochFromEnv() (*time.Time, error) {
epochStr, ok := os.LookupEnv("SOURCE_DATE_EPOCH")
if !ok || epochStr == "" {
return nil, nil
}
epoch, err := strconv.ParseInt(epochStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid SOURCE_DATE_EPOCH: %w", err)
}
if epoch < 0 {
return nil, errors.New("invalid SOURCE_DATE_EPOCH: must not be negative")
}
t := time.Unix(epoch, 0).UTC()
return &t, nil
}

@ -0,0 +1,82 @@
/*
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 cmd
import (
"testing"
"time"
)
func TestSourceDateEpochFromEnv(t *testing.T) {
t.Setenv("SOURCE_DATE_EPOCH", "1609459200")
got, err := sourceDateEpochFromEnv()
if err != nil {
t.Fatalf("sourceDateEpochFromEnv() error: %v", err)
}
if got == nil {
t.Fatal("expected non-nil epoch")
}
want := time.Unix(1609459200, 0).UTC()
if !got.Equal(want) {
t.Fatalf("expected %v, got %v", want, *got)
}
}
func TestSourceDateEpochFromEnvUnset(t *testing.T) {
t.Setenv("SOURCE_DATE_EPOCH", "")
got, err := sourceDateEpochFromEnv()
if err != nil {
t.Fatalf("sourceDateEpochFromEnv() error: %v", err)
}
if got != nil {
t.Fatalf("expected nil epoch, got %v", *got)
}
}
func TestSourceDateEpochFromEnvInvalid(t *testing.T) {
t.Setenv("SOURCE_DATE_EPOCH", "not-a-number")
if _, err := sourceDateEpochFromEnv(); err == nil {
t.Fatal("expected error for invalid SOURCE_DATE_EPOCH")
}
}
func TestSourceDateEpochFromEnvNegative(t *testing.T) {
t.Setenv("SOURCE_DATE_EPOCH", "-1")
if _, err := sourceDateEpochFromEnv(); err == nil {
t.Fatal("expected error for negative SOURCE_DATE_EPOCH")
}
}
func TestSourceDateEpochFromEnvZero(t *testing.T) {
t.Setenv("SOURCE_DATE_EPOCH", "0")
got, err := sourceDateEpochFromEnv()
if err != nil {
t.Fatalf("sourceDateEpochFromEnv() error: %v", err)
}
if got == nil {
t.Fatal("expected non-nil epoch")
}
want := time.Unix(0, 0).UTC()
if !got.Equal(want) {
t.Fatalf("expected %v, got %v", want, *got)
}
}

@ -204,6 +204,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err
}
if req := ac.MetaDependencies(); len(req) > 0 {
sourceDateEpoch, err := sourceDateEpochFromEnv()
if err != nil {
return err
}
if err := action.CheckDependencies(ch, req); err != nil {
err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: %w", err)
if client.DependencyUpdate {
@ -217,6 +221,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
RepositoryCache: settings.RepositoryCache,
ContentCache: settings.ContentCache,
Debug: settings.Debug,
SourceDateEpoch: sourceDateEpoch,
}
if err := man.Update(); err != nil {
return err

@ -29,6 +29,7 @@ import (
"regexp"
"strings"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"sigs.k8s.io/yaml"
@ -78,6 +79,8 @@ type Manager struct {
// ContentCache is a location where a cache of charts can be stored
ContentCache string
// SourceDateEpoch, when set, normalizes chart timestamps for reproducible archives.
SourceDateEpoch *time.Time
}
// Build rebuilds a local charts directory from a lockfile.
@ -304,7 +307,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.SourceDateEpoch)
if err != nil {
saveError = err
break
@ -870,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, sourceDateEpoch *time.Time) (string, error) {
if !strings.HasPrefix(repo, "file://") {
return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo)
}
@ -885,6 +888,10 @@ func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, e
return "", err
}
if sourceDateEpoch != nil {
ch.StampModTimes(*sourceDateEpoch)
}
constraint, err := semver.NewConstraint(version)
if err != nil {
return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %w", name, err)

Loading…
Cancel
Save