Skip to content

Commit a83788f

Browse files
committed
update how we merge the YAML, directly reading/writing files
Signed-off-by: Prune <prune@lecentre.net>
1 parent 65d989b commit a83788f

File tree

4 files changed

+303
-173
lines changed

4 files changed

+303
-173
lines changed

internal/promotion/runner/builtin/yaml_merger.go

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@ func (y *yamlMerger) Run(
5151

5252
// convert validates yamlMerger configuration against a JSON schema and
5353
// converts it into a builtin.YAMLMergeConfig struct.
54-
func (y *yamlMerger) convert(
55-
cfg promotion.Config,
56-
) (builtin.YAMLMergeConfig, error) {
57-
return validateAndConvert[builtin.YAMLMergeConfig](y.schemaLoader, cfg, f.Name())
54+
func (y *yamlMerger) convert(cfg promotion.Config) (builtin.YAMLMergeConfig, error) {
55+
return validateAndConvert[builtin.YAMLMergeConfig](y.schemaLoader, cfg, y.Name())
5856
}
5957

6058
// validate validates yamlMerger configuration against a JSON schema.
@@ -71,63 +69,51 @@ func (y *yamlMerger) run(
7169
result := promotion.StepResult{Status: kargoapi.PromotionStepStatusSucceeded}
7270
failure := promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored}
7371

74-
// Keep track of files actually merged.
75-
mergedFiles := make([]string, 0, len(cfg.InFiles))
72+
// sanity check
73+
if len(cfg.InFiles) == 0 || cfg.OutFile == "" {
74+
return failure, fmt.Errorf("inFiles and OutFile must not be empty")
75+
}
7676

77-
// Secure join the paths to prevent path traversal attacks.
78-
yamlData := []string{}
77+
// Secure join the input paths to prevent path traversal attacks.
78+
filePaths := []string{}
7979
for _, path := range cfg.InFiles {
8080
inFile, err := securejoin.SecureJoin(stepCtx.WorkDir, path)
8181
if err != nil {
82-
return promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored},
83-
fmt.Errorf("could not secure join input file %q: %w", path, err)
82+
return failure, fmt.Errorf("could not secure join input file %q: %w", path, err)
8483
}
8584

86-
inBytes, err := os.ReadFile(inFile)
85+
// only add existing files
86+
_, err = os.Stat(inFile)
8787
if err != nil {
88-
// we skip if file does not exist
89-
if cfg.IgnoreMissingFiles && os.IsNotExist(err) {
88+
if cfg.IgnoreMissingFiles {
9089
continue
9190
}
92-
return failure, fmt.Errorf(
93-
"error reading file %q: %w",
94-
inFile,
95-
err,
96-
)
97-
}
91+
return failure, fmt.Errorf("input file not found: %s", inFile)
9892

99-
// we skip if file is empty
100-
if len(inBytes) == 0 {
101-
continue
10293
}
94+
filePaths = append(filePaths, inFile)
10395

104-
mergedFiles = append(mergedFiles, path)
105-
yamlData = append(yamlData, string(inBytes))
10696
}
10797

108-
// Merge
109-
outYAML, err := yaml.MergeYAMLFiles(yamlData)
110-
if err != nil {
111-
return promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored},
112-
fmt.Errorf("could not merge YAML files: %w", err)
113-
}
114-
115-
// Write out a single YAML file.
98+
// Secure join the output path to prevent path traversal attacks.
11699
outFile, err := securejoin.SecureJoin(stepCtx.WorkDir, cfg.OutFile)
117100
if err != nil {
118-
return promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored},
119-
fmt.Errorf("could not secure join outFile %q: %w", cfg.OutFile, err)
101+
return failure, fmt.Errorf("could not secure join outFile %q: %w", cfg.OutFile, err)
120102
}
121103

