From 7facf2984a393e7cd4f9700ba4b390e58a566733 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Sun, 21 Sep 2025 13:43:53 -0400 Subject: [PATCH] Reproducible chart archive builds Building the same chart into an archive multiple times will have the same sha256 hash. Perviously, the time in the headers for a file was time.Now() which changed each time. The time is now collected from the operating system when the file is loaded and this time is used. Fixes: #3612 Signed-off-by: Matt Farina --- internal/chart/v3/chart.go | 5 + internal/chart/v3/chart_test.go | 68 ++++--- internal/chart/v3/lint/rules/template_test.go | 22 ++- internal/chart/v3/loader/directory.go | 2 +- internal/chart/v3/loader/load.go | 17 +- internal/chart/v3/loader/load_test.go | 48 +++-- internal/chart/v3/util/create.go | 2 +- internal/chart/v3/util/save.go | 16 +- internal/chart/v3/util/save_test.go | 108 ++++++++++- pkg/action/action_test.go | 36 ++-- pkg/action/hooks_test.go | 10 +- pkg/action/install_test.go | 5 +- pkg/action/show_test.go | 30 ++-- pkg/chart/common/file.go | 4 + pkg/chart/common/util/values_test.go | 3 +- pkg/chart/loader/archive/archive.go | 8 +- pkg/chart/v2/chart.go | 5 + pkg/chart/v2/chart_test.go | 68 ++++--- pkg/chart/v2/lint/rules/template_test.go | 22 ++- pkg/chart/v2/loader/directory.go | 2 +- pkg/chart/v2/loader/load.go | 16 +- pkg/chart/v2/loader/load_test.go | 53 +++--- pkg/chart/v2/util/create.go | 2 +- pkg/chart/v2/util/save.go | 16 +- pkg/chart/v2/util/save_test.go | 107 ++++++++++- pkg/cmd/upgrade_test.go | 6 +- pkg/engine/engine_test.go | 170 ++++++++++-------- pkg/release/v1/mock.go | 2 +- 28 files changed, 586 insertions(+), 267 deletions(-) diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go index 2edc6c339..48f006e79 100644 --- a/internal/chart/v3/chart.go +++ b/internal/chart/v3/chart.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "helm.sh/helm/v4/pkg/chart/common" ) @@ -47,9 +48,13 @@ type Chart struct { Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` parent *Chart dependencies []*Chart diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index b1820ac0a..07cbf4b39 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -18,6 +18,7 @@ package v3 import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" @@ -25,27 +26,33 @@ import ( ) func TestCRDs(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ Raw: []*common.File{ { - Name: "fhqwhgads.yaml", - Data: []byte("Everybody to the Limit"), + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), }, }, } @@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) { } func TestCRDObjects(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", File: &common.File{ - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", File: &common.File{ - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, } diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go index 40bcfa26b..0fb4ca979 100644 --- a/internal/chart/v3/lint/rules/template_test.go +++ b/internal/chart/v3/lint/rules/template_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" "testing" + "time" chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/lint/support" @@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) { } func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() mychart := chart.Chart{ Metadata: &chart.Metadata{ APIVersion: "v2", @@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/baddeployment.yaml", - Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), }, { - Name: "templates/goodsecret.yaml", - Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), }, }, } @@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/configmap.yaml", - Data: []byte(manifest), + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), }, }, } @@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/empty-with-comments.yaml", - Data: []byte("#@formatter:off\n"), + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), }, }, } diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 8cb7323dc..dfe3af3b2 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &archive.BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index b1b4bba8f..1c5b4cad1 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -25,6 +25,7 @@ import ( "maps" "os" "path/filepath" + "slices" "strings" utilyaml "k8s.io/apimachinery/pkg/util/yaml" @@ -71,11 +72,12 @@ func Load(name string) (*chart.Chart, error) { func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c := new(chart.Chart) subcharts := make(map[string][]*archive.BufferedFile) + var subChartsKeys []string // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -89,6 +91,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { if c.Metadata.APIVersion == "" { c.Metadata.APIVersion = chart.APIVersionV3 } + c.ModTime = f.ModTime } } for _, f := range files { @@ -109,20 +112,24 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c.Values = values case f.Name == "values.schema.json": c.Schema = f.Data + c.SchemaModTime = f.ModTime case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) continue } fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) + if slices.Index(subChartsKeys, cname) == -1 { + subChartsKeys = append(subChartsKeys, cname) + } + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) default: - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } } diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 9f41429cc..f91005bf6 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -184,9 +184,11 @@ func TestLoadFile(t *testing.T) { } func TestLoadFiles(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v3 name: frobnitz description: This is a frobnitz. @@ -207,20 +209,24 @@ icon: https://example.com/64x64.png `), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "values.schema.json", - Data: []byte("type: Values"), + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, } @@ -260,26 +266,32 @@ icon: https://example.com/64x64.png // Test the order of file loading. The Chart.yaml file needs to come first for // later comparison checks. See https://github.com/helm/helm/pull/8948 func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "requirements.yaml", - Data: []byte("dependencies:"), + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v3 name: frobnitz description: This is a frobnitz. diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index 9f742e646..e193ef880 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) } schart.Templates = updatedTemplates diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index 49d93bf40..f755300ba 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -166,7 +166,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { return err } @@ -176,7 +176,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { return err } } @@ -184,7 +184,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save values.yaml for _, f := range c.Raw { if f.Name == ValuesfileName { - if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { return err } } @@ -195,7 +195,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if !json.Valid(c.Schema) { return errors.New("invalid JSON in " + SchemafileName) } - if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { return err } } @@ -203,7 +203,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save templates for _, f := range c.Templates { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -211,7 +211,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save files for _, f := range c.Files { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -226,13 +226,13 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { } // writeToTar writes a single file to a tar archive. -func writeToTar(out *tar.Writer, name string, body []byte) error { +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ Name: filepath.ToSlash(name), Mode: 0644, Size: int64(len(body)), - ModTime: time.Now(), + ModTime: modTime, } if err := out.WriteHeader(h); err != nil { return err diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 9b1b14a4c..93da34470 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -20,6 +20,8 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/sha256" + "fmt" "io" "os" "path" @@ -49,7 +51,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), } @@ -115,7 +117,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } _, err := Save(c, tmp) @@ -141,7 +143,6 @@ func TestSavePreservesTimestamps(t *testing.T) { // check will fail because `initialCreateTime` will be identical to the // written timestamp for the files. initialCreateTime := time.Now().Add(-1 * time.Second) - tmp := t.TempDir() c := &chart.Chart{ @@ -150,14 +151,16 @@ func TestSavePreservesTimestamps(t *testing.T) { Name: "ahab", Version: "1.2.3", }, + ModTime: initialCreateTime, Values: map[string]interface{}{ "imageName": "testimage", "imageId": 42, }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, } where, err := Save(c, tmp) @@ -170,8 +173,9 @@ func TestSavePreservesTimestamps(t *testing.T) { t.Fatalf("Failed to parse tar: %v", err) } + roundedTime := initialCreateTime.Round(time.Second) for _, header := range allHeaders { - if header.ModTime.Before(initialCreateTime) { + if !header.ModTime.Equal(roundedTime) { t.Fatalf("File timestamp not preserved: %v", header.ModTime) } } @@ -213,6 +217,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { func TestSaveDir(t *testing.T) { tmp := t.TempDir() + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ @@ -221,10 +226,10 @@ func TestSaveDir(t *testing.T) { Version: "1.2.3", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, }, Templates: []*common.File{ - {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -260,3 +265,90 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Did not get expected error for chart named %q", c.Name()) } } + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "bcb52ba7b7c2801be84cdc96d395f00749896a4679a7c9deacdfe934d0c49c1b", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "566bb87d0a044828e1e3acc4e9849b2c378eb9156a8662ceb618ea41b279bb10", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 78ca01089..5c17bb38b 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -122,9 +122,10 @@ type chartOptions struct { type chartOption func(*chartOptions) func buildChart(opts ...chartOption) *chart.Chart { + modTime := time.Now() defaultTemplates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifestWithHook)}, } return buildChartWithTemplates(defaultTemplates, opts...) } @@ -180,8 +181,9 @@ func withValues(values map[string]interface{}) chartOption { func withNotes(notes string) chartOption { return func(opts *chartOptions) { opts.Templates = append(opts.Templates, &common.File{ - Name: "templates/NOTES.txt", - Data: []byte(notes), + Name: "templates/NOTES.txt", + ModTime: time.Now(), + Data: []byte(notes), }) } } @@ -200,12 +202,13 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { func withSampleTemplates() chartOption { return func(opts *chartOptions) { + modTime := time.Now() sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -213,20 +216,21 @@ func withSampleTemplates() chartOption { func withSampleSecret() chartOption { return func(opts *chartOptions) { - sampleSecret := &common.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} + sampleSecret := &common.File{Name: "templates/secret.yaml", ModTime: time.Now(), Data: []byte("apiVersion: v1\nkind: Secret\n")} opts.Templates = append(opts.Templates, sampleSecret) } } func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { + modTime := time.Now() sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/incorrect", ModTime: modTime, Data: []byte("{{ .Values.bad.doh }}")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -235,7 +239,7 @@ func withSampleIncludingIncorrectTemplates() chartOption { func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { sampleTemplates := []*common.File{ - {Name: "templates/rbac", Data: []byte(rbacManifests)}, + {Name: "templates/rbac", ModTime: time.Now(), Data: []byte(rbacManifests)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -852,7 +856,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { Version: "0.1.0", }, Templates: []*common.File{ - {Name: "templates/invalid", Data: []byte("invalid: yaml: content:")}, + {Name: "templates/invalid", ModTime: time.Now(), Data: []byte("invalid: yaml: content:")}, }, } values := map[string]interface{}{} diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index fb7d1b4ec..b510c655f 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -178,9 +178,10 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + modTime := time.Now() templates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)}, } vals := map[string]interface{}{} @@ -205,9 +206,10 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + modTime := time.Now() templates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)}, } vals := map[string]interface{}{} diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index aae36152d..e85d04066 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -427,8 +427,9 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { mockChart := buildChart(withSampleTemplates()) mockChart.Templates = append(mockChart.Templates, &common.File{ - Name: "templates/lookup", - Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), + Name: "templates/lookup", + ModTime: time.Now(), + Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), }) res, err := instAction.Run(mockChart, vals) diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index faf306f2a..f3b767fca 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -18,6 +18,7 @@ package action import ( "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -26,17 +27,18 @@ import ( func TestShow(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "README.md", Data: []byte("README\n")}, - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, - {Name: "crds/baz.yaml", Data: []byte("baz\n")}, + {Name: "README.md", ModTime: modTime, Data: []byte("README\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, Raw: []*common.File{ - {Name: "values.yaml", Data: []byte("VALUES\n")}, + {Name: "values.yaml", ModTime: modTime, Data: []byte("VALUES\n")}, }, Values: map[string]interface{}{}, } @@ -104,13 +106,14 @@ func TestShowValuesByJsonPathFormat(t *testing.T) { func TestShowCRDs(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowCRDs, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, - {Name: "crds/baz.yaml", Data: []byte("baz\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, } @@ -137,12 +140,13 @@ baz func TestShowNoReadme(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, }, } diff --git a/pkg/chart/common/file.go b/pkg/chart/common/file.go index 304643f1a..1068bf450 100644 --- a/pkg/chart/common/file.go +++ b/pkg/chart/common/file.go @@ -15,6 +15,8 @@ limitations under the License. package common +import "time" + // File represents a file as a name/value pair. // // By convention, name is a relative path within the scope of the chart's @@ -24,4 +26,6 @@ type File struct { Name string `json:"name"` // Data is the template as byte data. Data []byte `json:"data"` + // ModTime is the file's mod-time + ModTime time.Time `json:"modtime,omitzero"` } diff --git a/pkg/chart/common/util/values_test.go b/pkg/chart/common/util/values_test.go index 5fc030567..706d3cfda 100644 --- a/pkg/chart/common/util/values_test.go +++ b/pkg/chart/common/util/values_test.go @@ -18,6 +18,7 @@ package util import ( "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -46,7 +47,7 @@ func TestToRenderValues(t *testing.T) { Templates: []*common.File{}, Values: chartValues, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } c.AddDependency(&chart.Chart{ diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go index 4d4ca4391..c6875db3f 100644 --- a/pkg/chart/loader/archive/archive.go +++ b/pkg/chart/loader/archive/archive.go @@ -29,6 +29,7 @@ import ( "path" "regexp" "strings" + "time" ) // MaxDecompressedChartSize is the maximum size of a chart archive that will be @@ -46,8 +47,9 @@ var utf8bom = []byte{0xEF, 0xBB, 0xBF} // BufferedFile represents an archive file buffered for later processing. type BufferedFile struct { - Name string - Data []byte + Name string + ModTime time.Time + Data []byte } // LoadArchiveFiles reads in files out of an archive into memory. This function @@ -148,7 +150,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { data := bytes.TrimPrefix(b.Bytes(), utf8bom) - files = append(files, &BufferedFile{Name: n, Data: data}) + files = append(files, &BufferedFile{Name: n, ModTime: hd.ModTime, Data: data}) b.Reset() } diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index f59bcd8b3..d77a53ddc 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "helm.sh/helm/v4/pkg/chart/common" ) @@ -50,9 +51,13 @@ type Chart struct { Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` parent *Chart dependencies []*Chart diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index a96d8c0c0..d0837eb16 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -18,6 +18,7 @@ package v2 import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" @@ -25,27 +26,33 @@ import ( ) func TestCRDs(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ Raw: []*common.File{ { - Name: "fhqwhgads.yaml", - Data: []byte("Everybody to the Limit"), + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), }, }, } @@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) { } func TestCRDObjects(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", File: &common.File{ - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", File: &common.File{ - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, } diff --git a/pkg/chart/v2/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go index 3e8e0b371..ec2f5ac82 100644 --- a/pkg/chart/v2/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) { } func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() mychart := chart.Chart{ Metadata: &chart.Metadata{ APIVersion: "v2", @@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/baddeployment.yaml", - Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), }, { - Name: "templates/goodsecret.yaml", - Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), }, }, } @@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/configmap.yaml", - Data: []byte(manifest), + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), }, }, } @@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/empty-with-comments.yaml", - Data: []byte("#@formatter:off\n"), + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), }, }, } diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index c6f31560c..82578d924 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &archive.BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 028d59e82..ba3a9b6bc 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -76,7 +76,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -90,6 +90,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { if c.Metadata.APIVersion == "" { c.Metadata.APIVersion = chart.APIVersionV1 } + c.ModTime = f.ModTime } } for _, f := range files { @@ -110,6 +111,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c.Values = values case f.Name == "values.schema.json": c.Schema = f.Data + c.SchemaModTime = f.ModTime // Deprecated: requirements.yaml is deprecated use Chart.yaml. // We will handle it for you because we are nice people @@ -124,7 +126,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { return c, fmt.Errorf("cannot load requirements.yaml: %w", err) } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } // Deprecated: requirements.lock is deprecated use Chart.lock. case f.Name == "requirements.lock": @@ -139,22 +141,22 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.") } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) continue } fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) default: - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } } diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 7eca5f402..ee0be5b18 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -219,8 +219,9 @@ func TestLoadFiles_BadCases(t *testing.T) { name: "These files contain only requirements.lock", bufferedFiles: []*archive.BufferedFile{ { - Name: "requirements.lock", - Data: []byte(""), + Name: "requirements.lock", + ModTime: time.Now(), + Data: []byte(""), }, }, expectError: "validation: chart.metadata.apiVersion is required"}, @@ -236,9 +237,11 @@ func TestLoadFiles_BadCases(t *testing.T) { } func TestLoadFiles(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v1 name: frobnitz description: This is a frobnitz. @@ -259,20 +262,24 @@ icon: https://example.com/64x64.png `), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "values.schema.json", - Data: []byte("type: Values"), + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, } @@ -312,26 +319,32 @@ icon: https://example.com/64x64.png // Test the order of file loading. The Chart.yaml file needs to come first for // later comparison checks. See https://github.com/helm/helm/pull/8948 func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "requirements.yaml", - Data: []byte("dependencies:"), + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v1 name: frobnitz description: This is a frobnitz. diff --git a/pkg/chart/v2/util/create.go b/pkg/chart/v2/util/create.go index d7c1fe31c..bf572c707 100644 --- a/pkg/chart/v2/util/create.go +++ b/pkg/chart/v2/util/create.go @@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) } schart.Templates = updatedTemplates diff --git a/pkg/chart/v2/util/save.go b/pkg/chart/v2/util/save.go index 69a98924c..632588b68 100644 --- a/pkg/chart/v2/util/save.go +++ b/pkg/chart/v2/util/save.go @@ -175,7 +175,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { return err } @@ -187,7 +187,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { return err } } @@ -196,7 +196,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save values.yaml for _, f := range c.Raw { if f.Name == ValuesfileName { - if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { return err } } @@ -207,7 +207,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if !json.Valid(c.Schema) { return errors.New("invalid JSON in " + SchemafileName) } - if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { return err } } @@ -215,7 +215,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save templates for _, f := range c.Templates { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -223,7 +223,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save files for _, f := range c.Files { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -238,13 +238,13 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { } // writeToTar writes a single file to a tar archive. -func writeToTar(out *tar.Writer, name string, body []byte) error { +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ Name: filepath.ToSlash(name), Mode: 0644, Size: int64(len(body)), - ModTime: time.Now(), + ModTime: modTime, } if err := out.WriteHeader(h); err != nil { return err diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index ef822a82a..5dc36e786 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -20,6 +20,8 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/sha256" + "fmt" "io" "os" "path" @@ -49,7 +51,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), } @@ -118,7 +120,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } _, err := Save(c, tmp) @@ -153,14 +155,16 @@ func TestSavePreservesTimestamps(t *testing.T) { Name: "ahab", Version: "1.2.3", }, + ModTime: initialCreateTime, Values: map[string]interface{}{ "imageName": "testimage", "imageId": 42, }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, } where, err := Save(c, tmp) @@ -173,8 +177,9 @@ func TestSavePreservesTimestamps(t *testing.T) { t.Fatalf("Failed to parse tar: %v", err) } + roundedTime := initialCreateTime.Round(time.Second) for _, header := range allHeaders { - if header.ModTime.Before(initialCreateTime) { + if !header.ModTime.Equal(roundedTime) { t.Fatalf("File timestamp not preserved: %v", header.ModTime) } } @@ -217,6 +222,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { func TestSaveDir(t *testing.T) { tmp := t.TempDir() + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -224,10 +230,10 @@ func TestSaveDir(t *testing.T) { Version: "1.2.3", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, }, Templates: []*common.File{ - {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -263,3 +269,90 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Did not get expected error for chart named %q", c.Name()) } } + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "5e14a06037e5d4cb277c7b21770639d4e1a337be9ae391460e50653bac5a80ed", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "6967787da46fbfcc563cad31240e564e14f2602e6f66302129a59a9669622a36", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 9b17f187d..019e249ae 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -23,6 +23,7 @@ import ( "reflect" "strings" "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -383,7 +384,7 @@ func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: time.Now(), Data: configmapData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { @@ -484,6 +485,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri if err != nil { t.Fatalf("Error loading template yaml %v", err) } + modTime := time.Now() cfile := &chart.Chart{ Metadata: &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -491,7 +493,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: modTime, Data: configmapData}, {Name: "templates/secret.yaml", ModTime: modTime, Data: secretData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 542ac2a9c..e541ef9d7 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -23,6 +23,7 @@ import ( "sync" "testing" "text/template" + "time" "github.com/stretchr/testify/assert" @@ -90,17 +91,18 @@ func TestFuncMap(t *testing.T) { } func TestRender(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ Name: "moby", Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, - {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, - {Name: "templates/test3", Data: []byte("{{.noValue}}")}, - {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, - {Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + {Name: "templates/test1", ModTime: modTime, Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", ModTime: modTime, Data: []byte("{{.Values.global.callme | lower }}")}, + {Name: "templates/test3", ModTime: modTime, Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", ModTime: modTime, Data: []byte("{{toJson .Values}}")}, + {Name: "templates/test5", ModTime: modTime, Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } @@ -140,14 +142,16 @@ func TestRender(t *testing.T) { } func TestRenderRefsOrdering(t *testing.T) { + modTime := time.Now() + parentChart := &chart.Chart{ Metadata: &chart.Metadata{ Name: "parent", Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, - {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, }, } childChart := &chart.Chart{ @@ -156,7 +160,7 @@ func TestRenderRefsOrdering(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, }, } parentChart.AddDependency(childChart) @@ -220,7 +224,7 @@ func TestRenderWithDNS(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + {Name: "templates/test1", ModTime: time.Now(), Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{}, } @@ -355,10 +359,12 @@ func TestRenderWithClientProvider(t *testing.T) { Values: map[string]interface{}{}, } + modTime := time.Now() for name, exp := range cases { c.Templates = append(c.Templates, &common.File{ - Name: path.Join("templates", name), - Data: []byte(exp.template), + Name: path.Join("templates", name), + ModTime: modTime, + Data: []byte(exp.template), }) } @@ -393,7 +399,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, + {Name: "templates/error", ModTime: time.Now(), Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, }, Values: map[string]interface{}{}, } @@ -558,18 +564,19 @@ func TestFailErrors(t *testing.T) { } func TestAllTemplates(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, Templates: []*common.File{ - {Name: "templates/foo", Data: []byte("foo")}, - {Name: "templates/bar", Data: []byte("bar")}, + {Name: "templates/foo", ModTime: modTime, Data: []byte("foo")}, + {Name: "templates/bar", ModTime: modTime, Data: []byte("bar")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "laboratory mice"}, Templates: []*common.File{ - {Name: "templates/pinky", Data: []byte("pinky")}, - {Name: "templates/brain", Data: []byte("brain")}, + {Name: "templates/pinky", ModTime: modTime, Data: []byte("pinky")}, + {Name: "templates/brain", ModTime: modTime, Data: []byte("brain")}, }, } ch1.AddDependency(dep1) @@ -577,7 +584,7 @@ func TestAllTemplates(t *testing.T) { dep2 := &chart.Chart{ Metadata: &chart.Metadata{Name: "same thing we do every night"}, Templates: []*common.File{ - {Name: "templates/innermost", Data: []byte("innermost")}, + {Name: "templates/innermost", ModTime: modTime, Data: []byte("innermost")}, }, } dep1.AddDependency(dep2) @@ -589,16 +596,17 @@ func TestAllTemplates(t *testing.T) { } func TestChartValuesContainsIsRoot(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "parent"}, Templates: []*common.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "child"}, Templates: []*common.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } ch1.AddDependency(dep1) @@ -621,16 +629,17 @@ func TestChartValuesContainsIsRoot(t *testing.T) { func TestRenderDependency(t *testing.T) { deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` + modTime := time.Now() ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, Templates: []*common.File{ - {Name: "templates/outer", Data: []byte(toptpl)}, + {Name: "templates/outer", ModTime: modTime, Data: []byte(toptpl)}, }, } ch.AddDependency(&chart.Chart{ Metadata: &chart.Metadata{Name: "innerchart"}, Templates: []*common.File{ - {Name: "templates/inner", Data: []byte(deptpl)}, + {Name: "templates/inner", ModTime: modTime, Data: []byte(deptpl)}, }, }) @@ -659,11 +668,12 @@ func TestRenderNestedValues(t *testing.T) { // Ensure subcharts scopes are working. subchartspath := "templates/subcharts.tpl" + modTime := time.Now() deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, Templates: []*common.File{ - {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, - {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, + {Name: deepestpath, ModTime: modTime, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, + {Name: checkrelease, ModTime: modTime, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, Values: map[string]interface{}{"what": "milkshake", "where": "here"}, } @@ -671,7 +681,7 @@ func TestRenderNestedValues(t *testing.T) { inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, Templates: []*common.File{ - {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, + {Name: innerpath, ModTime: modTime, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, } @@ -680,8 +690,8 @@ func TestRenderNestedValues(t *testing.T) { outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, Templates: []*common.File{ - {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, - {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, + {Name: outerpath, ModTime: modTime, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, + {Name: subchartspath, ModTime: modTime, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, Values: map[string]interface{}{ "what": "stinkweed", @@ -754,23 +764,24 @@ func TestRenderNestedValues(t *testing.T) { } func TestRenderBuiltinValues(t *testing.T) { + modTime := time.Now() inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, Templates: []*common.File{ - {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, + {Name: "templates/Lavinia", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/From", ModTime: modTime, Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, Files: []*common.File{ - {Name: "author", Data: []byte("Virgil")}, - {Name: "book/title.txt", Data: []byte("Aeneid")}, + {Name: "author", ModTime: modTime, Data: []byte("Virgil")}, + {Name: "book/title.txt", ModTime: modTime, Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, Templates: []*common.File{ - {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, + {Name: "templates/Aeneas", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/Amata", ModTime: modTime, Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) @@ -805,11 +816,12 @@ func TestRenderBuiltinValues(t *testing.T) { } func TestAlterFuncMap_include(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } @@ -817,8 +829,8 @@ func TestAlterFuncMap_include(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "nested"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } @@ -848,11 +860,12 @@ func TestAlterFuncMap_include(t *testing.T) { } func TestAlterFuncMap_require(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, - {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, + {Name: "templates/bases", ModTime: modTime, Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, } @@ -913,7 +926,7 @@ func TestAlterFuncMap_tpl(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, } @@ -942,7 +955,7 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, } @@ -968,11 +981,12 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { } func TestAlterFuncMap_tplinclude(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, - {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Template.Name}}`)}, }, } v := common.Values{ @@ -998,12 +1012,14 @@ func TestAlterFuncMap_tplinclude(t *testing.T) { } func TestRenderRecursionLimit(t *testing.T) { + modTime := time.Now() + // endless recursion should produce an error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "bad"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, - {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{include "recursion" . }}`)}, + {Name: "templates/recursion", ModTime: modTime, Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, }, } v := common.Values{ @@ -1032,8 +1048,8 @@ func TestRenderRecursionLimit(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(repeatedIncl.String())}, - {Name: "templates/_function", Data: []byte(printFunc)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(repeatedIncl.String())}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(printFunc)}, }, } @@ -1053,15 +1069,16 @@ func TestRenderRecursionLimit(t *testing.T) { } func TestRenderLoadTemplateForTplFromFile(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, - {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, }, Files: []*common.File{ - {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, - {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, + {Name: "test", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, + {Name: "test2", ModTime: modTime, Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, }, } @@ -1088,12 +1105,13 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) { } func TestRenderTplEmpty(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplEmpty"}, Templates: []*common.File{ - {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, - {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, - {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, + {Name: "templates/empty-string", ModTime: modTime, Data: []byte(`{{tpl "" .}}`)}, + {Name: "templates/empty-action", ModTime: modTime, Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, + {Name: "templates/only-defines", ModTime: modTime, Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, }, } v := common.Values{ @@ -1121,15 +1139,16 @@ func TestRenderTplEmpty(t *testing.T) { } func TestRenderTplTemplateNames(t *testing.T) { + modTime := time.Now() // .Template.BasePath and .Name make it through c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplTemplateNames"}, Templates: []*common.File{ - {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, - {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, - {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, - {Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, - {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, + {Name: "templates/default-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, + {Name: "templates/default-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, + {Name: "templates/modified-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, + {Name: "templates/modified-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, + {Name: "templates/modified-field", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, }, } v := common.Values{ @@ -1168,12 +1187,13 @@ func TestRenderTplTemplateNames(t *testing.T) { } func TestRenderTplRedefines(t *testing.T) { + modTime := time.Now() // Redefining a template inside 'tpl' does not affect the outer definition c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplRedefines"}, Templates: []*common.File{ - {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, - {Name: "templates/partial", Data: []byte( + {Name: "templates/_partials", ModTime: modTime, Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, + {Name: "templates/partial", ModTime: modTime, Data: []byte( `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, )}, {Name: "templates/manifest", Data: []byte( @@ -1238,7 +1258,7 @@ func TestRenderTplMissingKey(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKey"}, Templates: []*common.File{ - {Name: "templates/manifest", Data: []byte( + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, @@ -1271,7 +1291,7 @@ func TestRenderTplMissingKeyString(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, Templates: []*common.File{ - {Name: "templates/manifest", Data: []byte( + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, @@ -1300,16 +1320,17 @@ func TestRenderTplMissingKeyString(t *testing.T) { } func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, Templates: []*common.File{ - {Name: "templates/svc.yaml", Data: []byte( + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, - {Name: "templates/_helpers_1.tpl", Data: []byte( + {Name: "templates/_helpers_1.tpl", ModTime: modTime, Data: []byte( `{{- define "nested_helper.name" -}}{{- include "common.names.get_name" . -}}{{- end -}}`, )}, - {Name: "charts/common/templates/_helpers_2.tpl", Data: []byte( + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( `{{- define "common.names.get_name" -}}{{- .Values.nonexistant.key | trunc 63 | trimSuffix "-" -}}{{- end -}}`, )}, }, @@ -1338,16 +1359,17 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 } func TestMultilineNoTemplateAssociatedError(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "multiline"}, Templates: []*common.File{ - {Name: "templates/svc.yaml", Data: []byte( + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, - {Name: "templates/test.yaml", Data: []byte( + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte( `{{ toYaml .Values }}`, )}, - {Name: "charts/common/templates/_helpers_2.tpl", Data: []byte( + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( `{{ toYaml .Values }}`, )}, }, @@ -1371,17 +1393,21 @@ template: no template "nested_helper.name" associated with template "gotpl"` } func TestRenderCustomTemplateFuncs(t *testing.T) { + modTime := time.Now() + // Create a chart with two templates that use custom functions c := &chart.Chart{ Metadata: &chart.Metadata{Name: "CustomFunc"}, Templates: []*common.File{ { - Name: "templates/manifest", - Data: []byte(`{{exclaim .Values.message}}`), + Name: "templates/manifest", + ModTime: modTime, + Data: []byte(`{{exclaim .Values.message}}`), }, { - Name: "templates/override", - Data: []byte(`{{ upper .Values.message }}`), + Name: "templates/override", + ModTime: modTime, + Data: []byte(`{{ upper .Values.message }}`), }, }, } diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 818cd777e..e6634644e 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -100,7 +100,7 @@ func Mock(opts *MockReleaseOptions) *Release { }, }, Templates: []*common.File{ - {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, + {Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)}, }, } }