-
-
Notifications
You must be signed in to change notification settings - Fork 303
feat: Add support for key+ syntax #2088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
please fix DCO |
Done! @yxxhero |
@nocturo please fix tests issues. |
@nocturo still failed. |
Please document this behaviour, so users would actually able to discover and use it. |
@nocturo ping |
@nocturo awesome!thanks for your work. |
pkg/state/state.go
Outdated
return generatedFiles, nil | ||
} | ||
|
||
// mergeAppendValues merges two values for the same key, preserving key+ keys for later processing | ||
func mergeAppendValues(existing, incoming any) any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add unittest for this func?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a unit test for it, is it suitable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah
@nocturo please fix DCO issue. |
@yxxhero fixed DCO |
@mumoshu WDYT? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work @nocturo!
Looks great and feeling very close to merge this!
But I have several comments regarding unused and potentially unnecessary functions.
Could you confirm?
If we could remove those unnecessary parts, we could also slim down the additional tests.
pkg/yaml/append_processor_test.go
Outdated
var result map[string]any | ||
err := UnmarshalWithAppend([]byte(tt.yamlData), &result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also cover cases when result
is non-empty, like you already did (thanks!) for TestAppendProcessor_MergeWithAppend
, but for UnmarshalWithAppend
this time.
pkg/yaml/append_processor.go
Outdated
return &AppendProcessor{} | ||
} | ||
|
||
func (ap *AppendProcessor) ProcessMap(data map[string]any) (map[string]any, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite understand why ProcessMap is needed!
I think the key idea is the use the new MergeWithAppend
instead of mergo.Merge
when merging src values to dst values, and MergeWithAppend
does not depend on this ProcessMap
(and the utility processValue
), right?
Could it be possible that you can just remove this ProcessMap
and the new merge-with-append feature just work?
pkg/state/state.go
Outdated
@@ -1683,11 +1682,12 @@ func (st *HelmState) WriteReleasesValues(helm helmexec.Interface, additionalValu | |||
return []error{fmt.Errorf("reading %s: %w", f, err)} | |||
} | |||
|
|||
if err := yaml.Unmarshal(srcBytes, &src); err != nil { | |||
if err := yaml.UnmarshalWithAppend(srcBytes, &src); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the special Unmarshal function here? The merge happens on MergeWithAppend
, which is aware of the new key+
syntax.
Isnt this special unmarshal function unnecessary, given we could just unmarshal using yaml.Unmarshal
so that you get the new src map whose keys may or may not contain the +
suffix, which is then merged using optional list-appending feature by the new MergeWithAppend
.
pkg/yaml/yaml.go
Outdated
return Unmarshal(processedYAML, v) | ||
} | ||
|
||
// NewDecoderWithAppend creates and returns a function that is used to decode a YAML document |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this new NewDecoderWithAppend
has no use and that's why this looks unused?
If I'm not mistaken, this could be removed?
pkg/yaml/yaml.go
Outdated
@@ -63,3 +63,76 @@ func Unmarshal(data []byte, v any) error { | |||
|
|||
return v2.Unmarshal(data, v) | |||
} | |||
|
|||
// UnmarshalWithAppend unmarshals YAML data with support for key+ syntax |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As commented elsewhere, I feel like this is not necessary and can be removed. Merge-with-list-appending happens elsewhere so you won't need to touch unmarshaler and decoder?
I've simplified the code further, hopefully a much cleaner solution! |
Signed-off-by: Nemanja Zeljkovic <[email protected]>
Signed-off-by: Nemanja Zeljkovic <[email protected]>
Signed-off-by: Nemanja Zeljkovic <[email protected]>
Signed-off-by: Nemanja Zeljkovic <[email protected]>
Signed-off-by: Nemanja Zeljkovic <[email protected]>
Hi, interesting feature—thanks for sharing! I was just reading the PR description and got a bit confused by the statement that the Also, in the examples provided, I don’t actually see any lists being appended—only maps being merged. As far as I can tell, what’s shown in the PR is already default behavior for maps in many YAML tools, so it’s not clear what benefit the The overall idea sounds very useful, especially for lists (where an append would be really handy!), but as written, the explanation around maps and lists could be a bit misleading. If I’m missing something, please let me know—just wanted to flag this for clarity! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👎 I Strongly Oppose Introducing a Custom Append Processor
This code duplicates and oversimplifies—therefore misrepresents—functionality that is already better handled by mergo
:
- Redundant Wheel Reinvention –
mergo.Merge(&dst, src, mergo.WithOverride, mergo.WithAppendSlice)
already does this (and much more), including recursive map merging and safe slice appending - Incorrect “Append” Semantics – When
srcValue
ordestValue
aren't slices, the result is a destructive overwrite (dest[baseKey] = srcValue
). This breaks user expectations, as “append” should imply non-destructive extension - Panic Risk – The type assertion
srcValue.([]any)
panics if the slice holds a different type;mergo
handles this safely via reflect and error-return logic - Maps ≠ Lists – Go maps are explicitly unordered (per the Go spec); “appending” to a map makes no conceptual sense—only key merging is possible, not preserving order
- Maintainability & Test Coverage –
mergo
is battle-tested (Docker, K8s, Datadog) ; this custom implementation has minimal test coverage and increases bug surface (+95 LOC in this PR) - Unnecessary Complexity – Now there are two merge paths (Mergo + AppendProcessor), increasing cognitive overhead and behavioral divergence risk
- Breaking Change with No Functional Gain – This is a definitive breaking change that offers no meaningful functionality in return. It disrupts expected behavior without justification.
⚠️ Insufficient Tests
The current pkg/yaml/append_processor_test.go
includes only four happy-path cases:
append slice→slice, recursion, creating a new slice, and overwriting a non-slice. It fails to cover:
- Type Collisions – e.g.,
[]string
vs[]any
; current implementation panics on type assert - Append on Maps or Scalars – silently overwrites; completely untested.
- Immutability of
src
–delete
mutates the input, but tests don’t verify this side effect. - Nil vs Empty Slices – subtle but important; diverges from
mergo.WithAppendSlice
behavior - Property-Based Edge Cases – would easily reveal issues above; just use
gopter
orrapid
The result is a false positive CI: the function never returns an error, and tests don't provoke panic conditions—so everything "passes."
💡 Recommendation
- Retain only
key+
detection, but delegate merging tomergo
(WithOverride
,WithAppendSlice
); for special cases, use a custommergo.Transformer
- Add negative tests (type mismatches, maps, nil slices) and property-based tests.
- Eliminate input mutation → return a merged result without side effects.
TL;DR: Instead of 100 lines of risky custom code, use one clean call to
mergo.Merge(...)
—with proper test coverage. It’s safer, more robust, and fully maintainable.
srcSlice := srcValue.([]any) | ||
dest[baseKey] = append(destSlice, srcSlice...) | ||
} else { | ||
dest[baseKey] = srcValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I seeing this correctly? Is this just a hard overwrite of the value instead of a merge? 🫨
dest[baseKey] = srcValue | ||
} | ||
} else { | ||
dest[baseKey] = srcValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the same case here as well — a hard overwrite instead of a merge?
@JuryA Thanks! The core problem for us is that we don't want a global setting to toggle My concern is that I think @nocturo and you have divergent ideas on how to implement it/move this forward? Thank you in advance for your support! |
This PR adds support for key+ syntax that is used in several other places for appending to lists and maps instead of overwriting them and keeping your code DRY (in case you want to append to some map you've defined before).
How would you use it?
helmfile.yaml:
base.yaml:
override.yaml:
and we get these values out:
Let me know if this is something that can benefit the project.