Skip to content

Commit 5e4cbec

Browse files
committed
fix!: allow values files from any directory on Zarf package create
Signed-off-by: Wayne Starr <[email protected]>
1 parent febfefe commit 5e4cbec

File tree

8 files changed

+153
-48
lines changed

8 files changed

+153
-48
lines changed

examples/values-templating/zarf.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ metadata:
44
description: Example nginx package to demonstrate Zarf Values templating
55
values:
66
files:
7-
- values.yaml
7+
- values/values.yaml
88

99
# Until Consts and Vars are fully deprecated, they'll be available in go-templates
1010
constants:

src/internal/value/value.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func ParseFiles(ctx context.Context, paths []string, _ ParseFilesOptions) (_ Val
8585
if err != nil {
8686
return nil, err
8787
}
88-
vals, err = parseLocalFile(ctx, path)
88+
vals, err = ParseLocalFile(ctx, path)
8989
if err != nil {
9090
return nil, err
9191
}
@@ -96,7 +96,8 @@ func ParseFiles(ctx context.Context, paths []string, _ ParseFilesOptions) (_ Val
9696
return m, nil
9797
}
9898

99-
func parseLocalFile(ctx context.Context, path string) (Values, error) {
99+
// ParseLocalFile reads and parses a single local YAML file into a Values map.
100+
func ParseLocalFile(ctx context.Context, path string) (Values, error) {
100101
m := make(Values)
101102

102103
// Handle files
@@ -114,7 +115,8 @@ func parseLocalFile(ctx context.Context, path string) (Values, error) {
114115
// Decode and merge values
115116
if err = yaml.NewDecoder(f).DecodeContext(ctx, &m); err != nil {
116117
if errors.Is(err, io.EOF) {
117-
return m, nil // Empty file is ok
118+
// Empty file - ensure we return initialized map not nil
119+
return make(Values), nil
118120
}
119121
return nil, &YAMLDecodeError{
120122
FilePath: path,

src/internal/value/value_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,111 @@ func TestParseFiles_Errors(t *testing.T) {
216216
}
217217
}
218218

219+
func TestParseLocalFile(t *testing.T) {
220+
tests := []struct {
221+
name string
222+
file string
223+
expectedResult Values
224+
expectError bool
225+
}{
226+
{
227+
name: "single valid YAML file",
228+
file: "testdata/valid/simple.yaml",
229+
expectedResult: Values{
230+
"my-component": map[string]any{
231+
"key1": "value1",
232+
"key2": "value2",
233+
},
234+
},
235+
expectError: false,
236+
},
237+
{
238+
name: "complex nested YAML",
239+
file: "testdata/valid/complex.yaml",
240+
expectedResult: Values{
241+
"deployment": map[string]any{
242+
"replicas": uint64(3),
243+
"image": map[string]any{
244+
"repository": "nginx",
245+
"tag": "1.21",
246+
},
247+
"resources": map[string]any{
248+
"limits": map[string]any{
249+
"cpu": "100m",
250+
"memory": "128Mi",
251+
},
252+
"requests": map[string]any{
253+
"cpu": "50m",
254+
"memory": "64Mi",
255+
},
256+
},
257+
},
258+
"service": map[string]any{
259+
"type": "ClusterIP",
260+
"port": uint64(80),
261+
"targetPort": uint64(8080),
262+
},
263+
"ingress": map[string]any{
264+
"enabled": true,
265+
"annotations": map[string]any{
266+
"kubernetes.io/ingress.class": "nginx",
267+
},
268+
"hosts": []any{
269+
map[string]any{
270+
"host": "example.com",
271+
"paths": []any{
272+
map[string]any{
273+
"path": "/",
274+
"pathType": "Prefix",
275+
},
276+
},
277+
},
278+
},
279+
},
280+
},
281+
expectError: false,
282+
},
283+
{
284+
name: "empty YAML file",
285+
file: "testdata/valid/empty.yaml",
286+
expectedResult: Values{},
287+
expectError: false,
288+
},
289+
{
290+
name: "file does not exist",
291+
file: "testdata/nonexistent.yaml",
292+
expectError: true,
293+
},
294+
{
295+
name: "invalid YAML syntax",
296+
file: "testdata/invalid/malformed.yaml",
297+
expectError: true,
298+
},
299+
{
300+
name: "malformed YAML with tabs",
301+
file: "testdata/invalid/tabs.yaml",
302+
expectError: true,
303+
},
304+
}
305+
306+
for _, tt := range tests {
307+
t.Run(tt.name, func(t *testing.T) {
308+
t.Parallel()
309+
ctx := testutil.TestContext(t)
310+
311+
result, err := ParseLocalFile(ctx, tt.file)
312+
313+
if tt.expectError {
314+
require.Error(t, err)
315+
require.Nil(t, result)
316+
} else {
317+
require.NoError(t, err)
318+
require.Equal(t, tt.expectedResult, result)
319+
}
320+
})
321+
}
322+
}
323+
219324
func TestExtract(t *testing.T) {
220325
tests := []struct {
221326
name string

src/pkg/packager/deploy.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,15 @@ func Deploy(ctx context.Context, pkgLayout *layout.PackageLayout, opts DeployOpt
142142
" Run again with --features=\"%s=true\"", feature.Values, feature.Values)
143143
}
144144

145-
// Read the default package values off of the pkgLayout.Pkg.Values.Files.
146-
// Resolve values file paths relative to the package directory
147-
valueFilePaths := make([]string, len(pkgLayout.Pkg.Values.Files))
148-
for i, vf := range pkgLayout.Pkg.Values.Files {
149-
valueFilePaths[i] = filepath.Join(pkgLayout.DirPath(), layout.ValuesDir, vf)
150-
}
151-
vals, err := value.ParseFiles(ctx, valueFilePaths, value.ParseFilesOptions{})
152-
if err != nil {
153-
return DeployResult{}, err
145+
// Read the package values from values.yaml if it exists
146+
vals := make(value.Values)
147+
valuesPath := filepath.Join(pkgLayout.DirPath(), layout.ValuesYAML)
148+
if _, err := os.Stat(valuesPath); err == nil {
149+
parsedVals, err := value.ParseLocalFile(ctx, valuesPath)
150+
if err != nil {
151+
return DeployResult{}, err
152+
}
153+
vals = parsedVals
154154
}
155155
// Package defaults are overridden by deploy values.
156156
vals.DeepMerge(opts.Values)

src/pkg/packager/layout/assemble.go

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/zarf-dev/zarf/src/internal/packager/helm"
3030
"github.com/zarf-dev/zarf/src/internal/packager/images"
3131
"github.com/zarf-dev/zarf/src/internal/packager/kustomize"
32+
"github.com/zarf-dev/zarf/src/internal/value"
3233
"github.com/zarf-dev/zarf/src/pkg/archive"
3334
"github.com/zarf-dev/zarf/src/pkg/logger"
3435
"github.com/zarf-dev/zarf/src/pkg/packager/actions"
@@ -156,11 +157,9 @@ func AssemblePackage(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath
156157
}
157158
}
158159

159-
l.Debug("copying values files to package", "files", pkg.Values.Files)
160-
for _, file := range pkg.Values.Files {
161-
if err = copyValuesFile(ctx, file, packagePath, buildPath); err != nil {
162-
return nil, err
163-
}
160+
l.Debug("merging values files to package", "files", pkg.Values.Files)
161+
if err = mergeAndWriteValuesFile(ctx, pkg.Values.Files, packagePath, buildPath); err != nil {
162+
return nil, err
164163
}
165164

166165
checksumContent, checksumSha, err := getChecksum(buildPath)
@@ -876,35 +875,34 @@ func createReproducibleTarballFromDir(dirPath, dirPrefix, tarballPath string, ov
876875
})
877876
}
878877

879-
func copyValuesFile(ctx context.Context, file, packagePath, buildPath string) error {
878+
func mergeAndWriteValuesFile(ctx context.Context, files []string, packagePath, buildPath string) error {
880879
l := logger.From(ctx)
881880

882-
// Process local values file
883-
src := file
884-
if !filepath.IsAbs(src) {
885-
src = filepath.Join(packagePath, ValuesDir, file)
886-
}
887-
// Validate src
888-
if _, err := os.Stat(src); err != nil {
889-
return fmt.Errorf("unable to access values file %s: %w", src, err)
890-
}
891-
892-
// Ensure relative paths don't munge the destination and write outside of the package tmpdir
893-
cleanFile := filepath.Clean(file)
894-
if strings.HasPrefix(cleanFile, "..") {
895-
return fmt.Errorf("values file path %s escapes package directory", file)
881+
// Build absolute paths for all values files
882+
valueFilePaths := make([]string, len(files))
883+
for i, file := range files {
884+
src := file
885+
if !filepath.IsAbs(src) {
886+
src = filepath.Join(packagePath, file)
887+
}
888+
// Validate src exists
889+
if _, err := os.Stat(src); err != nil {
890+
return fmt.Errorf("unable to access values file %s: %w", src, err)
891+
}
892+
valueFilePaths[i] = src
896893
}
897894

898-
//Copy file to pre-archive package - destination includes ValuesDir
899-
dst := filepath.Join(buildPath, ValuesDir, cleanFile)
900-
l.Debug("copying values file", "src", src, "dst", dst)
901-
if err := helpers.CreatePathAndCopy(src, dst); err != nil {
902-
return fmt.Errorf("failed to copy values file %s: %w", src, err)
895+
// Parse and merge all values files
896+
vals, err := value.ParseFiles(ctx, valueFilePaths, value.ParseFilesOptions{})
897+
if err != nil {
898+
return fmt.Errorf("failed to parse values files: %w", err)
903899
}
904900

905-
// Set appropriate file permissions
906-
if err := os.Chmod(dst, helpers.ReadWriteUser); err != nil {
907-
return fmt.Errorf("failed to set permissions on values file %s: %w", dst, err)
901+
// Write merged values to YAML
902+
dst := filepath.Join(buildPath, ValuesYAML)
903+
l.Debug("writing merged values file", "dst", dst, "fileCount", len(files))
904+
if err := utils.WriteYaml(dst, vals, helpers.ReadWriteUser); err != nil {
905+
return fmt.Errorf("failed to write merged values file: %w", err)
908906
}
909907

910908
return nil

src/pkg/packager/layout/layout.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010

1111
// Constants used in the default package layout.
1212
const (
13-
ZarfYAML = "zarf.yaml"
14-
Signature = "zarf.yaml.sig"
15-
Checksums = "checksums.txt"
13+
ZarfYAML = "zarf.yaml"
14+
Signature = "zarf.yaml.sig"
15+
Checksums = "checksums.txt"
16+
ValuesYAML = "values.yaml"
1617

1718
ImagesDir = "images"
1819
ComponentsDir = "components"
19-
ValuesDir = "values"
2020

2121
SBOMDir = "zarf-sbom"
2222
SBOMTar = "sboms.tar"

src/pkg/zoci/pull_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ func TestAssembleLayers(t *testing.T) {
8989
// get all layers
9090
layers, err := remote.AssembleLayers(ctx, layoutExpected.Pkg.Components, false, zoci.AllLayers)
9191
require.NoError(t, err)
92-
require.Len(t, layers, 9)
92+
require.Len(t, layers, 10)
9393

94-
nonDeterministicLayers := []string{"zarf.yaml", "checksums.txt"}
94+
nonDeterministicLayers := []string{"zarf.yaml", "checksums.txt", "values.yaml"}
9595

9696
// get sbom layers - it appears as though the sbom layers are not deterministic
9797
sbomInspectLayers, err := remote.AssembleLayers(ctx, layoutExpected.Pkg.Components, false, zoci.SbomLayers)

src/test/packages/42-values/zarf.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55

66
values:
77
files:
8-
- values.yaml
8+
- values/values.yaml
99

1010
components:
1111
- name: test-values-component

0 commit comments

Comments
 (0)