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 <matt.farina@suse.com>
pull/31323/head
Matt Farina 4 days ago
parent 93c02523bb
commit 7facf2984a

@ -19,6 +19,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
) )
@ -47,9 +48,13 @@ type Chart struct {
Values map[string]interface{} `json:"values"` Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values // Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"` Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
// Files are miscellaneous files in a chart archive, // Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc. // e.g. README, LICENSE, etc.
Files []*common.File `json:"files"` Files []*common.File `json:"files"`
// ModTime the chart metadata was last modified
ModTime time.Time `json:"modtime,omitzero"`
parent *Chart parent *Chart
dependencies []*Chart dependencies []*Chart

@ -18,6 +18,7 @@ package v3
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -25,27 +26,33 @@ import (
) )
func TestCRDs(t *testing.T) { func TestCRDs(t *testing.T) {
modTime := time.Now()
chrt := Chart{ chrt := Chart{
Files: []*common.File{ Files: []*common.File{
{ {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "bar.yaml", Name: "bar.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crdsfoo/bar/baz.yaml", Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/README.md", Name: "crds/README.md",
Data: []byte("# hello"), ModTime: modTime,
Data: []byte("# hello"),
}, },
}, },
} }
@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) {
chrt := Chart{ chrt := Chart{
Raw: []*common.File{ Raw: []*common.File{
{ {
Name: "fhqwhgads.yaml", Name: "fhqwhgads.yaml",
Data: []byte("Everybody to the Limit"), ModTime: time.Now(),
Data: []byte("Everybody to the Limit"),
}, },
}, },
} }
@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) {
} }
func TestCRDObjects(t *testing.T) { func TestCRDObjects(t *testing.T) {
modTime := time.Now()
chrt := Chart{ chrt := Chart{
Files: []*common.File{ Files: []*common.File{
{ {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "bar.yaml", Name: "bar.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crdsfoo/bar/baz.yaml", Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/README.md", Name: "crds/README.md",
Data: []byte("# hello"), ModTime: modTime,
Data: []byte("# hello"),
}, },
}, },
} }
@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Filename: "crds/foo.yaml", Filename: "crds/foo.yaml",
File: &common.File{ File: &common.File{
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Filename: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml",
File: &common.File{ File: &common.File{
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
}, },
} }

@ -22,6 +22,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
chart "helm.sh/helm/v4/internal/chart/v3" chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support" "helm.sh/helm/v4/internal/chart/v3/lint/support"
@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) {
} }
func TestDeprecatedAPIFails(t *testing.T) { func TestDeprecatedAPIFails(t *testing.T) {
modTime := time.Now()
mychart := chart.Chart{ mychart := chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
APIVersion: "v2", APIVersion: "v2",
@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/baddeployment.yaml", Name: "templates/baddeployment.yaml",
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), ModTime: modTime,
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
}, },
{ {
Name: "templates/goodsecret.yaml", Name: "templates/goodsecret.yaml",
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), ModTime: modTime,
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
}, },
}, },
} }
@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/configmap.yaml", Name: "templates/configmap.yaml",
Data: []byte(manifest), ModTime: time.Now(),
Data: []byte(manifest),
}, },
}, },
} }
@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/empty-with-comments.yaml", Name: "templates/empty-with-comments.yaml",
Data: []byte("#@formatter:off\n"), ModTime: time.Now(),
Data: []byte("#@formatter:off\n"),
}, },
}, },
} }

@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom) 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 return nil
} }
if err = sympath.Walk(topdir, walk); err != nil { if err = sympath.Walk(topdir, walk); err != nil {

@ -25,6 +25,7 @@ import (
"maps" "maps"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
utilyaml "k8s.io/apimachinery/pkg/util/yaml" 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) { func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart) c := new(chart.Chart)
subcharts := make(map[string][]*archive.BufferedFile) subcharts := make(map[string][]*archive.BufferedFile)
var subChartsKeys []string
// do not rely on assumed ordering of files in the chart and crash // do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata // if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files { 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 f.Name == "Chart.yaml" {
if c.Metadata == nil { if c.Metadata == nil {
c.Metadata = new(chart.Metadata) c.Metadata = new(chart.Metadata)
@ -89,6 +91,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == "" { if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV3 c.Metadata.APIVersion = chart.APIVersionV3
} }
c.ModTime = f.ModTime
} }
} }
for _, f := range files { for _, f := range files {
@ -109,20 +112,24 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Values = values c.Values = values
case f.Name == "values.schema.json": case f.Name == "values.schema.json":
c.Schema = f.Data c.Schema = f.Data
c.SchemaModTime = f.ModTime
case strings.HasPrefix(f.Name, "templates/"): 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/"): case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" { 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 continue
} }
fname := strings.TrimPrefix(f.Name, "charts/") fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0] 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: 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})
} }
} }

