pull/31323/merge
Matt Farina 7 hours ago committed by GitHub
commit eb50e4eee5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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

@ -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"),
},
},
}

@ -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"),
},
},
}

@ -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 {

@ -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})
}
}

@ -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.

@ -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

@ -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

@ -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
}

@ -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{}{}

@ -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{}{}

@ -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)

@ -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")},
},
}

@ -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"`
}

@ -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{

@ -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()
}

@ -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

@ -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"),
},
},
}

@ -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"),
},
},
}

@ -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 {

@ -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})
}
}

@ -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.

@ -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

@ -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

@ -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
}

@ -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 {

@ -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 }}`),
},
},
}

@ -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)},
},
}
}

Loading…
Cancel
Save