104+
// ensure output path fully exist
122105
if err = os.MkdirAll(filepath.Dir(outFile), 0o700); err != nil {
123106
return failure, fmt.Errorf("error creating directory structure %s: %w", filepath.Dir(outFile), err)
124107
}
125-
if err = os.WriteFile(outFile, []byte(outYAML), 0o600); err != nil {
126-
return failure, fmt.Errorf("error writing to file %s: %w", outFile, err)
108+
109+
// Merge files
110+
err = yaml.MergeYAMLFiles(filePaths, outFile)
111+
if err != nil {
112+
return failure, fmt.Errorf("could not merge YAML files: %w", err)
127113
}
128114

129115
// Add a commit message fragment to the step's output.
130-
if commitMsg := y.generateCommitMessage(cfg.OutFile, mergedFiles); commitMsg != "" {
116+
if commitMsg := y.generateCommitMessage(cfg.OutFile, filePaths); commitMsg != "" {
131117
result.Output = map[string]any{
132118
"commitMessage": commitMsg,
133119
}

internal/promotion/runner/builtin/yaml_merger_test.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func Test_YAMLMerger_run(t *testing.T) {
100100
},
101101
cfg: builtin.YAMLMergeConfig{
102102
InFiles: []string{"base.yaml", "overrides.yaml"},
103-
outFile: "modified.yaml",
103+
OutFile: "modified.yaml",
104104
},
105105
files: map[string]string{
106106
"base.yaml": `
@@ -129,17 +129,17 @@ app:
129129
},
130130
cfg: builtin.YAMLMergeConfig{
131131
InFiles: []string{"non-existent/values.yaml"},
132-
outFile: "modified.yaml",
132+
OutFile: "modified.yaml",
133133
IgnoreMissingFiles: false,
134134
},
135135
assertions: func(t *testing.T, _ string, result promotion.StepResult, err error) {
136136
assert.Error(t, err)
137137
assert.Equal(t, promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored}, result)
138-
assert.Contains(t, err.Error(), "error reading file")
138+
assert.Contains(t, err.Error(), "input file not found: ")
139139
},
140140
},
141141
{
142-
name: "failed to read InFiles file",
142+
name: "failed when nonexistent input file is given",
143143
stepCtx: &promotion.StepContext{
144144
Project: "test-project",
145145
},
@@ -149,13 +149,9 @@ app:
149149
IgnoreMissingFiles: true,
150150
},
151151
assertions: func(t *testing.T, _ string, result promotion.StepResult, err error) {
152-
assert.NoError(t, err)
153-
assert.Equal(t, promotion.StepResult{
154-
Status: kargoapi.PromotionStepStatusSucceeded,
155-
Output: map[string]any{
156-
"commitMessage": "Merged YAML files to modified.yaml\n",
157-
},
158-
}, result)
152+
assert.Error(t, err)
153+
assert.Equal(t, promotion.StepResult{Status: kargoapi.PromotionStepStatusErrored}, result)
154+
assert.Contains(t, err.Error(), "could not merge YAML files:")
159155
},
160156
},
161157
{
@@ -184,7 +180,7 @@ app:
184180
assert.Equal(t, promotion.StepResult{
185181
Status: kargoapi.PromotionStepStatusErrored,
186182
}, result)
187-
assert.Contains(t, err.Error(), "Error writing to file")
183+
assert.Contains(t, err.Error(), "inFiles and OutFile must not be empty")
188184
},
189185
},
190186
}

internal/yaml/yaml.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"bytes"
66
"fmt"
7-
"io"
87
"os"
98
"strconv"
109
"strings"
@@ -150,30 +149,36 @@ func findScalarNode(node *yaml.Node, keyPath []string) (int, int, error) {
150149
return 0, 0, fmt.Errorf("key path not found")
151150
}
152151

153-
// MergeYAMLFiles merges a list of yaml strings.
154-
func MergeYAMLFiles(inputs []string) (string, error) {
155-
if len(inputs) == 0 {
156-
return "", fmt.Errorf("empty input list provided")
152+
// MergeYAMLFiles merges a list of YAML files.
153+
func MergeYAMLFiles(inputPaths []string, outputPath string) error {
154+
if len(inputPaths) == 0 || outputPath == "" {
155+
return fmt.Errorf("inFiles and OutFile must not be empty")
157156
}
158157

159-
mergedNode, err := kyaml.Parse(inputs[0])
158+
// read first YAML file
159+
mergedNode, err := kyaml.ReadFile(inputPaths[0])
160160
if err != nil {
161-
return "", fmt.Errorf("error parsing first input: %w", err)
161+
return fmt.Errorf("error parsing first input file: %w", err)
162162
}
163163

164-
for i := 1; i < len(inputs); i++ {
165-
patchNode, err := kyaml.Parse(inputs[i])
164+
// read all other YAML file and apply the patch
165+
for i := 1; i < len(inputPaths); i++ {
166+
patchNode, err := kyaml.ReadFile(inputPaths[i])
166167
if err != nil {
167-
if err == io.EOF {
168-
continue
169-
}
170-
return "", fmt.Errorf("error parsing input at index %d: %w", i, err)
168+
return fmt.Errorf("error parsing input file %s: %w", inputPaths[i], err)
171169
}
170+
172171
mergedNode, err = merge2.Merge(patchNode, mergedNode, kyaml.MergeOptions{ListIncreaseDirection: 1})
173172
if err != nil {
174-
return "", fmt.Errorf("error merging node at index %d: %w", i, err)
173+
return fmt.Errorf("error merging node at index %d: %w", i, err)
175174
}
176175
}
177176

178-
return mergedNode.MustString(), nil
177+
// write the resulting file
178+
err = kyaml.WriteFile(mergedNode, outputPath)
179+
if err != nil {
180+
return fmt.Errorf("error writing the merged file to %s: %w", outputPath, err)
181+
}
182+
183+
return nil
179184
}

0 commit comments

Comments
 (0)