@ -184,9 +184,11 @@ func TestLoadFile(t *testing.T) {
} }
func TestLoadFiles(t *testing.T) { func TestLoadFiles(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{ goodFiles := []*archive.BufferedFile{
{ {
Name: "Chart.yaml", Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v3 Data: []byte(`apiVersion: v3
name: frobnitz name: frobnitz
description: This is a frobnitz. description: This is a frobnitz.
@ -207,20 +209,24 @@ icon: https://example.com/64x64.png
`), `),
}, },
{ {
Name: "values.yaml", Name: "values.yaml",
Data: []byte("var: some values"), ModTime: modTime,
Data: []byte("var: some values"),
}, },
{ {
Name: "values.schema.json", Name: "values.schema.json",
Data: []byte("type: Values"), ModTime: modTime,
Data: []byte("type: Values"),
}, },
{ {
Name: "templates/deployment.yaml", Name: "templates/deployment.yaml",
Data: []byte("some deployment"), ModTime: modTime,
Data: []byte("some deployment"),
}, },
{ {
Name: "templates/service.yaml", Name: "templates/service.yaml",
Data: []byte("some service"), 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 // 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 // later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) { func TestLoadFilesOrder(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{ goodFiles := []*archive.BufferedFile{
{ {
Name: "requirements.yaml", Name: "requirements.yaml",
Data: []byte("dependencies:"), ModTime: modTime,
Data: []byte("dependencies:"),
}, },
{ {
Name: "values.yaml", Name: "values.yaml",
Data: []byte("var: some values"), ModTime: modTime,
Data: []byte("var: some values"),
}, },
{ {
Name: "templates/deployment.yaml", Name: "templates/deployment.yaml",
Data: []byte("some deployment"), ModTime: modTime,
Data: []byte("some deployment"),
}, },
{ {
Name: "templates/service.yaml", Name: "templates/service.yaml",
Data: []byte("some service"), ModTime: modTime,
Data: []byte("some service"),
}, },
{ {
Name: "Chart.yaml", Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v3 Data: []byte(`apiVersion: v3
name: frobnitz name: frobnitz
description: This is a frobnitz. description: This is a frobnitz.

@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
for _, template := range schart.Templates { for _, template := range schart.Templates {
newData := transform(string(template.Data), schart.Name()) 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 schart.Templates = updatedTemplates

@ -166,7 +166,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil { if err != nil {
return err 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 return err
} }
@ -176,7 +176,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil { if err != nil {
return err 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 return err
} }
} }
@ -184,7 +184,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save values.yaml // Save values.yaml
for _, f := range c.Raw { for _, f := range c.Raw {
if f.Name == ValuesfileName { 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 return err
} }
} }
@ -195,7 +195,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if !json.Valid(c.Schema) { if !json.Valid(c.Schema) {
return errors.New("invalid JSON in " + SchemafileName) 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 return err
} }
} }
@ -203,7 +203,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save templates // Save templates
for _, f := range c.Templates { for _, f := range c.Templates {
n := filepath.Join(base, f.Name) 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 return err
} }
} }
@ -211,7 +211,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save files // Save files
for _, f := range c.Files { for _, f := range c.Files {
n := filepath.Join(base, f.Name) 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 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. // 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? // TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{ h := &tar.Header{
Name: filepath.ToSlash(name), Name: filepath.ToSlash(name),
Mode: 0644, Mode: 0644,
Size: int64(len(body)), Size: int64(len(body)),
ModTime: time.Now(), ModTime: modTime,
} }
if err := out.WriteHeader(h); err != nil { if err := out.WriteHeader(h); err != nil {
return err return err

@ -20,6 +20,8 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256"
"fmt"
"io" "io"
"os" "os"
"path" "path"
@ -49,7 +51,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest", Digest: "testdigest",
}, },
Files: []*common.File{ 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}"), Schema: []byte("{\n \"title\": \"Values\"\n}"),
} }
@ -115,7 +117,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest", Digest: "testdigest",
}, },
Files: []*common.File{ 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) _, err := Save(c, tmp)
@ -141,7 +143,6 @@ func TestSavePreservesTimestamps(t *testing.T) {
// check will fail because `initialCreateTime` will be identical to the // check will fail because `initialCreateTime` will be identical to the
// written timestamp for the files. // written timestamp for the files.
initialCreateTime := time.Now().Add(-1 * time.Second) initialCreateTime := time.Now().Add(-1 * time.Second)
tmp := t.TempDir() tmp := t.TempDir()
c := &chart.Chart{ c := &chart.Chart{
@ -150,14 +151,16 @@ func TestSavePreservesTimestamps(t *testing.T) {
Name: "ahab", Name: "ahab",
Version: "1.2.3", Version: "1.2.3",
}, },
ModTime: initialCreateTime,
Values: map[string]interface{}{ Values: map[string]interface{}{
"imageName": "testimage", "imageName": "testimage",
"imageId": 42, "imageId": 42,
}, },
Files: []*common.File{ 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) where, err := Save(c, tmp)
@ -170,8 +173,9 @@ func TestSavePreservesTimestamps(t *testing.T) {
t.Fatalf("Failed to parse tar: %v", err) t.Fatalf("Failed to parse tar: %v", err)
} }
roundedTime := initialCreateTime.Round(time.Second)
for _, header := range allHeaders { for _, header := range allHeaders {
if header.ModTime.Before(initialCreateTime) { if !header.ModTime.Equal(roundedTime) {
t.Fatalf("File timestamp not preserved: %v", header.ModTime) 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) { func TestSaveDir(t *testing.T) {
tmp := t.TempDir() tmp := t.TempDir()
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -221,10 +226,10 @@ func TestSaveDir(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Files: []*common.File{ 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{ 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()) 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
}

@ -122,9 +122,10 @@ type chartOptions struct {
type chartOption func(*chartOptions) type chartOption func(*chartOptions)
func buildChart(opts ...chartOption) *chart.Chart { func buildChart(opts ...chartOption) *chart.Chart {
modTime := time.Now()
defaultTemplates := []*common.File{ defaultTemplates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)}, {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifestWithHook)},
} }
return buildChartWithTemplates(defaultTemplates, opts...) return buildChartWithTemplates(defaultTemplates, opts...)
} }
@ -180,8 +181,9 @@ func withValues(values map[string]interface{}) chartOption {
func withNotes(notes string) chartOption { func withNotes(notes string) chartOption {
return func(opts *chartOptions) { return func(opts *chartOptions) {
opts.Templates = append(opts.Templates, &common.File{ opts.Templates = append(opts.Templates, &common.File{
Name: "templates/NOTES.txt", Name: "templates/NOTES.txt",
Data: []byte(notes), ModTime: time.Now(),
Data: []byte(notes),
}) })
} }
} }
@ -200,12 +202,13 @@ func withMetadataDependency(dependency chart.Dependency) chartOption {
func withSampleTemplates() chartOption { func withSampleTemplates() chartOption {
return func(opts *chartOptions) { return func(opts *chartOptions) {
modTime := time.Now()
sampleTemplates := []*common.File{ sampleTemplates := []*common.File{
// This adds basic templates and partials. // This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")}, {Name: "templates/empty", ModTime: modTime, Data: []byte("")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
} }
opts.Templates = append(opts.Templates, sampleTemplates...) opts.Templates = append(opts.Templates, sampleTemplates...)
} }
@ -213,20 +216,21 @@ func withSampleTemplates() chartOption {
func withSampleSecret() chartOption { func withSampleSecret() chartOption {
return func(opts *chartOptions) { 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) opts.Templates = append(opts.Templates, sampleSecret)
} }
} }
func withSampleIncludingIncorrectTemplates() chartOption { func withSampleIncludingIncorrectTemplates() chartOption {
return func(opts *chartOptions) { return func(opts *chartOptions) {
modTime := time.Now()
sampleTemplates := []*common.File{ sampleTemplates := []*common.File{
// This adds basic templates and partials. // This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")}, {Name: "templates/empty", ModTime: modTime, Data: []byte("")},
{Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, {Name: "templates/incorrect", ModTime: modTime, Data: []byte("{{ .Values.bad.doh }}")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
} }
opts.Templates = append(opts.Templates, sampleTemplates...) opts.Templates = append(opts.Templates, sampleTemplates...)
} }
@ -235,7 +239,7 @@ func withSampleIncludingIncorrectTemplates() chartOption {
func withMultipleManifestTemplate() chartOption { func withMultipleManifestTemplate() chartOption {
return func(opts *chartOptions) { return func(opts *chartOptions) {
sampleTemplates := []*common.File{ sampleTemplates := []*common.File{
{Name: "templates/rbac", Data: []byte(rbacManifests)}, {Name: "templates/rbac", ModTime: time.Now(), Data: []byte(rbacManifests)},
} }
opts.Templates = append(opts.Templates, sampleTemplates...) opts.Templates = append(opts.Templates, sampleTemplates...)
} }
@ -852,7 +856,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) {
Version: "0.1.0", Version: "0.1.0",
}, },
Templates: []*common.File{ 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{}{} values := map[string]interface{}{}

@ -178,9 +178,10 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
outBuffer := &bytes.Buffer{} outBuffer := &bytes.Buffer{}
instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
modTime := time.Now()
templates := []*common.File{ templates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)}, {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)},
} }
vals := map[string]interface{}{} vals := map[string]interface{}{}
@ -205,9 +206,10 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str
outBuffer := &bytes.Buffer{} outBuffer := &bytes.Buffer{}
failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
modTime := time.Now()
templates := []*common.File{ templates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)}, {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)},
} }
vals := map[string]interface{}{} vals := map[string]interface{}{}

@ -427,8 +427,9 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
mockChart := buildChart(withSampleTemplates()) mockChart := buildChart(withSampleTemplates())
mockChart.Templates = append(mockChart.Templates, &common.File{ mockChart.Templates = append(mockChart.Templates, &common.File{
Name: "templates/lookup", Name: "templates/lookup",
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), ModTime: time.Now(),
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
}) })
res, err := instAction.Run(mockChart, vals) res, err := instAction.Run(mockChart, vals)

@ -18,6 +18,7 @@ package action
import ( import (
"testing" "testing"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
@ -26,17 +27,18 @@ import (
func TestShow(t *testing.T) { func TestShow(t *testing.T) {
config := actionConfigFixture(t) config := actionConfigFixture(t)
client := NewShow(ShowAll, config) client := NewShow(ShowAll, config)
modTime := time.Now()
client.chart = &chart.Chart{ client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"}, Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{ Files: []*common.File{
{Name: "README.md", Data: []byte("README\n")}, {Name: "README.md", ModTime: modTime, Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")}, {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")},
}, },
Raw: []*common.File{ Raw: []*common.File{
{Name: "values.yaml", Data: []byte("VALUES\n")}, {Name: "values.yaml", ModTime: modTime, Data: []byte("VALUES\n")},
}, },
Values: map[string]interface{}{}, Values: map[string]interface{}{},
} }
@ -104,13 +106,14 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
func TestShowCRDs(t *testing.T) { func TestShowCRDs(t *testing.T) {
config := actionConfigFixture(t) config := actionConfigFixture(t)
client := NewShow(ShowCRDs, config) client := NewShow(ShowCRDs, config)
modTime := time.Now()
client.chart = &chart.Chart{ client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"}, Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{ Files: []*common.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")}, {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")},
}, },
} }
@ -137,12 +140,13 @@ baz
func TestShowNoReadme(t *testing.T) { func TestShowNoReadme(t *testing.T) {
config := actionConfigFixture(t) config := actionConfigFixture(t)
client := NewShow(ShowAll, config) client := NewShow(ShowAll, config)
modTime := time.Now()
client.chart = &chart.Chart{ client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"}, Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{ Files: []*common.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
}, },
} }

@ -15,6 +15,8 @@ limitations under the License.
package common package common
import "time"
// File represents a file as a name/value pair. // File represents a file as a name/value pair.
// //
// By convention, name is a relative path within the scope of the chart's // 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"` Name string `json:"name"`
// Data is the template as byte data. // Data is the template as byte data.
Data []byte `json:"data"` Data []byte `json:"data"`
// ModTime is the file's mod-time
ModTime time.Time `json:"modtime,omitzero"`
} }

@ -18,6 +18,7 @@ package util
import ( import (
"testing" "testing"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
@ -46,7 +47,7 @@ func TestToRenderValues(t *testing.T) {
Templates: []*common.File{}, Templates: []*common.File{},
Values: chartValues, Values: chartValues,
Files: []*common.File{ 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{ c.AddDependency(&chart.Chart{

@ -29,6 +29,7 @@ import (
"path" "path"
"regexp" "regexp"
"strings" "strings"
"time"
) )
// MaxDecompressedChartSize is the maximum size of a chart archive that will be // 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. // BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct { type BufferedFile struct {
Name string Name string
Data []byte ModTime time.Time
Data []byte
} }
// LoadArchiveFiles reads in files out of an archive into memory. This function // 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) 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() b.Reset()
} }

@ -19,6 +19,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
) )
@ -50,9 +51,13 @@ type Chart struct {
Values map[string]interface{} `json:"values"` Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values // Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"` Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
// Files are miscellaneous files in a chart archive, // Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc. // e.g. README, LICENSE, etc.
Files []*common.File `json:"files"` Files []*common.File `json:"files"`
// ModTime the chart metadata was last modified
ModTime time.Time `json:"modtime,omitzero"`
parent *Chart parent *Chart
dependencies []*Chart dependencies []*Chart

@ -18,6 +18,7 @@ package v2
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -25,27 +26,33 @@ import (
) )
func TestCRDs(t *testing.T) { func TestCRDs(t *testing.T) {
modTime := time.Now()
chrt := Chart{ chrt := Chart{
Files: []*common.File{ Files: []*common.File{
{ {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "bar.yaml", Name: "bar.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crdsfoo/bar/baz.yaml", Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/README.md", Name: "crds/README.md",
Data: []byte("# hello"), ModTime: modTime,
Data: []byte("# hello"),
}, },
}, },
} }
@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) {
chrt := Chart{ chrt := Chart{
Raw: []*common.File{ Raw: []*common.File{
{ {
Name: "fhqwhgads.yaml", Name: "fhqwhgads.yaml",
Data: []byte("Everybody to the Limit"), ModTime: time.Now(),
Data: []byte("Everybody to the Limit"),
}, },
}, },
} }
@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) {
} }
func TestCRDObjects(t *testing.T) { func TestCRDObjects(t *testing.T) {
modTime := time.Now()
chrt := Chart{ chrt := Chart{
Files: []*common.File{ Files: []*common.File{
{ {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "bar.yaml", Name: "bar.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crdsfoo/bar/baz.yaml", Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
{ {
Name: "crds/README.md", Name: "crds/README.md",
Data: []byte("# hello"), ModTime: modTime,
Data: []byte("# hello"),
}, },
}, },
} }
@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) {
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Filename: "crds/foo.yaml", Filename: "crds/foo.yaml",
File: &common.File{ File: &common.File{
Name: "crds/foo.yaml", Name: "crds/foo.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
}, },
{ {
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Filename: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml",
File: &common.File{ File: &common.File{
Name: "crds/foo/bar/baz.yaml", Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"), ModTime: modTime,
Data: []byte("hello"),
}, },
}, },
} }

@ -22,6 +22,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) {
} }
func TestDeprecatedAPIFails(t *testing.T) { func TestDeprecatedAPIFails(t *testing.T) {
modTime := time.Now()
mychart := chart.Chart{ mychart := chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
APIVersion: "v2", APIVersion: "v2",
@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/baddeployment.yaml", Name: "templates/baddeployment.yaml",
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), ModTime: modTime,
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
}, },
{ {
Name: "templates/goodsecret.yaml", Name: "templates/goodsecret.yaml",
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), ModTime: modTime,
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
}, },
}, },
} }
@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/configmap.yaml", Name: "templates/configmap.yaml",
Data: []byte(manifest), ModTime: time.Now(),
Data: []byte(manifest),
}, },
}, },
} }
@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
}, },
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/empty-with-comments.yaml", Name: "templates/empty-with-comments.yaml",
Data: []byte("#@formatter:off\n"), ModTime: time.Now(),
Data: []byte("#@formatter:off\n"),
}, },
}, },
} }

@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom) 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 return nil
} }
if err = sympath.Walk(topdir, walk); err != nil { if err = sympath.Walk(topdir, walk); err != nil {

@ -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 // do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata // if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files { 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 f.Name == "Chart.yaml" {
if c.Metadata == nil { if c.Metadata == nil {
c.Metadata = new(chart.Metadata) c.Metadata = new(chart.Metadata)
@ -90,6 +90,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == "" { if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV1 c.Metadata.APIVersion = chart.APIVersionV1
} }
c.ModTime = f.ModTime
} }
} }
for _, f := range files { for _, f := range files {
@ -110,6 +111,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Values = values c.Values = values
case f.Name == "values.schema.json": case f.Name == "values.schema.json":
c.Schema = f.Data c.Schema = f.Data
c.SchemaModTime = f.ModTime
// Deprecated: requirements.yaml is deprecated use Chart.yaml. // Deprecated: requirements.yaml is deprecated use Chart.yaml.
// We will handle it for you because we are nice people // 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) return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
} }
if c.Metadata.APIVersion == chart.APIVersionV1 { 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. // Deprecated: requirements.lock is deprecated use Chart.lock.
case f.Name == "requirements.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.") 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 { 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/"): 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/"): case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" { 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 continue
} }
fname := strings.TrimPrefix(f.Name, "charts/") fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0] 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: 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})
} }
} }

@ -219,8 +219,9 @@ func TestLoadFiles_BadCases(t *testing.T) {
name: "These files contain only requirements.lock", name: "These files contain only requirements.lock",
bufferedFiles: []*archive.BufferedFile{ bufferedFiles: []*archive.BufferedFile{
{ {
Name: "requirements.lock", Name: "requirements.lock",
Data: []byte(""), ModTime: time.Now(),
Data: []byte(""),
}, },
}, },
expectError: "validation: chart.metadata.apiVersion is required"}, expectError: "validation: chart.metadata.apiVersion is required"},
@ -236,9 +237,11 @@ func TestLoadFiles_BadCases(t *testing.T) {
} }
func TestLoadFiles(t *testing.T) { func TestLoadFiles(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{ goodFiles := []*archive.BufferedFile{
{ {
Name: "Chart.yaml", Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v1 Data: []byte(`apiVersion: v1
name: frobnitz name: frobnitz
description: This is a frobnitz. description: This is a frobnitz.
@ -259,20 +262,24 @@ icon: https://example.com/64x64.png
`), `),
}, },
{ {
Name: "values.yaml", Name: "values.yaml",
Data: []byte("var: some values"), ModTime: modTime,
Data: []byte("var: some values"),
}, },
{ {
Name: "values.schema.json", Name: "values.schema.json",
Data: []byte("type: Values"), ModTime: modTime,
Data: []byte("type: Values"),
}, },
{ {
Name: "templates/deployment.yaml", Name: "templates/deployment.yaml",
Data: []byte("some deployment"), ModTime: modTime,
Data: []byte("some deployment"),
}, },
{ {
Name: "templates/service.yaml", Name: "templates/service.yaml",
Data: []byte("some service"), 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 // 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 // later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) { func TestLoadFilesOrder(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{ goodFiles := []*archive.BufferedFile{
{ {
Name: "requirements.yaml", Name: "requirements.yaml",
Data: []byte("dependencies:"), ModTime: modTime,
Data: []byte("dependencies:"),
}, },
{ {
Name: "values.yaml", Name: "values.yaml",
Data: []byte("var: some values"), ModTime: modTime,
Data: []byte("var: some values"),
}, },
{ {
Name: "templates/deployment.yaml", Name: "templates/deployment.yaml",
Data: []byte("some deployment"), ModTime: modTime,
Data: []byte("some deployment"),
}, },
{ {
Name: "templates/service.yaml", Name: "templates/service.yaml",
Data: []byte("some service"), ModTime: modTime,
Data: []byte("some service"),
}, },
{ {
Name: "Chart.yaml", Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v1 Data: []byte(`apiVersion: v1
name: frobnitz name: frobnitz
description: This is a frobnitz. description: This is a frobnitz.

@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
for _, template := range schart.Templates { for _, template := range schart.Templates {
newData := transform(string(template.Data), schart.Name()) 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 schart.Templates = updatedTemplates

@ -175,7 +175,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil { if err != nil {
return err 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 return err
} }
@ -187,7 +187,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil { if err != nil {
return err 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 return err
} }
} }
@ -196,7 +196,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save values.yaml // Save values.yaml
for _, f := range c.Raw { for _, f := range c.Raw {
if f.Name == ValuesfileName { 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 return err
} }
} }
@ -207,7 +207,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if !json.Valid(c.Schema) { if !json.Valid(c.Schema) {
return errors.New("invalid JSON in " + SchemafileName) 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 return err
} }
} }
@ -215,7 +215,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save templates // Save templates
for _, f := range c.Templates { for _, f := range c.Templates {
n := filepath.Join(base, f.Name) 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 return err
} }
} }
@ -223,7 +223,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save files // Save files
for _, f := range c.Files { for _, f := range c.Files {
n := filepath.Join(base, f.Name) 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 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. // 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? // TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{ h := &tar.Header{
Name: filepath.ToSlash(name), Name: filepath.ToSlash(name),
Mode: 0644, Mode: 0644,
Size: int64(len(body)), Size: int64(len(body)),
ModTime: time.Now(), ModTime: modTime,
} }
if err := out.WriteHeader(h); err != nil { if err := out.WriteHeader(h); err != nil {
return err return err

@ -20,6 +20,8 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256"
"fmt"
"io" "io"
"os" "os"
"path" "path"
@ -49,7 +51,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest", Digest: "testdigest",
}, },
Files: []*common.File{ 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}"), Schema: []byte("{\n \"title\": \"Values\"\n}"),
} }
@ -118,7 +120,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest", Digest: "testdigest",
}, },
Files: []*common.File{ 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) _, err := Save(c, tmp)
@ -153,14 +155,16 @@ func TestSavePreservesTimestamps(t *testing.T) {
Name: "ahab", Name: "ahab",
Version: "1.2.3", Version: "1.2.3",
}, },
ModTime: initialCreateTime,
Values: map[string]interface{}{ Values: map[string]interface{}{
"imageName": "testimage", "imageName": "testimage",
"imageId": 42, "imageId": 42,
}, },
Files: []*common.File{ 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) where, err := Save(c, tmp)
@ -173,8 +177,9 @@ func TestSavePreservesTimestamps(t *testing.T) {
t.Fatalf("Failed to parse tar: %v", err) t.Fatalf("Failed to parse tar: %v", err)
} }
roundedTime := initialCreateTime.Round(time.Second)
for _, header := range allHeaders { for _, header := range allHeaders {
if header.ModTime.Before(initialCreateTime) { if !header.ModTime.Equal(roundedTime) {
t.Fatalf("File timestamp not preserved: %v", header.ModTime) 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) { func TestSaveDir(t *testing.T) {
tmp := t.TempDir() tmp := t.TempDir()
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1, APIVersion: chart.APIVersionV1,
@ -224,10 +230,10 @@ func TestSaveDir(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Files: []*common.File{ 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{ 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()) 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
}

@ -23,6 +23,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time"
"helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2" 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", Description: "A Helm chart for Kubernetes",
Version: "0.1.0", 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) chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
if err := chartutil.SaveDir(cfile, tmpChart); err != nil { 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 { if err != nil {
t.Fatalf("Error loading template yaml %v", err) t.Fatalf("Error loading template yaml %v", err)
} }
modTime := time.Now()
cfile := &chart.Chart{ cfile := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1, APIVersion: chart.APIVersionV1,
@ -491,7 +493,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri
Description: "A Helm chart for Kubernetes", Description: "A Helm chart for Kubernetes",
Version: "0.1.0", 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) chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
if err := chartutil.SaveDir(cfile, tmpChart); err != nil { if err := chartutil.SaveDir(cfile, tmpChart); err != nil {

@ -23,6 +23,7 @@ import (
"sync" "sync"
"testing" "testing"
"text/template" "text/template"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -90,17 +91,18 @@ func TestFuncMap(t *testing.T) {
} }
func TestRender(t *testing.T) { func TestRender(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "moby", Name: "moby",
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, {Name: "templates/test1", ModTime: modTime, Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")},
{Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test2", ModTime: modTime, Data: []byte("{{.Values.global.callme | lower }}")},
{Name: "templates/test3", Data: []byte("{{.noValue}}")}, {Name: "templates/test3", ModTime: modTime, Data: []byte("{{.noValue}}")},
{Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, {Name: "templates/test4", ModTime: modTime, Data: []byte("{{toJson .Values}}")},
{Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, {Name: "templates/test5", ModTime: modTime, Data: []byte("{{getHostByName \"helm.sh\"}}")},
}, },
Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"},
} }
@ -140,14 +142,16 @@ func TestRender(t *testing.T) {
} }
func TestRenderRefsOrdering(t *testing.T) { func TestRenderRefsOrdering(t *testing.T) {
modTime := time.Now()
parentChart := &chart.Chart{ parentChart := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "parent", Name: "parent",
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)},
{Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, {Name: "templates/test.yaml", ModTime: modTime, Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)},
}, },
} }
childChart := &chart.Chart{ childChart := &chart.Chart{
@ -156,7 +160,7 @@ func TestRenderRefsOrdering(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*common.File{ 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) parentChart.AddDependency(childChart)
@ -220,7 +224,7 @@ func TestRenderWithDNS(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*common.File{ 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{}{}, Values: map[string]interface{}{},
} }
@ -355,10 +359,12 @@ func TestRenderWithClientProvider(t *testing.T) {
Values: map[string]interface{}{}, Values: map[string]interface{}{},
} }
modTime := time.Now()
for name, exp := range cases { for name, exp := range cases {
c.Templates = append(c.Templates, &common.File{ c.Templates = append(c.Templates, &common.File{
Name: path.Join("templates", name), Name: path.Join("templates", name),
Data: []byte(exp.template), ModTime: modTime,
Data: []byte(exp.template),
}) })
} }
@ -393,7 +399,7 @@ func TestRenderWithClientProvider_error(t *testing.T) {
Version: "1.2.3", Version: "1.2.3",
}, },
Templates: []*common.File{ 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{}{}, Values: map[string]interface{}{},
} }
@ -558,18 +564,19 @@ func TestFailErrors(t *testing.T) {
} }
func TestAllTemplates(t *testing.T) { func TestAllTemplates(t *testing.T) {
modTime := time.Now()
ch1 := &chart.Chart{ ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "ch1"}, Metadata: &chart.Metadata{Name: "ch1"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/foo", Data: []byte("foo")}, {Name: "templates/foo", ModTime: modTime, Data: []byte("foo")},
{Name: "templates/bar", Data: []byte("bar")}, {Name: "templates/bar", ModTime: modTime, Data: []byte("bar")},
}, },
} }
dep1 := &chart.Chart{ dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "laboratory mice"}, Metadata: &chart.Metadata{Name: "laboratory mice"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/pinky", Data: []byte("pinky")}, {Name: "templates/pinky", ModTime: modTime, Data: []byte("pinky")},
{Name: "templates/brain", Data: []byte("brain")}, {Name: "templates/brain", ModTime: modTime, Data: []byte("brain")},
}, },
} }
ch1.AddDependency(dep1) ch1.AddDependency(dep1)
@ -577,7 +584,7 @@ func TestAllTemplates(t *testing.T) {
dep2 := &chart.Chart{ dep2 := &chart.Chart{
Metadata: &chart.Metadata{Name: "same thing we do every night"}, Metadata: &chart.Metadata{Name: "same thing we do every night"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/innermost", Data: []byte("innermost")}, {Name: "templates/innermost", ModTime: modTime, Data: []byte("innermost")},
}, },
} }
dep1.AddDependency(dep2) dep1.AddDependency(dep2)
@ -589,16 +596,17 @@ func TestAllTemplates(t *testing.T) {
} }
func TestChartValuesContainsIsRoot(t *testing.T) { func TestChartValuesContainsIsRoot(t *testing.T) {
modTime := time.Now()
ch1 := &chart.Chart{ ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"}, Metadata: &chart.Metadata{Name: "parent"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")},
}, },
} }
dep1 := &chart.Chart{ dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"}, Metadata: &chart.Metadata{Name: "child"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")},
}, },
} }
ch1.AddDependency(dep1) ch1.AddDependency(dep1)
@ -621,16 +629,17 @@ func TestChartValuesContainsIsRoot(t *testing.T) {
func TestRenderDependency(t *testing.T) { func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}` deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}` toptpl := `Hello {{template "myblock"}}`
modTime := time.Now()
ch := &chart.Chart{ ch := &chart.Chart{
Metadata: &chart.Metadata{Name: "outerchart"}, Metadata: &chart.Metadata{Name: "outerchart"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/outer", Data: []byte(toptpl)}, {Name: "templates/outer", ModTime: modTime, Data: []byte(toptpl)},
}, },
} }
ch.AddDependency(&chart.Chart{ ch.AddDependency(&chart.Chart{
Metadata: &chart.Metadata{Name: "innerchart"}, Metadata: &chart.Metadata{Name: "innerchart"},
Templates: []*common.File{ 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. // Ensure subcharts scopes are working.
subchartspath := "templates/subcharts.tpl" subchartspath := "templates/subcharts.tpl"
modTime := time.Now()
deepest := &chart.Chart{ deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"}, Metadata: &chart.Metadata{Name: "deepest"},
Templates: []*common.File{ Templates: []*common.File{
{Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, {Name: deepestpath, ModTime: modTime, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)},
{Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, {Name: checkrelease, ModTime: modTime, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)},
}, },
Values: map[string]interface{}{"what": "milkshake", "where": "here"}, Values: map[string]interface{}{"what": "milkshake", "where": "here"},
} }
@ -671,7 +681,7 @@ func TestRenderNestedValues(t *testing.T) {
inner := &chart.Chart{ inner := &chart.Chart{
Metadata: &chart.Metadata{Name: "herrick"}, Metadata: &chart.Metadata{Name: "herrick"},
Templates: []*common.File{ 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"}, Values: map[string]interface{}{"who": "Robert", "what": "glasses"},
} }
@ -680,8 +690,8 @@ func TestRenderNestedValues(t *testing.T) {
outer := &chart.Chart{ outer := &chart.Chart{
Metadata: &chart.Metadata{Name: "top"}, Metadata: &chart.Metadata{Name: "top"},
Templates: []*common.File{ Templates: []*common.File{
{Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, {Name: outerpath, ModTime: modTime, 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: subchartspath, ModTime: modTime, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)},
}, },
Values: map[string]interface{}{ Values: map[string]interface{}{
"what": "stinkweed", "what": "stinkweed",
@ -754,23 +764,24 @@ func TestRenderNestedValues(t *testing.T) {
} }
func TestRenderBuiltinValues(t *testing.T) { func TestRenderBuiltinValues(t *testing.T) {
modTime := time.Now()
inner := &chart.Chart{ inner := &chart.Chart{
Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/Lavinia", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, {Name: "templates/From", ModTime: modTime, Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)},
}, },
Files: []*common.File{ Files: []*common.File{
{Name: "author", Data: []byte("Virgil")}, {Name: "author", ModTime: modTime, Data: []byte("Virgil")},
{Name: "book/title.txt", Data: []byte("Aeneid")}, {Name: "book/title.txt", ModTime: modTime, Data: []byte("Aeneid")},
}, },
} }
outer := &chart.Chart{ outer := &chart.Chart{
Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/Aeneas", ModTime: modTime, 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/Amata", ModTime: modTime, Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)},
}, },
} }
outer.AddDependency(inner) outer.AddDependency(inner)
@ -805,11 +816,12 @@ func TestRenderBuiltinValues(t *testing.T) {
} }
func TestAlterFuncMap_include(t *testing.T) { func TestAlterFuncMap_include(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "conrad"}, Metadata: &chart.Metadata{Name: "conrad"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)},
{Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)},
}, },
} }
@ -817,8 +829,8 @@ func TestAlterFuncMap_include(t *testing.T) {
d := &chart.Chart{ d := &chart.Chart{
Metadata: &chart.Metadata{Name: "nested"}, Metadata: &chart.Metadata{Name: "nested"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)},
{Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, {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) { func TestAlterFuncMap_require(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "conan"}, Metadata: &chart.Metadata{Name: "conan"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, {Name: "templates/quote", ModTime: modTime, 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/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{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"}, Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{ 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{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"}, Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{ 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) { func TestAlterFuncMap_tplinclude(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"}, Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)},
{Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Template.Name}}`)},
}, },
} }
v := common.Values{ v := common.Values{
@ -998,12 +1012,14 @@ func TestAlterFuncMap_tplinclude(t *testing.T) {
} }
func TestRenderRecursionLimit(t *testing.T) { func TestRenderRecursionLimit(t *testing.T) {
modTime := time.Now()
// endless recursion should produce an error // endless recursion should produce an error
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "bad"}, Metadata: &chart.Metadata{Name: "bad"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, {Name: "templates/base", ModTime: modTime, Data: []byte(`{{include "recursion" . }}`)},
{Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, {Name: "templates/recursion", ModTime: modTime, Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)},
}, },
} }
v := common.Values{ v := common.Values{
@ -1032,8 +1048,8 @@ func TestRenderRecursionLimit(t *testing.T) {
d := &chart.Chart{ d := &chart.Chart{
Metadata: &chart.Metadata{Name: "overlook"}, Metadata: &chart.Metadata{Name: "overlook"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/quote", Data: []byte(repeatedIncl.String())}, {Name: "templates/quote", ModTime: modTime, Data: []byte(repeatedIncl.String())},
{Name: "templates/_function", Data: []byte(printFunc)}, {Name: "templates/_function", ModTime: modTime, Data: []byte(printFunc)},
}, },
} }
@ -1053,15 +1069,16 @@ func TestRenderRecursionLimit(t *testing.T) {
} }
func TestRenderLoadTemplateForTplFromFile(t *testing.T) { func TestRenderLoadTemplateForTplFromFile(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, Metadata: &chart.Metadata{Name: "TplLoadFromFile"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)},
{Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, {Name: "templates/_function", ModTime: modTime, Data: []byte(`{{define "test-function"}}test-function{{end}}`)},
}, },
Files: []*common.File{ Files: []*common.File{
{Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, {Name: "test", ModTime: modTime, 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: "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) { func TestRenderTplEmpty(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplEmpty"}, Metadata: &chart.Metadata{Name: "TplEmpty"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, {Name: "templates/empty-string", ModTime: modTime, Data: []byte(`{{tpl "" .}}`)},
{Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, {Name: "templates/empty-action", ModTime: modTime, Data: []byte(`{{tpl "{{ \"\"}}" .}}`)},
{Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, {Name: "templates/only-defines", ModTime: modTime, Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)},
}, },
} }
v := common.Values{ v := common.Values{
@ -1121,15 +1139,16 @@ func TestRenderTplEmpty(t *testing.T) {
} }
func TestRenderTplTemplateNames(t *testing.T) { func TestRenderTplTemplateNames(t *testing.T) {
modTime := time.Now()
// .Template.BasePath and .Name make it through // .Template.BasePath and .Name make it through
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplTemplateNames"}, Metadata: &chart.Metadata{Name: "TplTemplateNames"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, {Name: "templates/default-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)},
{Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, {Name: "templates/default-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)},
{Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, {Name: "templates/modified-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)},
{Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, {Name: "templates/modified-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)},
{Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, {Name: "templates/modified-field", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)},
}, },
} }
v := common.Values{ v := common.Values{
@ -1168,12 +1187,13 @@ func TestRenderTplTemplateNames(t *testing.T) {
} }
func TestRenderTplRedefines(t *testing.T) { func TestRenderTplRedefines(t *testing.T) {
modTime := time.Now()
// Redefining a template inside 'tpl' does not affect the outer definition // Redefining a template inside 'tpl' does not affect the outer definition
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplRedefines"}, Metadata: &chart.Metadata{Name: "TplRedefines"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, {Name: "templates/_partials", ModTime: modTime, Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)},
{Name: "templates/partial", Data: []byte( {Name: "templates/partial", ModTime: modTime, Data: []byte(
`before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`,
)}, )},
{Name: "templates/manifest", Data: []byte( {Name: "templates/manifest", Data: []byte(
@ -1238,7 +1258,7 @@ func TestRenderTplMissingKey(t *testing.T) {
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKey"}, Metadata: &chart.Metadata{Name: "TplMissingKey"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/manifest", Data: []byte( {Name: "templates/manifest", ModTime: time.Now(), Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)}, )},
}, },
@ -1271,7 +1291,7 @@ func TestRenderTplMissingKeyString(t *testing.T) {
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/manifest", Data: []byte( {Name: "templates/manifest", ModTime: time.Now(), Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)}, )},
}, },
@ -1300,16 +1320,17 @@ func TestRenderTplMissingKeyString(t *testing.T) {
} }
func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, Metadata: &chart.Metadata{Name: "NestedHelperFunctions"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/svc.yaml", Data: []byte( {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte(
`name: {{ include "nested_helper.name" . }}`, `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 -}}`, `{{- 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 -}}`, `{{- 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) { func TestMultilineNoTemplateAssociatedError(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "multiline"}, Metadata: &chart.Metadata{Name: "multiline"},
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/svc.yaml", Data: []byte( {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte(
`name: {{ include "nested_helper.name" . }}`, `name: {{ include "nested_helper.name" . }}`,
)}, )},
{Name: "templates/test.yaml", Data: []byte( {Name: "templates/test.yaml", ModTime: modTime, Data: []byte(
`{{ toYaml .Values }}`, `{{ toYaml .Values }}`,
)}, )},
{Name: "charts/common/templates/_helpers_2.tpl", Data: []byte( {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte(
`{{ toYaml .Values }}`, `{{ toYaml .Values }}`,
)}, )},
}, },
@ -1371,17 +1393,21 @@ template: no template "nested_helper.name" associated with template "gotpl"`
} }
func TestRenderCustomTemplateFuncs(t *testing.T) { func TestRenderCustomTemplateFuncs(t *testing.T) {
modTime := time.Now()
// Create a chart with two templates that use custom functions // Create a chart with two templates that use custom functions
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{Name: "CustomFunc"}, Metadata: &chart.Metadata{Name: "CustomFunc"},
Templates: []*common.File{ Templates: []*common.File{
{ {
Name: "templates/manifest", Name: "templates/manifest",
Data: []byte(`{{exclaim .Values.message}}`), ModTime: modTime,
Data: []byte(`{{exclaim .Values.message}}`),
}, },
{ {
Name: "templates/override", Name: "templates/override",
Data: []byte(`{{ upper .Values.message }}`), ModTime: modTime,
Data: []byte(`{{ upper .Values.message }}`),
}, },
}, },
} }

@ -100,7 +100,7 @@ func Mock(opts *MockReleaseOptions) *Release {
}, },
}, },
Templates: []*common.File{ Templates: []*common.File{
{Name: "templates/foo.tpl", Data: []byte(MockManifest)}, {Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)},
}, },
} }
} }

Loading…
Cancel
Save