From d3678a84a50fb78c46bb463e60f672c9b8e283f2 Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Wed, 1 Apr 2026 11:36:27 +0200 Subject: [PATCH 1/8] Fix outdated comment in components.yaml --- pkg/utils/componentvector/componentvector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/componentvector/componentvector.go b/pkg/utils/componentvector/componentvector.go index f8476c8..3fe55d2 100644 --- a/pkg/utils/componentvector/componentvector.go +++ b/pkg/utils/componentvector/componentvector.go @@ -269,7 +269,7 @@ func WriteComponentVectorFile(fs afero.Afero, targetDirPath string, componentVec header := []byte(strings.Join([]string{ "# This file is updated by the gardener-landscape-kit.", - "# If this file is specified in the gardener-landscape-kit configuration file, the component versions will be used as overrides.", + "# If this file is present in the root of a gardener-landscape-kit-managed repository, the component versions will be used as overrides.", "# If custom component versions should be used, it is recommended to modify the specified versions here and run the `generate` command afterwards.", }, "\n") + "\n") From 399bd32c9b6c1caa4b72fb03846da4732451d5c1 Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Wed, 1 Apr 2026 11:47:36 +0200 Subject: [PATCH 2/8] Use general `WriteObjectsToFilesystem` for plain image vector resolution and writing --- pkg/cmd/resolve/plain/plain.go | 34 +++++++++++--------- pkg/utils/componentvector/componentvector.go | 15 +++++++++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/resolve/plain/plain.go b/pkg/cmd/resolve/plain/plain.go index 1329d14..88942fd 100644 --- a/pkg/cmd/resolve/plain/plain.go +++ b/pkg/cmd/resolve/plain/plain.go @@ -6,10 +6,7 @@ package plain import ( "context" - "errors" "fmt" - "os" - "path" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -22,6 +19,7 @@ import ( configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/cmd" utilscomponentvector "github.com/gardener/gardener-landscape-kit/pkg/utils/componentvector" + utilsfiles "github.com/gardener/gardener-landscape-kit/pkg/utils/files" ) var configDecoder runtime.Decoder @@ -122,21 +120,25 @@ func run(_ context.Context, opts *Options) error { } } - compVectorFile := path.Join(opts.TargetDirPath, utilscomponentvector.ComponentVectorFilename) - opts.Log.Info("Writing component vector file", "file", compVectorFile) - - var customBytes []byte - var err error - if customBytes, err = opts.fs.ReadFile(compVectorFile); err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to read component vector override file: %w", err) - } + var ( + err error + newDefaultCV utilscomponentvector.Interface + newDefaultBytes []byte + ) + newDefaultCV, err = utilscomponentvector.NewWithOverride(componentvector.DefaultComponentsYAML) + if err != nil { + return fmt.Errorf("failed to build default component vector: %w", err) } - - componentVector, err := utilscomponentvector.NewWithOverride(componentvector.DefaultComponentsYAML, customBytes) + newDefaultBytes, err = utilscomponentvector.NameVersionBytes(newDefaultCV) if err != nil { - return fmt.Errorf("failed to create component vector: %w", err) + return fmt.Errorf("failed to marshal default component vector: %w", err) } - return utilscomponentvector.WriteComponentVectorFile(opts.fs, opts.TargetDirPath, componentVector) + if err := utilsfiles.WriteObjectsToFilesystem(map[string][]byte{utilscomponentvector.ComponentVectorFilename: newDefaultBytes}, opts.TargetDirPath, "", opts.fs); err != nil { + return fmt.Errorf("failed to write updated component vector: %w", err) + } + + //return utilscomponentvector.WriteComponentVectorFile(opts.fs, opts.TargetDirPath, componentVector) + + return nil } diff --git a/pkg/utils/componentvector/componentvector.go b/pkg/utils/componentvector/componentvector.go index 3fe55d2..9800bf6 100644 --- a/pkg/utils/componentvector/componentvector.go +++ b/pkg/utils/componentvector/componentvector.go @@ -238,6 +238,21 @@ func stripDefaultVersionComments(data []byte) []byte { return []byte(strings.Join(out, "\n")) } +// NameVersionBytes marshals cv into a name+version-only Components YAML, stripping all other fields. +// This compact format is used for the .glk/defaults/ snapshot and as the three-way merge baseline in plain.go. +func NameVersionBytes(cv Interface) ([]byte, error) { + stripped := &Components{} + for _, name := range cv.ComponentNames() { + version, _ := cv.FindComponentVersion(name) + stripped.Components = append(stripped.Components, &ComponentVector{Name: name, Version: version}) + } + data, err := yaml.Marshal(stripped) + if err != nil { + return nil, fmt.Errorf("failed to marshal component versions: %w", err) + } + return data, nil +} + // WriteComponentVectorFile writes the component vector file effectively used to the target directory if applicable. func WriteComponentVectorFile(fs afero.Afero, targetDirPath string, componentVector Interface) error { var ( From 910a9fca1fa0311888b482b85dab63e2313b86fc Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Wed, 1 Apr 2026 11:52:28 +0200 Subject: [PATCH 3/8] Correctly support slices during three-way-merge and retain user modifications --- pkg/utils/meta/diff_test.go | 15 +++++++++++++++ pkg/utils/meta/merge.go | 7 ++++++- .../meta/testdata/merge-slice-1-default.yaml | 3 +++ pkg/utils/meta/testdata/merge-slice-2-edited.yaml | 3 +++ .../meta/testdata/merge-slice-3-new-default.yaml | 3 +++ .../merge-slice-4-expected-generated.yaml | 3 +++ 6 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 pkg/utils/meta/testdata/merge-slice-1-default.yaml create mode 100644 pkg/utils/meta/testdata/merge-slice-2-edited.yaml create mode 100644 pkg/utils/meta/testdata/merge-slice-3-new-default.yaml create mode 100644 pkg/utils/meta/testdata/merge-slice-4-expected-generated.yaml diff --git a/pkg/utils/meta/diff_test.go b/pkg/utils/meta/diff_test.go index 7d626a2..c0aa175 100644 --- a/pkg/utils/meta/diff_test.go +++ b/pkg/utils/meta/diff_test.go @@ -229,5 +229,20 @@ var _ = Describe("Meta Dir Config Diff", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) + + It("should retain user modifications in slices during a three-way-merge", func() { + oldDefault, err := testdata.ReadFile("testdata/merge-slice-1-default.yaml") + Expect(err).NotTo(HaveOccurred()) + newDefault, err := testdata.ReadFile("testdata/merge-slice-3-new-default.yaml") + Expect(err).NotTo(HaveOccurred()) + current, err := testdata.ReadFile("testdata/merge-slice-2-edited.yaml") + Expect(err).NotTo(HaveOccurred()) + expected, err := testdata.ReadFile("testdata/merge-slice-4-expected-generated.yaml") + Expect(err).NotTo(HaveOccurred()) + + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(Equal(string(expected))) + }) }) }) diff --git a/pkg/utils/meta/merge.go b/pkg/utils/meta/merge.go index 805d28a..4ecab8d 100644 --- a/pkg/utils/meta/merge.go +++ b/pkg/utils/meta/merge.go @@ -103,13 +103,18 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { oldValue = &yaml.Node{Kind: yaml.SequenceNode} } resultValue = threeWayMergeSequence(oldValue, newValueNode, currentValue) - case oldExists && !nodesEqual(oldValue, newValueNode, false): + case oldExists && !nodesEqual(oldValue, newValueNode, false) && nodesEqual(oldValue, currentValue, false): + // Default changed and current was not modified: take the new default. resultValue = &yaml.Node{ Kind: newValueNode.Kind, Value: newValueNode.Value, Style: newValueNode.Style, Tag: newValueNode.Tag, HeadComment: currentValue.HeadComment, LineComment: currentValue.LineComment, FootComment: currentValue.FootComment, Content: newValueNode.Content, } mergeNodeComments(oldValue, newValueNode, resultValue) + case oldExists && !nodesEqual(oldValue, newValueNode, false): + // Both default and current changed: keep current (user's value wins). + resultValue = currentValue + mergeNodeComments(oldValue, newValueNode, resultValue) default: resultValue = currentValue if oldExists { diff --git a/pkg/utils/meta/testdata/merge-slice-1-default.yaml b/pkg/utils/meta/testdata/merge-slice-1-default.yaml new file mode 100644 index 0000000..da79a13 --- /dev/null +++ b/pkg/utils/meta/testdata/merge-slice-1-default.yaml @@ -0,0 +1,3 @@ +components: +- name: github.com/gardener/gardener + version: v1.139.0 diff --git a/pkg/utils/meta/testdata/merge-slice-2-edited.yaml b/pkg/utils/meta/testdata/merge-slice-2-edited.yaml new file mode 100644 index 0000000..db6a125 --- /dev/null +++ b/pkg/utils/meta/testdata/merge-slice-2-edited.yaml @@ -0,0 +1,3 @@ +components: +- name: github.com/gardener/gardener + version: v1.99.0 diff --git a/pkg/utils/meta/testdata/merge-slice-3-new-default.yaml b/pkg/utils/meta/testdata/merge-slice-3-new-default.yaml new file mode 100644 index 0000000..8542ceb --- /dev/null +++ b/pkg/utils/meta/testdata/merge-slice-3-new-default.yaml @@ -0,0 +1,3 @@ +components: +- name: github.com/gardener/gardener + version: v1.139.1 diff --git a/pkg/utils/meta/testdata/merge-slice-4-expected-generated.yaml b/pkg/utils/meta/testdata/merge-slice-4-expected-generated.yaml new file mode 100644 index 0000000..db6a125 --- /dev/null +++ b/pkg/utils/meta/testdata/merge-slice-4-expected-generated.yaml @@ -0,0 +1,3 @@ +components: +- name: github.com/gardener/gardener + version: v1.99.0 From cee558aaea135a3994d0576278dec5694646803f Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Wed, 1 Apr 2026 12:04:52 +0200 Subject: [PATCH 4/8] Add `MergeMode` type and informative annotation logic to three-way merge Introduces `MergeMode` (Silent/Informative) and threads it through `threeWayMerge`, `threeWayMergeSection`, and `threeWayMergeSequence`. In `MergeModeInformative`, when an operator-overridden scalar value differs from the current GLK default, a `# glk default: # <-- glk-managed` line comment is appended to the value node. For complex nodes (mappings/sequences), a head comment on the key node signals the divergence. `WriteComponentVectorFile`, `stripDefaultVersionComments`, and `defaultVersionCommentMarker` are deleted. Their functionality is now covered generically by `WriteObjectsToFilesystem(MergeModeInformative)``, which plain.go already calls directly. The corresponding test describe block is removed along with the now-unused imports. Operator-comments are retained. --- pkg/apis/config/v1alpha1/types.go | 29 +++++ .../config/v1alpha1/validation/validation.go | 4 + .../v1alpha1/validation/validation_test.go | 37 ++++++ .../config/v1alpha1/zz_generated.deepcopy.go | 5 + pkg/cmd/resolve/plain/plain.go | 12 +- pkg/components/types.go | 18 ++- pkg/utils/componentvector/componentvector.go | 86 ------------- .../componentvector/componentvector_test.go | 116 ------------------ pkg/utils/files/writer.go | 33 ++++- pkg/utils/files/writer_test.go | 111 ++++++++++++++++- pkg/utils/meta/diff.go | 8 +- pkg/utils/meta/diff_test.go | 82 ++++++++++--- pkg/utils/meta/merge.go | 68 ++++++++-- pkg/utils/meta/preprocessing_test.go | 5 +- 14 files changed, 364 insertions(+), 250 deletions(-) diff --git a/pkg/apis/config/v1alpha1/types.go b/pkg/apis/config/v1alpha1/types.go index 7f893fa..1a79b16 100644 --- a/pkg/apis/config/v1alpha1/types.go +++ b/pkg/apis/config/v1alpha1/types.go @@ -26,6 +26,10 @@ type LandscapeKitConfiguration struct { // VersionConfig is the configuration for versioning. // +optional VersionConfig *VersionConfiguration `json:"versionConfig,omitempty"` + // MergeMode controls how operator overrides conflicting with updated GLK defaults are handled during three-way merge. + // Possible values are "Informative" (default) and "Silent". + // +optional + MergeMode *MergeMode `json:"mergeMode,omitempty"` } // ComponentsConfiguration contains configuration for components. @@ -119,3 +123,28 @@ type VersionConfiguration struct { // +optional DefaultVersionsUpdateStrategy *DefaultVersionsUpdateStrategy `json:"defaultVersionsUpdateStrategy,omitempty"` } + +// MergeMode controls how operator overrides are handled during three-way merge. +type MergeMode string + +const ( + // MergeModeInformative annotates operator-overridden values with a comment showing the current GLK default. + MergeModeInformative MergeMode = "Informative" + // MergeModeSilent retains operator overrides without annotation. + MergeModeSilent MergeMode = "Silent" +) + +// AllowedMergeModes lists all allowed merge modes. +var AllowedMergeModes = []string{ + string(MergeModeInformative), + string(MergeModeSilent), +} + +// GetMergeMode returns the configured MergeMode, defaulting to MergeModeInformative. +// It is safe to call on a nil receiver. +func (c *LandscapeKitConfiguration) GetMergeMode() MergeMode { + if c != nil && c.MergeMode != nil { + return *c.MergeMode + } + return MergeModeInformative +} diff --git a/pkg/apis/config/v1alpha1/validation/validation.go b/pkg/apis/config/v1alpha1/validation/validation.go index 1144b81..c0567e9 100644 --- a/pkg/apis/config/v1alpha1/validation/validation.go +++ b/pkg/apis/config/v1alpha1/validation/validation.go @@ -36,6 +36,10 @@ func ValidateLandscapeKitConfiguration(conf *configv1alpha1.LandscapeKitConfigur allErrs = append(allErrs, ValidateVersionConfig(conf.VersionConfig, field.NewPath("versionConfig"))...) } + if conf.MergeMode != nil && !slices.Contains(configv1alpha1.AllowedMergeModes, string(*conf.MergeMode)) { + allErrs = append(allErrs, field.Invalid(field.NewPath("mergeMode"), *conf.MergeMode, "allowed values are: "+strings.Join(configv1alpha1.AllowedMergeModes, ", "))) + } + return allErrs } diff --git a/pkg/apis/config/v1alpha1/validation/validation_test.go b/pkg/apis/config/v1alpha1/validation/validation_test.go index 1d8614c..5ccd0e4 100644 --- a/pkg/apis/config/v1alpha1/validation/validation_test.go +++ b/pkg/apis/config/v1alpha1/validation/validation_test.go @@ -294,6 +294,43 @@ var _ = Describe("Validation", func() { Expect(errList).To(BeEmpty()) }) }) + + Context("MergeMode Configuration", func() { + It("should pass with valid MergeMode values", func() { + for _, mode := range []v1alpha1.MergeMode{ + v1alpha1.MergeModeInformative, + v1alpha1.MergeModeSilent, + } { + conf := &v1alpha1.LandscapeKitConfiguration{ + MergeMode: &mode, + } + errList := validation.ValidateLandscapeKitConfiguration(conf) + Expect(errList).To(BeEmpty(), fmt.Sprintf("MergeMode %q should be valid", mode)) + } + }) + + It("should pass when MergeMode is not set", func() { + conf := &v1alpha1.LandscapeKitConfiguration{} + errList := validation.ValidateLandscapeKitConfiguration(conf) + Expect(errList).To(BeEmpty()) + }) + + It("should fail with an invalid MergeMode value", func() { + invalid := v1alpha1.MergeMode("Invalid") + conf := &v1alpha1.LandscapeKitConfiguration{ + MergeMode: &invalid, + } + + errList := validation.ValidateLandscapeKitConfiguration(conf) + Expect(errList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("mergeMode"), + "BadValue": Equal(invalid), + })), + )) + }) + }) }) }) diff --git a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go index 5d2b130..3ce6502 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -112,6 +112,11 @@ func (in *LandscapeKitConfiguration) DeepCopyInto(out *LandscapeKitConfiguration *out = new(VersionConfiguration) (*in).DeepCopyInto(*out) } + if in.MergeMode != nil { + in, out := &in.MergeMode, &out.MergeMode + *out = new(MergeMode) + **out = **in + } return } diff --git a/pkg/cmd/resolve/plain/plain.go b/pkg/cmd/resolve/plain/plain.go index 88942fd..6ac366d 100644 --- a/pkg/cmd/resolve/plain/plain.go +++ b/pkg/cmd/resolve/plain/plain.go @@ -7,6 +7,7 @@ package plain import ( "context" "fmt" + "strings" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -134,11 +135,16 @@ func run(_ context.Context, opts *Options) error { return fmt.Errorf("failed to marshal default component vector: %w", err) } - if err := utilsfiles.WriteObjectsToFilesystem(map[string][]byte{utilscomponentvector.ComponentVectorFilename: newDefaultBytes}, opts.TargetDirPath, "", opts.fs); err != nil { + header := []byte(strings.Join([]string{ + "# This file is updated by the gardener-landscape-kit.", + "# If this file is present in the root of a gardener-landscape-kit-managed repository, the component versions will be used as overrides.", + "# If custom component versions should be used, it is recommended to modify the specified versions here and run the `generate` command afterwards.", + }, "\n") + "\n") + newDefaultBytes = append(header, newDefaultBytes...) + + if err := utilsfiles.WriteObjectsToFilesystem(map[string][]byte{utilscomponentvector.ComponentVectorFilename: newDefaultBytes}, opts.TargetDirPath, "", opts.fs, opts.Config.GetMergeMode()); err != nil { return fmt.Errorf("failed to write updated component vector: %w", err) } - //return utilscomponentvector.WriteComponentVectorFile(opts.fs, opts.TargetDirPath, componentVector) - return nil } diff --git a/pkg/components/types.go b/pkg/components/types.go index ca38214..0b0c22a 100644 --- a/pkg/components/types.go +++ b/pkg/components/types.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/afero" "github.com/gardener/gardener-landscape-kit/componentvector" - "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" generateoptions "github.com/gardener/gardener-landscape-kit/pkg/cmd/generate/options" utilscomponentvector "github.com/gardener/gardener-landscape-kit/pkg/utils/componentvector" "github.com/gardener/gardener-landscape-kit/pkg/utils/files" @@ -36,6 +36,8 @@ type Options interface { GetFilesystem() afero.Afero // GetLogger returns the logger instance. GetLogger() logr.Logger + // GetMergeMode returns the configured merge mode for three-way merges. + GetMergeMode() configv1alpha1.MergeMode } // LandscapeOptions is an interface for options passed to components for generating the landscape. @@ -43,7 +45,7 @@ type LandscapeOptions interface { Options // GetGitRepository returns the git repository information. - GetGitRepository() *v1alpha1.GitRepository + GetGitRepository() *configv1alpha1.GitRepository // GetRelativeBasePath returns the base directory that is relative to the target path. GetRelativeBasePath() string // GetRelativeLandscapePath returns the landscape directory that is relative to the target path. @@ -65,6 +67,7 @@ type options struct { targetPath string filesystem afero.Afero logger logr.Logger + mergeMode configv1alpha1.MergeMode } // GetComponentVector returns the component vector. @@ -87,6 +90,11 @@ func (o *options) GetLogger() logr.Logger { return o.logger } +// GetMergeMode returns the configured merge mode for three-way merges. +func (o *options) GetMergeMode() configv1alpha1.MergeMode { + return o.mergeMode +} + // NewOptions returns a new Options instance. func NewOptions(opts *generateoptions.Options, fs afero.Afero) (Options, error) { var customComponentVectors [][]byte @@ -113,11 +121,13 @@ func NewOptions(opts *generateoptions.Options, fs afero.Afero) (Options, error) if err != nil { return nil, fmt.Errorf("failed to create component vector: %w", err) } + return &options{ componentVector: componentVector, targetPath: path.Clean(opts.TargetDirPath), filesystem: fs, logger: opts.Log, + mergeMode: opts.Config.GetMergeMode(), }, nil } @@ -134,11 +144,11 @@ func readCustomComponentsFile(opts *generateoptions.Options, fs afero.Afero, fil type landscapeOptions struct { Options - gitRepository *v1alpha1.GitRepository + gitRepository *configv1alpha1.GitRepository } // GetGitRepository returns the git repository information. -func (l *landscapeOptions) GetGitRepository() *v1alpha1.GitRepository { +func (l *landscapeOptions) GetGitRepository() *configv1alpha1.GitRepository { return l.gitRepository } diff --git a/pkg/utils/componentvector/componentvector.go b/pkg/utils/componentvector/componentvector.go index 9800bf6..9b24e6c 100644 --- a/pkg/utils/componentvector/componentvector.go +++ b/pkg/utils/componentvector/componentvector.go @@ -7,18 +7,12 @@ package componentvector import ( "fmt" "maps" - "path/filepath" "reflect" "slices" - "strings" - "github.com/spf13/afero" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" - - "github.com/gardener/gardener-landscape-kit/componentvector" - "github.com/gardener/gardener-landscape-kit/pkg/utils/files" ) const ( @@ -220,24 +214,6 @@ func resourcesToUnstructuredMap(resources map[string]ResourceData) (map[string]a return unstructuredMap, nil } -const ( - defaultVersionCommentMarker = "# <-- gardener-landscape-kit version default" -) - -// stripDefaultVersionComments removes GLK-managed default-version comment lines from a components.yaml file. -// A line is considered GLK-managed when it contains the unique GLK marker suffix. -// Stripping them before the three-way merge ensures the canonical comment is always (re-)written on the next run, even when the user has edited the comment text. -func stripDefaultVersionComments(data []byte) []byte { - lines := strings.Split(string(data), "\n") - out := make([]string, 0, len(lines)) - for _, line := range lines { - if !strings.Contains(line, defaultVersionCommentMarker) { - out = append(out, line) - } - } - return []byte(strings.Join(out, "\n")) -} - // NameVersionBytes marshals cv into a name+version-only Components YAML, stripping all other fields. // This compact format is used for the .glk/defaults/ snapshot and as the three-way merge baseline in plain.go. func NameVersionBytes(cv Interface) ([]byte, error) { @@ -252,65 +228,3 @@ func NameVersionBytes(cv Interface) ([]byte, error) { } return data, nil } - -// WriteComponentVectorFile writes the component vector file effectively used to the target directory if applicable. -func WriteComponentVectorFile(fs afero.Afero, targetDirPath string, componentVector Interface) error { - var ( - comp = &Components{} - postGenerateDefaultVersionCommentFns []func(string) string - ) - cvDefault, err := NewWithOverride(componentvector.DefaultComponentsYAML) - if err != nil { - return fmt.Errorf("failed to build default component vector: %w", err) - } - for _, componentName := range componentVector.ComponentNames() { - componentVersion, _ := componentVector.FindComponentVersion(componentName) - comp.Components = append(comp.Components, &ComponentVector{ - Name: componentName, - Version: componentVersion, - }) - defaultVersion, found := cvDefault.FindComponentVersion(componentName) - if found && componentVersion != defaultVersion { - defaultVersionComment := "# version: " + defaultVersion + " " + defaultVersionCommentMarker - postGenerateDefaultVersionCommentFns = append(postGenerateDefaultVersionCommentFns, func(data string) string { - return strings.ReplaceAll(data, componentName+"\n", componentName+"\n"+defaultVersionComment+"\n") - }) - } - } - data, err := yaml.Marshal(comp) - if err != nil { - return fmt.Errorf("failed to marshal component vector: %w", err) - } - - header := []byte(strings.Join([]string{ - "# This file is updated by the gardener-landscape-kit.", - "# If this file is present in the root of a gardener-landscape-kit-managed repository, the component versions will be used as overrides.", - "# If custom component versions should be used, it is recommended to modify the specified versions here and run the `generate` command afterwards.", - }, "\n") + "\n") - - // Before writing, strip any GLK-managed default-version comment lines from the on-disk file. - // This resets GLK-owned annotations so the canonical comment is always (re-)applied below, even when the user has edited or removed the comment line. - filePath := filepath.Join(targetDirPath, ComponentVectorFilename) - if existing, readErr := fs.ReadFile(filePath); readErr == nil { - if stripped := stripDefaultVersionComments(existing); string(stripped) != string(existing) { - if writeErr := fs.WriteFile(filePath, stripped, 0600); writeErr != nil { - return writeErr - } - } - } - - // Pass 1: write without default-version comments so the three-way merge operates on - // comment-free content. This establishes a clean baseline in the .glk/defaults/ snapshot. - dataWithoutComments := append(header, data...) - if err := files.WriteObjectsToFilesystem(map[string][]byte{ComponentVectorFilename: dataWithoutComments}, targetDirPath, "", fs); err != nil { - return err - } - - // Pass 2: inject default-version comments and write again. Because the .glk/defaults/ snapshot from Pass 1 has no comments, - // the comments are always treated as "new" by the three-way merge and are therefore reliably written into the output file. - for _, fn := range postGenerateDefaultVersionCommentFns { - data = []byte(fn(string(data))) - } - dataWithComments := append(header, data...) - return files.WriteObjectsToFilesystem(map[string][]byte{ComponentVectorFilename: dataWithComments}, targetDirPath, "", fs) -} diff --git a/pkg/utils/componentvector/componentvector_test.go b/pkg/utils/componentvector/componentvector_test.go index 753d556..8836547 100644 --- a/pkg/utils/componentvector/componentvector_test.go +++ b/pkg/utils/componentvector/componentvector_test.go @@ -5,14 +5,9 @@ package componentvector_test import ( - "strings" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/spf13/afero" - "sigs.k8s.io/yaml" - "github.com/gardener/gardener-landscape-kit/componentvector" . "github.com/gardener/gardener-landscape-kit/pkg/utils/componentvector" ) @@ -683,115 +678,4 @@ components: }) }) - Describe("#WriteComponentVectorFile", func() { - const outputDir = "/output" - - // componentNames parses the written components.yaml and returns the list of component names. - componentNames := func(fs afero.Afero) []string { - data, err := fs.ReadFile(outputDir + "/components.yaml") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - var comps struct { - Components []struct { - Name string `json:"name"` - } `json:"components"` - } - ExpectWithOffset(1, yaml.Unmarshal(data, &comps)).NotTo(HaveOccurred()) - names := make([]string, 0, len(comps.Components)) - for _, c := range comps.Components { - names = append(names, c.Name) - } - return names - } - - cv := func(contents []byte) Interface { - cv, err := NewWithOverride(contents) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - return cv - } - - BeforeEach(func() { - componentvector.DefaultComponentsYAML = []byte(`components: -- name: github.com/gardener/gardener - sourceRepository: https://github.com/gardener/gardener - version: v1.137.1 -- name: github.com/gardener/other-component - sourceRepository: https://github.com/gardener/other-component - version: v2.0.0 -`) - }) - - It("should not produce duplicate entries when the user edits the injected default-version comment", func() { - fs := afero.Afero{Fs: afero.NewMemMapFs()} - - overrideCV := []byte(`components: -- name: github.com/gardener/gardener - sourceRepository: https://github.com/gardener/gardener - version: v1.99.0 -- name: github.com/gardener/other-component - sourceRepository: https://github.com/gardener/other-component - version: v2.0.0 -`) - - // Run 1: write from default CV so no comment is injected. - Expect(WriteComponentVectorFile(fs, outputDir, cv(componentvector.DefaultComponentsYAML))).To(Succeed()) - - // User changes the gardener version. - writtenFile := outputDir + "/components.yaml" - writtenData, err := fs.ReadFile(writtenFile) - Expect(err).NotTo(HaveOccurred()) - Expect(fs.WriteFile(writtenFile, - []byte(strings.ReplaceAll(string(writtenData), "version: v1.137.1", "version: v1.99.0")), - 0600)).To(Succeed()) - - // Run 2: the injected default-version comment appears. - Expect(WriteComponentVectorFile(fs, outputDir, cv(overrideCV))).To(Succeed()) - - writtenData, err = fs.ReadFile(writtenFile) - Expect(err).NotTo(HaveOccurred()) - Expect(string(writtenData)).To(ContainSubstring("# version: v1.137.1 # <-- gardener-landscape-kit version default")) - - // User edits the injected comment (e.g. adds a personal annotation). - writtenData, err = fs.ReadFile(writtenFile) - Expect(err).NotTo(HaveOccurred()) - Expect(fs.WriteFile(writtenFile, - []byte(strings.ReplaceAll(string(writtenData), - "# version: v1.137.1 # <-- gardener-landscape-kit version default", - "# version: v1.137.1 # <-- default (my annotation)")), - 0600)).To(Succeed()) - - // Run 3: must not duplicate gardener entry and amend the comment with a new default version comment. - Expect(WriteComponentVectorFile(fs, outputDir, cv(overrideCV))).To(Succeed()) - - Expect(componentNames(fs)).To(ConsistOf( - "github.com/gardener/gardener", - "github.com/gardener/other-component", - )) - - // The correct default-version comment must have been restored. - writtenData, err = fs.ReadFile(writtenFile) - Expect(err).NotTo(HaveOccurred()) - Expect(string(writtenData)).To(ContainSubstring("# <-- gardener-landscape-kit version default")) - Expect(string(writtenData)).To(ContainSubstring("my annotation")) - }) - - It("should not re-add entries that the user removed from the file", func() { - fs := afero.Afero{Fs: afero.NewMemMapFs()} - - // Run 1: write both entries. - Expect(WriteComponentVectorFile(fs, outputDir, cv(componentvector.DefaultComponentsYAML))).To(Succeed()) - - // User removes the other-component entry entirely. - writtenFile := outputDir + "/components.yaml" - writtenData, err := fs.ReadFile(writtenFile) - Expect(err).NotTo(HaveOccurred()) - idx := strings.Index(string(writtenData), "- name: github.com/gardener/other-component") - Expect(idx).To(BeNumerically(">", 0)) - Expect(fs.WriteFile(writtenFile, writtenData[:idx], 0600)).To(Succeed()) - - // Run 2: same vector — the removed entry must not come back. - Expect(WriteComponentVectorFile(fs, outputDir, cv(componentvector.DefaultComponentsYAML))).To(Succeed()) - - Expect(componentNames(fs)).To(ConsistOf("github.com/gardener/gardener")) - }) - }) }) diff --git a/pkg/utils/files/writer.go b/pkg/utils/files/writer.go index 26f7602..e9f7930 100644 --- a/pkg/utils/files/writer.go +++ b/pkg/utils/files/writer.go @@ -10,9 +10,11 @@ import ( "os" "path" "path/filepath" + "strings" "github.com/spf13/afero" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/utils/meta" ) @@ -44,10 +46,34 @@ func isSecret(contents []byte) bool { return bytes.HasPrefix(contents, []byte(kindSecret)) || bytes.Contains(contents, []byte("\n"+kindSecret)) } +// stripGLKManagedAnnotations removes GLK-managed annotation comments from YAML bytes. +// A line is annotated when it contains both GLKDefaultPrefix and GLKManagedMarker. +func stripGLKManagedAnnotations(data []byte) []byte { + lines := strings.Split(string(data), "\n") + out := make([]string, 0, len(lines)) + for _, line := range lines { + if !strings.Contains(line, meta.GLKManagedMarker) { + out = append(out, line) + continue + } + // Strip from the start of the GLK annotation prefix. + if idx := strings.Index(line, meta.GLKDefaultPrefix); idx >= 0 { + stripped := strings.TrimRight(line[:idx], " \t") + // If the entire line was a head comment annotation, drop the line entirely. + if strings.TrimSpace(stripped) == "" { + continue + } + out = append(out, stripped) + } + // If for some reason the marker is present but the prefix is not, drop the line. + } + return []byte(strings.Join(out, "\n")) +} + // WriteObjectsToFilesystem writes the given objects to the filesystem at the specified rootDir and relativeFilePath. // If the manifest file already exists, it patches changes from the new default. // Additionally, it maintains a default version of the manifest in a separate directory for future diff checks. -func WriteObjectsToFilesystem(objects map[string][]byte, rootDir, relativeFilePath string, fs afero.Afero) error { +func WriteObjectsToFilesystem(objects map[string][]byte, rootDir, relativeFilePath string, fs afero.Afero, mode configv1alpha1.MergeMode) error { if err := fs.MkdirAll(path.Join(rootDir, relativeFilePath), 0700); err != nil { return err } @@ -81,7 +107,10 @@ func WriteObjectsToFilesystem(objects map[string][]byte, rootDir, relativeFilePa object = append([]byte(secretEncryptionDisclaimer), object...) } - output, err := meta.ThreeWayMergeManifest(oldDefaultYaml, object, currentYaml) + // Strip GLK-managed annotations from the current file before merging, so they are always re-evaluated (idempotency: the annotation reflects the *current* GLK default). + currentYaml = stripGLKManagedAnnotations(currentYaml) + + output, err := meta.ThreeWayMergeManifest(oldDefaultYaml, object, currentYaml, mode) if err != nil { return err } diff --git a/pkg/utils/files/writer_test.go b/pkg/utils/files/writer_test.go index 8d2a91b..547682d 100644 --- a/pkg/utils/files/writer_test.go +++ b/pkg/utils/files/writer_test.go @@ -15,7 +15,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/utils/files" + "github.com/gardener/gardener-landscape-kit/pkg/utils/meta" ) var _ = Describe("Writer", func() { @@ -53,7 +55,7 @@ var _ = Describe("Writer", func() { baseDir := "/path/to" path := "my/files" - Expect(files.WriteObjectsToFilesystem(objects, baseDir, path, fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(objects, baseDir, path, fs, configv1alpha1.MergeModeSilent)).To(Succeed()) contents, err := fs.ReadFile("/path/to/my/files/file.yaml") Expect(err).NotTo(HaveOccurred()) @@ -65,7 +67,7 @@ var _ = Describe("Writer", func() { }) It("should overwrite the manifest file if no meta file is present yet", func() { - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/.glk/defaults/manifest/config.yaml") Expect(err).ToNot(HaveOccurred()) @@ -77,7 +79,7 @@ var _ = Describe("Writer", func() { }) It("should patch only changed default values on subsequent generates and retain custom modifications", func() { - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/manifest/config.yaml") Expect(err).ToNot(HaveOccurred()) @@ -96,7 +98,7 @@ var _ = Describe("Writer", func() { objYaml, err = yaml.Marshal(obj) Expect(err).NotTo(HaveOccurred()) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"config.yaml": objYaml}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err = fs.ReadFile("/landscape/.glk/defaults/manifest/config.yaml") Expect(err).ToNot(HaveOccurred()) @@ -112,7 +114,7 @@ var _ = Describe("Writer", func() { objYaml, err := yaml.Marshal(obj) Expect(err).NotTo(HaveOccurred()) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"secret.yaml": objYaml}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"secret.yaml": objYaml}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/manifest/secret.yaml") Expect(err).ToNot(HaveOccurred()) @@ -129,7 +131,7 @@ spec: kind: Secret name: my-secret`) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"secret.yaml": objYaml}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"secret.yaml": objYaml}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/manifest/secret.yaml") Expect(err).ToNot(HaveOccurred()) @@ -140,6 +142,103 @@ spec: }) }) + Describe("#WriteObjectsToFilesystem - MergeModeInformative", func() { + It("should annotate operator-overridden scalar values with the GLK default and preserve user comments idempotently", func() { + initial := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.0 +`) + // First generate: establish defaults + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + // Operator pins to a custom version with a comment explaining why + Expect(fs.WriteFile("/landscape/manifest/test.yaml", []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 # pinned for production +`), 0600)).To(Succeed()) + + // GLK ships a new default with a newer version + updated := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.1.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + content, err := fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + // Operator's override is preserved + Expect(string(content)).To(ContainSubstring("version: v1.0.5")) + // User comment is preserved + Expect(string(content)).To(ContainSubstring("pinned for production")) + // GLK default annotation is added + Expect(string(content)).To(ContainSubstring("# glk default: v1.1.0")) + Expect(string(content)).To(ContainSubstring(meta.GLKManagedMarker)) + + // Re-run with the same inputs — annotation and user comment must not be doubled + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + content2, err := fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content2)).To(Equal(string(content))) + }) + + It("should remove the annotation entirely when the GLK default reverts to the operator's value", func() { + initial := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + // Operator pins to v1.0.5 + Expect(fs.WriteFile("/landscape/revert/test.yaml", []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 +`), 0600)).To(Succeed()) + + // GLK ships v1.1.0 — annotation appears + updated := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.1.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err := fs.ReadFile("/landscape/revert/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring(meta.GLKManagedMarker)) + + // GLK reverts to v1.0.5 — operator's value now matches the default, no annotation + reverted := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": reverted}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/revert/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKManagedMarker)) + Expect(string(content)).NotTo(ContainSubstring("# glk default:")) + }) + }) + Describe("#RelativePathFromDirDepth", func() { It("should not go up if the passed relativeDir has no real depth", func() { Expect(files.RelativePathFromDirDepth("")).To(Equal(".")) diff --git a/pkg/utils/meta/diff.go b/pkg/utils/meta/diff.go index d5e8e10..7699ee7 100644 --- a/pkg/utils/meta/diff.go +++ b/pkg/utils/meta/diff.go @@ -13,6 +13,8 @@ import ( "github.com/elliotchance/orderedmap/v3" "go.yaml.in/yaml/v4" + + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" ) // section represents a single section in a manifest file (either a manifest or a comment) @@ -41,7 +43,7 @@ type manifestDiff struct { // It performs a three-way merge between the old default template, the new default template, and the current user-modified version. // It preserves user modifications while applying updates from the new default template. // Contents from the current manifest are prioritized and sorted first. -func ThreeWayMergeManifest(oldDefaultYaml, newDefaultYaml, currentYaml []byte) ([]byte, error) { +func ThreeWayMergeManifest(oldDefaultYaml, newDefaultYaml, currentYaml []byte, mode configv1alpha1.MergeMode) ([]byte, error) { var ( output []byte @@ -61,7 +63,7 @@ func ThreeWayMergeManifest(oldDefaultYaml, newDefaultYaml, currentYaml []byte) ( current := sect.content newDefault, _ := diff.newDefault.Get(sect.key) oldDefault, _ := diff.oldDefault.Get(sect.key) - merged, err := threeWayMergeSection(oldDefault, newDefault, current) + merged, err := threeWayMergeSection(oldDefault, newDefault, current, mode) if err != nil { return nil, err } @@ -75,7 +77,7 @@ func ThreeWayMergeManifest(oldDefaultYaml, newDefaultYaml, currentYaml []byte) ( continue } // Applying threeWayMergeSection with only the new section content to ensure proper formatting (idempotency). - merged, err := threeWayMergeSection(nil, sect.content, nil) + merged, err := threeWayMergeSection(nil, sect.content, nil, mode) if err != nil { return nil, err } diff --git a/pkg/utils/meta/diff_test.go b/pkg/utils/meta/diff_test.go index c0aa175..29b766b 100644 --- a/pkg/utils/meta/diff_test.go +++ b/pkg/utils/meta/diff_test.go @@ -15,6 +15,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/utils/meta" ) @@ -41,7 +42,7 @@ var _ = Describe("Meta Dir Config Diff", func() { objYaml, err := yaml.Marshal(obj) Expect(err).NotTo(HaveOccurred()) - newContents, err := meta.ThreeWayMergeManifest(nil, objYaml, nil) + newContents, err := meta.ThreeWayMergeManifest(nil, objYaml, nil, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) // Modify the manifest on disk @@ -57,7 +58,7 @@ var _ = Describe("Meta Dir Config Diff", func() { newObjYaml, err := yaml.Marshal(obj) Expect(err).NotTo(HaveOccurred()) - content, err = meta.ThreeWayMergeManifest(objYaml, newObjYaml, content) + content, err = meta.ThreeWayMergeManifest(objYaml, newObjYaml, content, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) expectedConfigMapOutputWithNewKey, err := testdata.ReadFile("testdata/expected_configmap_output_newkey.yaml") @@ -76,7 +77,7 @@ var _ = Describe("Meta Dir Config Diff", func() { manifestGenerated, err := testdata.ReadFile("testdata/manifest-4-expected-generated.yaml") Expect(err).NotTo(HaveOccurred()) - mergedManifest, err := meta.ThreeWayMergeManifest(manifestDefault, manifestDefaultNew, manifestEdited) + mergedManifest, err := meta.ThreeWayMergeManifest(manifestDefault, manifestDefaultNew, manifestEdited, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(mergedManifest)).To(Equal(string(manifestGenerated))) }) @@ -87,7 +88,7 @@ var _ = Describe("Meta Dir Config Diff", func() { expectedConfigMapOutputWithNewKey, err := testdata.ReadFile("testdata/expected_configmap_output_newkey.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(nil, expectedConfigMapOutputWithNewKey, []byte(strings.ReplaceAll(string(expectedDefaultConfigMapOutput), "key: value", "key: newDefaultValue"))) + content, err := meta.ThreeWayMergeManifest(nil, expectedConfigMapOutputWithNewKey, []byte(strings.ReplaceAll(string(expectedDefaultConfigMapOutput), "key: value", "key: newDefaultValue")), configv1alpha1.MergeModeSilent) Expect(err).ToNot(HaveOccurred()) Expect(string(content)).To(Equal(strings.ReplaceAll(string(expectedConfigMapOutputWithNewKey), "key: value", "key: newDefaultValue") + "\n")) }) @@ -102,21 +103,21 @@ var _ = Describe("Meta Dir Config Diff", func() { multipleManifestsExpectedGenerated, err := testdata.ReadFile("testdata/multiple-manifests-4-expected-generated.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(nil, multipleManifestsInitial, nil) + content, err := meta.ThreeWayMergeManifest(nil, multipleManifestsInitial, nil, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(multipleManifestsInitial))) - content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsInitial, multipleManifestsInitial) + content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsInitial, multipleManifestsInitial, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(multipleManifestsInitial))) // Editing the written manifest and updating the manifest with the same default content should not overwrite anything - content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsInitial, multipleManifestsEdited) + content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsInitial, multipleManifestsEdited, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(multipleManifestsEdited))) // New default manifest changes should be applied, while custom edits should be retained. - content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsNewDefault, multipleManifestsEdited) + content, err = meta.ThreeWayMergeManifest(multipleManifestsInitial, multipleManifestsNewDefault, multipleManifestsEdited, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(multipleManifestsExpectedGenerated))) }) @@ -131,7 +132,7 @@ var _ = Describe("Meta Dir Config Diff", func() { expected, err := testdata.ReadFile("testdata/order-4-expected.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) @@ -145,22 +146,22 @@ var _ = Describe("Meta Dir Config Diff", func() { invalidYaml = []byte(`keyWith: colonSuffix:`) ) - _, err = meta.ThreeWayMergeManifest(emptyYaml, invalidYaml, emptyYaml) + _, err = meta.ThreeWayMergeManifest(emptyYaml, invalidYaml, emptyYaml, configv1alpha1.MergeModeSilent) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("parsing newDefault file for manifest diff failed")) - _, err = meta.ThreeWayMergeManifest(invalidYaml, validYaml, validYaml) + _, err = meta.ThreeWayMergeManifest(invalidYaml, validYaml, validYaml, configv1alpha1.MergeModeSilent) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("parsing oldDefault file for manifest diff failed")) - _, err = meta.ThreeWayMergeManifest(validYaml, validYaml, invalidYaml) + _, err = meta.ThreeWayMergeManifest(validYaml, validYaml, invalidYaml, configv1alpha1.MergeModeSilent) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("parsing current file for manifest diff failed")) - _, err = meta.ThreeWayMergeManifest(validYaml, validYaml, validYaml) + _, err = meta.ThreeWayMergeManifest(validYaml, validYaml, validYaml, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) - _, err = meta.ThreeWayMergeManifest(emptyYaml, emptyYaml, emptyYaml) + _, err = meta.ThreeWayMergeManifest(emptyYaml, emptyYaml, emptyYaml, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) }) @@ -175,7 +176,7 @@ var _ = Describe("Meta Dir Config Diff", func() { expected, err := testdata.ReadFile("testdata/replaced-file-4-expected-generated.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) @@ -190,7 +191,7 @@ var _ = Describe("Meta Dir Config Diff", func() { expected, err := testdata.ReadFile("testdata/replaced-file-2-new-default.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) @@ -201,7 +202,7 @@ var _ = Describe("Meta Dir Config Diff", func() { // Two documents with the same structure should be treated as the same manifest across generations. nonK8sYaml := []byte("foo: bar\nbaz: qux\n") - content, err := meta.ThreeWayMergeManifest(nonK8sYaml, nonK8sYaml, nonK8sYaml) + content, err := meta.ThreeWayMergeManifest(nonK8sYaml, nonK8sYaml, nonK8sYaml, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(nonK8sYaml))) @@ -210,7 +211,7 @@ var _ = Describe("Meta Dir Config Diff", func() { newDefault := []byte("foo: bar\nbaz: updated\n") expected := []byte("foo: user-value\nbaz: updated\n") - content, err = meta.ThreeWayMergeManifest(nonK8sYaml, newDefault, edited) + content, err = meta.ThreeWayMergeManifest(nonK8sYaml, newDefault, edited, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(MatchYAML(string(expected))) }) @@ -225,7 +226,7 @@ var _ = Describe("Meta Dir Config Diff", func() { expected, err := testdata.ReadFile("testdata/replaced-file-6-different-name-merged.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) @@ -240,9 +241,50 @@ var _ = Describe("Meta Dir Config Diff", func() { expected, err := testdata.ReadFile("testdata/merge-slice-4-expected-generated.yaml") Expect(err).NotTo(HaveOccurred()) - content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current) + content, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeSilent) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(string(expected))) }) }) + + Describe("#ThreeWayMergeManifest - MergeModeInformative", func() { + It("should annotate a scalar value that the operator overrode and GLK updated, but not when there is no conflict", func() { + oldDefault := []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.0 +`) + newDefault := []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.1.0 +`) + // Operator pinned to v1.0.5 — conflicts with GLK's new default v1.1.0 + current := []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 +`) + result, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeInformative) + Expect(err).NotTo(HaveOccurred()) + Expect(string(result)).To(ContainSubstring("version: v1.0.5")) + Expect(string(result)).To(ContainSubstring("# glk default: v1.1.0")) + Expect(string(result)).To(ContainSubstring(meta.GLKManagedMarker)) + + // No conflict: operator did not change the value → new default taken silently, no annotation + result, err = meta.ThreeWayMergeManifest(oldDefault, newDefault, oldDefault, configv1alpha1.MergeModeInformative) + Expect(err).NotTo(HaveOccurred()) + Expect(string(result)).To(ContainSubstring("version: v1.1.0")) + Expect(string(result)).NotTo(ContainSubstring(meta.GLKManagedMarker)) + }) + }) }) diff --git a/pkg/utils/meta/merge.go b/pkg/utils/meta/merge.go index 4ecab8d..18d4757 100644 --- a/pkg/utils/meta/merge.go +++ b/pkg/utils/meta/merge.go @@ -6,10 +6,22 @@ package meta import ( "go.yaml.in/yaml/v4" + + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" +) + +const ( + // GLKManagedMarker is the suffix that marks a comment as GLK-managed. + // It is used to strip GLK-owned annotations before re-running the merge (idempotency). + GLKManagedMarker = "# <-- glk-managed" + + // GLKDefaultPrefix is the comment prefix for GLK-managed default annotations. + // It is exported so callers can use it as the strip anchor when removing annotations. + GLKDefaultPrefix = "# glk default: " ) // threeWayMergeSection performs a three-way merge on a single YAML section -func threeWayMergeSection(oldDefaultYaml, newDefaultYaml, currentYaml []byte) ([]byte, error) { +func threeWayMergeSection(oldDefaultYaml, newDefaultYaml, currentYaml []byte, mode configv1alpha1.MergeMode) ([]byte, error) { // Parse all three versions var oldDefault, newDefault, current yaml.Node if err := yaml.Unmarshal(newDefaultYaml, &newDefault); err != nil { @@ -26,14 +38,14 @@ func threeWayMergeSection(oldDefaultYaml, newDefaultYaml, currentYaml []byte) ([ } } - return EncodeResult(threeWayMerge(&oldDefault, &newDefault, ¤t)) + return EncodeResult(threeWayMerge(&oldDefault, &newDefault, ¤t, mode)) } // threeWayMerge performs a three-way merge of YAML nodes // oldDefault: the previous default template // newDefault: the new default template // current: the user's current version (possibly modified) -func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { +func threeWayMerge(oldDefault, newDefault, current *yaml.Node, mode configv1alpha1.MergeMode) *yaml.Node { // Unwrap document nodes if oldDefault.Kind == yaml.DocumentNode { oldDefault = oldDefault.Content[0] @@ -44,7 +56,7 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { if current.Kind == yaml.DocumentNode { return &yaml.Node{ Kind: yaml.DocumentNode, - Content: []*yaml.Node{threeWayMerge(oldDefault, newDefault, current.Content[0])}, + Content: []*yaml.Node{threeWayMerge(oldDefault, newDefault, current.Content[0], mode)}, } } @@ -97,12 +109,12 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { if !oldExists { oldValue = &yaml.Node{Kind: yaml.MappingNode} } - resultValue = threeWayMerge(oldValue, newValueNode, currentValue) + resultValue = threeWayMerge(oldValue, newValueNode, currentValue, mode) case currentValue.Kind == yaml.SequenceNode && newValueNode.Kind == yaml.SequenceNode: if !oldExists { oldValue = &yaml.Node{Kind: yaml.SequenceNode} } - resultValue = threeWayMergeSequence(oldValue, newValueNode, currentValue) + resultValue = threeWayMergeSequence(oldValue, newValueNode, currentValue, mode) case oldExists && !nodesEqual(oldValue, newValueNode, false) && nodesEqual(oldValue, currentValue, false): // Default changed and current was not modified: take the new default. resultValue = &yaml.Node{ @@ -115,10 +127,17 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { // Both default and current changed: keep current (user's value wins). resultValue = currentValue mergeNodeComments(oldValue, newValueNode, resultValue) + if mode == configv1alpha1.MergeModeInformative && !nodesEqual(newValueNode, currentValue, false) { + annotateConflict(resultKeyNode, resultValue, newValueNode) + } default: resultValue = currentValue if oldExists { mergeNodeComments(oldValue, newValueNode, resultValue) + if mode == configv1alpha1.MergeModeInformative && !nodesEqual(newValueNode, currentValue, false) { + // The operator's value differs from the current GLK default: annotate it. + annotateConflict(resultKeyNode, resultValue, newValueNode) + } } } } @@ -143,6 +162,39 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node) *yaml.Node { return result } +// annotateConflict adds a GLK-managed annotation comment to resultValue (or resultKeyNode for complex nodes) indicating the current GLK default. +// This is used in MergeModeInformative when an operator override conflicts with an updated GLK default, so the user is informed of the divergence. +// +// For scalar nodes, the annotation is a line comment on the value node (same line as the value). +// For complex nodes (mappings/sequences), the annotation is a head comment on the key node (line above the key). +func annotateConflict(resultKeyNode, resultValue, newDefaultNode *yaml.Node) { + switch newDefaultNode.Kind { + case yaml.ScalarNode: + annotation := glkManagedLineComment(newDefaultNode.Value) + if resultValue.LineComment != "" { + resultValue.LineComment = resultValue.LineComment + " " + annotation + } else { + resultValue.LineComment = annotation + } + default: + resultKeyNode.HeadComment = glkManagedHeadComment(resultKeyNode.HeadComment) + } +} + +// glkManagedLineComment returns a GLK-managed line comment for a scalar value conflict. +func glkManagedLineComment(newValue string) string { + return GLKDefaultPrefix + newValue + " " + GLKManagedMarker +} + +// glkManagedHeadComment returns a GLK-managed head comment for a complex node conflict. +func glkManagedHeadComment(existingHead string) string { + annotation := GLKDefaultPrefix + "(complex node changed) " + GLKManagedMarker + if existingHead == "" { + return annotation + } + return existingHead + "\n" + annotation +} + // mergeComment performs a three-way merge on a single comment string. // If the default comment changed (old != new), the new default is applied — unless the user // has also modified the comment (current != old), in which case the user's comment is kept @@ -177,7 +229,7 @@ func mergeNodeComments(oldNode, newNode, resultNode *yaml.Node) { } // Order is preserved based on newDefault, with user additions appended at the end -func threeWayMergeSequence(oldDefault, newDefault, current *yaml.Node) *yaml.Node { +func threeWayMergeSequence(oldDefault, newDefault, current *yaml.Node, mode configv1alpha1.MergeMode) *yaml.Node { if nodesEqual(oldDefault, current, true) { return newDefault } @@ -234,7 +286,7 @@ func threeWayMergeSequence(oldDefault, newDefault, current *yaml.Node) *yaml.Nod if !existsInOld { oldItem = &yaml.Node{Kind: yaml.MappingNode} } - result.Content = append(result.Content, threeWayMerge(oldItem, newItem, currentItem)) + result.Content = append(result.Content, threeWayMerge(oldItem, newItem, currentItem, mode)) } // Append items from newDefault that are truly new (not in old) and not already in current. diff --git a/pkg/utils/meta/preprocessing_test.go b/pkg/utils/meta/preprocessing_test.go index f879d0e..8e1971a 100644 --- a/pkg/utils/meta/preprocessing_test.go +++ b/pkg/utils/meta/preprocessing_test.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/afero" "go.yaml.in/yaml/v4" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/utils/files" "github.com/gardener/gardener-landscape-kit/pkg/utils/meta" ) @@ -68,7 +69,7 @@ var _ = Describe("YAML Preprocessing", func() { Expect(err).ToNot(HaveOccurred()) Expect(fs.WriteFile("/landscape/manifest/test.yaml", testFile, 0600)).To(Succeed()) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": {}}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": {}}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/manifest/test.yaml") Expect(err).ToNot(HaveOccurred()) @@ -82,7 +83,7 @@ var _ = Describe("YAML Preprocessing", func() { Expect(err).ToNot(HaveOccurred()) Expect(fs.WriteFile("/landscape/manifest/test.yaml", testFile, 0600)).To(Succeed()) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": {}}, "/landscape", "manifest", fs)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": {}}, "/landscape", "manifest", fs, configv1alpha1.MergeModeSilent)).To(Succeed()) content, err := fs.ReadFile("/landscape/manifest/test.yaml") Expect(err).ToNot(HaveOccurred()) From a513021283c07761eea041b78261bf1a7b51cb31 Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Wed, 1 Apr 2026 13:39:24 +0200 Subject: [PATCH 5/8] Update all `WriteObjectsToFilesystem` and `ThreeWayMergeManifest` call-sites Existing callers pass `MergeModeSilent` (zero value, no behaviour change). plain.go passes `MergeModeInformative` so the components.yaml file gets GLK default annotations for operator-overridden component versions. --- hack/tools/prettify/main.go | 3 ++- pkg/components/flux/component.go | 2 +- .../networking-calico/component.go | 4 ++-- .../networking-cilium/component.go | 4 ++-- .../os-gardenlinux/component.go | 4 ++-- .../os-suse-chost/component.go | 4 ++-- .../provider-alicloud/component.go | 4 ++-- .../gardener-extensions/provider-aws/component.go | 4 ++-- .../provider-azure/component.go | 4 ++-- .../gardener-extensions/provider-gcp/component.go | 4 ++-- .../provider-openstack/component.go | 4 ++-- .../runtime-gvisor/component.go | 4 ++-- .../shoot-cert-service/component.go | 4 ++-- .../shoot-dns-service/component.go | 4 ++-- .../shoot-networking-problemdetector/component.go | 4 ++-- .../shoot-oidc-service/component.go | 4 ++-- pkg/components/gardener/garden/component.go | 4 ++-- pkg/components/gardener/operator/component.go | 4 ++-- .../gardener/virtual-garden-access/component.go | 4 ++-- .../virtual-garden/garden-config/component.go | 4 ++-- pkg/utils/kustomization/kustomization.go | 15 ++++++++------- pkg/utils/kustomization/kustomization_test.go | 3 ++- 22 files changed, 49 insertions(+), 46 deletions(-) diff --git a/hack/tools/prettify/main.go b/hack/tools/prettify/main.go index dbd33c7..1b27837 100644 --- a/hack/tools/prettify/main.go +++ b/hack/tools/prettify/main.go @@ -11,6 +11,7 @@ import ( flag "github.com/spf13/pflag" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/utils/meta" ) @@ -31,7 +32,7 @@ func main() { if err != nil { log.Fatalf("Error reading file: %s", err) } - prettified, err := meta.ThreeWayMergeManifest(nil, content, nil) + prettified, err := meta.ThreeWayMergeManifest(nil, content, nil, configv1alpha1.MergeModeSilent) if err != nil { log.Fatalf("Marshalling failed: %s", err) } diff --git a/pkg/components/flux/component.go b/pkg/components/flux/component.go index 6a47c9a..54b2e4f 100644 --- a/pkg/components/flux/component.go +++ b/pkg/components/flux/component.go @@ -94,7 +94,7 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { delete(objects, "flux-system/gitignore") delete(objects, "flux-system/doc.go") - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), DirName, opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), DirName, opts.GetFilesystem(), opts.GetMergeMode()) } func writeGitignoreFile(options components.LandscapeOptions) error { diff --git a/pkg/components/gardener-extensions/networking-calico/component.go b/pkg/components/gardener-extensions/networking-calico/component.go index c297146..eeb3d19 100644 --- a/pkg/components/gardener-extensions/networking-calico/component.go +++ b/pkg/components/gardener-extensions/networking-calico/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/networking-cilium/component.go b/pkg/components/gardener-extensions/networking-cilium/component.go index b934ce4..e39d969 100644 --- a/pkg/components/gardener-extensions/networking-cilium/component.go +++ b/pkg/components/gardener-extensions/networking-cilium/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/os-gardenlinux/component.go b/pkg/components/gardener-extensions/os-gardenlinux/component.go index 2cc5eb0..0ee9de5 100644 --- a/pkg/components/gardener-extensions/os-gardenlinux/component.go +++ b/pkg/components/gardener-extensions/os-gardenlinux/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/os-suse-chost/component.go b/pkg/components/gardener-extensions/os-suse-chost/component.go index 3e93384..2c7be6a 100644 --- a/pkg/components/gardener-extensions/os-suse-chost/component.go +++ b/pkg/components/gardener-extensions/os-suse-chost/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/provider-alicloud/component.go b/pkg/components/gardener-extensions/provider-alicloud/component.go index b703f15..3870483 100644 --- a/pkg/components/gardener-extensions/provider-alicloud/component.go +++ b/pkg/components/gardener-extensions/provider-alicloud/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/provider-aws/component.go b/pkg/components/gardener-extensions/provider-aws/component.go index 76e3408..2d65d34 100644 --- a/pkg/components/gardener-extensions/provider-aws/component.go +++ b/pkg/components/gardener-extensions/provider-aws/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/provider-azure/component.go b/pkg/components/gardener-extensions/provider-azure/component.go index 6bbb7f7..2b75ca2 100644 --- a/pkg/components/gardener-extensions/provider-azure/component.go +++ b/pkg/components/gardener-extensions/provider-azure/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/provider-gcp/component.go b/pkg/components/gardener-extensions/provider-gcp/component.go index dbbd327..c050ecf 100644 --- a/pkg/components/gardener-extensions/provider-gcp/component.go +++ b/pkg/components/gardener-extensions/provider-gcp/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/provider-openstack/component.go b/pkg/components/gardener-extensions/provider-openstack/component.go index 6ffc8e8..071b468 100644 --- a/pkg/components/gardener-extensions/provider-openstack/component.go +++ b/pkg/components/gardener-extensions/provider-openstack/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/runtime-gvisor/component.go b/pkg/components/gardener-extensions/runtime-gvisor/component.go index 3a6e0cb..c095911 100644 --- a/pkg/components/gardener-extensions/runtime-gvisor/component.go +++ b/pkg/components/gardener-extensions/runtime-gvisor/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/shoot-cert-service/component.go b/pkg/components/gardener-extensions/shoot-cert-service/component.go index b8c1c6b..0364518 100644 --- a/pkg/components/gardener-extensions/shoot-cert-service/component.go +++ b/pkg/components/gardener-extensions/shoot-cert-service/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/shoot-dns-service/component.go b/pkg/components/gardener-extensions/shoot-dns-service/component.go index b452074..c0a87bd 100644 --- a/pkg/components/gardener-extensions/shoot-dns-service/component.go +++ b/pkg/components/gardener-extensions/shoot-dns-service/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/shoot-networking-problemdetector/component.go b/pkg/components/gardener-extensions/shoot-networking-problemdetector/component.go index ab35beb..9053be4 100644 --- a/pkg/components/gardener-extensions/shoot-networking-problemdetector/component.go +++ b/pkg/components/gardener-extensions/shoot-networking-problemdetector/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener-extensions/shoot-oidc-service/component.go b/pkg/components/gardener-extensions/shoot-oidc-service/component.go index 9e8d9dc..9dbcd84 100644 --- a/pkg/components/gardener-extensions/shoot-oidc-service/component.go +++ b/pkg/components/gardener-extensions/shoot-oidc-service/component.go @@ -78,7 +78,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -100,5 +100,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener/garden/component.go b/pkg/components/gardener/garden/component.go index eefa0f2..5cbc3c3 100644 --- a/pkg/components/gardener/garden/component.go +++ b/pkg/components/gardener/garden/component.go @@ -71,7 +71,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -88,5 +88,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/gardener/operator/component.go b/pkg/components/gardener/operator/component.go index 5873ca7..0221ed8 100644 --- a/pkg/components/gardener/operator/component.go +++ b/pkg/components/gardener/operator/component.go @@ -80,7 +80,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func getTemplateValues(opts components.Options) (map[string]any, error) { @@ -134,7 +134,7 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func getOCIImageReferenceFromComponentVector(name string, cv *utilscomponentvector.ComponentVector) (string, error) { diff --git a/pkg/components/gardener/virtual-garden-access/component.go b/pkg/components/gardener/virtual-garden-access/component.go index 513753c..81dc414 100644 --- a/pkg/components/gardener/virtual-garden-access/component.go +++ b/pkg/components/gardener/virtual-garden-access/component.go @@ -73,7 +73,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -90,5 +90,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/components/virtual-garden/garden-config/component.go b/pkg/components/virtual-garden/garden-config/component.go index 56227b0..1104f31 100644 --- a/pkg/components/virtual-garden/garden-config/component.go +++ b/pkg/components/virtual-garden/garden-config/component.go @@ -73,7 +73,7 @@ func writeBaseTemplateFiles(opts components.Options) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { @@ -90,5 +90,5 @@ func writeLandscapeTemplateFiles(opts components.LandscapeOptions) error { return err } - return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem()) + return files.WriteObjectsToFilesystem(objects, opts.GetTargetPath(), path.Join(components.DirName, ComponentDirectory), opts.GetFilesystem(), opts.GetMergeMode()) } diff --git a/pkg/utils/kustomization/kustomization.go b/pkg/utils/kustomization/kustomization.go index 01e1123..6ea15ad 100644 --- a/pkg/utils/kustomization/kustomization.go +++ b/pkg/utils/kustomization/kustomization.go @@ -16,6 +16,7 @@ import ( kustomize "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/components" "github.com/gardener/gardener-landscape-kit/pkg/utils/files" ) @@ -51,14 +52,14 @@ func NewKustomization(resources []string, patches []kustomize.Patch) *kustomize. // WriteKustomizationComponent writes the objects and a Kustomization file to the fs. // The Kustomization file references all other objects. // The objects map will be modified to include the Kustomization file. -func WriteKustomizationComponent(objects map[string][]byte, baseDir, componentDir string, fs afero.Afero) error { +func WriteKustomizationComponent(objects map[string][]byte, baseDir, componentDir string, fs afero.Afero, mode configv1alpha1.MergeMode) error { kustomization := NewKustomization(slices.Collect(maps.Keys(objects)), nil) content, err := yaml.Marshal(kustomization) if err != nil { return err } objects[KustomizationFileName] = content - return files.WriteObjectsToFilesystem(objects, baseDir, componentDir, fs) + return files.WriteObjectsToFilesystem(objects, baseDir, componentDir, fs, mode) } // WriteLandscapeComponentsKustomizations traverses through the generated components directory and adds @@ -68,10 +69,10 @@ func WriteLandscapeComponentsKustomizations(options components.Options) error { targetDir := options.GetTargetPath() componentsDir := filepath.Join(targetDir, components.DirName) - return fs.Walk(componentsDir, writeKustomizationsToFileTree(fs, targetDir)) + return fs.Walk(componentsDir, writeKustomizationsToFileTree(fs, targetDir, options.GetMergeMode())) } -func writeKustomizationsToFileTree(fs afero.Afero, targetDir string) func(dir string, info os.FileInfo, err error) error { +func writeKustomizationsToFileTree(fs afero.Afero, targetDir string, mode configv1alpha1.MergeMode) func(dir string, info os.FileInfo, err error) error { var completedPaths []string return func(dir string, info os.FileInfo, err error) error { @@ -115,11 +116,11 @@ func writeKustomizationsToFileTree(fs afero.Afero, targetDir string) func(dir st } relativePath, _ := strings.CutPrefix(dir, targetDir) - return writeKustomizationFile(fs, targetDir, relativePath, directories) + return writeKustomizationFile(fs, targetDir, relativePath, directories, mode) } } -func writeKustomizationFile(fs afero.Afero, landscapeDir, relativePath string, directories []string) error { +func writeKustomizationFile(fs afero.Afero, landscapeDir, relativePath string, directories []string, mode configv1alpha1.MergeMode) error { var ( err error objects = make(map[string][]byte) @@ -132,5 +133,5 @@ func writeKustomizationFile(fs afero.Afero, landscapeDir, relativePath string, d objects[KustomizationFileName] = append([]byte(autoGenerationNotice), objects[KustomizationFileName]...) - return files.WriteObjectsToFilesystem(objects, landscapeDir, relativePath, fs) + return files.WriteObjectsToFilesystem(objects, landscapeDir, relativePath, fs, mode) } diff --git a/pkg/utils/kustomization/kustomization_test.go b/pkg/utils/kustomization/kustomization_test.go index 909fe1f..ff1815a 100644 --- a/pkg/utils/kustomization/kustomization_test.go +++ b/pkg/utils/kustomization/kustomization_test.go @@ -16,6 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" + configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-landscape-kit/pkg/cmd" generateoptions "github.com/gardener/gardener-landscape-kit/pkg/cmd/generate/options" "github.com/gardener/gardener-landscape-kit/pkg/components" @@ -59,7 +60,7 @@ var _ = Describe("Kustomization", func() { } ) - Expect(WriteKustomizationComponent(objects, landscapeDir, componentDir, fs)).To(Succeed()) + Expect(WriteKustomizationComponent(objects, landscapeDir, componentDir, fs, configv1alpha1.MergeModeSilent)).To(Succeed()) contents, err := fs.ReadFile(filepath.Join(landscapeDir, componentDir, "configmap.yaml")) Expect(err).NotTo(HaveOccurred()) From 3ddf8fb15688e6d2f53bc71c7a7eaa97920ede0c Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Fri, 10 Apr 2026 17:12:15 +0200 Subject: [PATCH 6/8] Address review comments --- docs/api-reference/landscapekit-v1alpha1.md | 17 ++ example/20-componentconfig-glk.yaml | 1 + pkg/apis/config/v1alpha1/types.go | 18 +- .../config/v1alpha1/validation/validation.go | 2 +- .../v1alpha1/validation/validation_test.go | 2 +- pkg/components/types.go | 2 +- pkg/utils/files/writer.go | 28 ---- pkg/utils/files/writer_test.go | 158 +++++++++++++----- pkg/utils/meta/diff_test.go | 5 +- pkg/utils/meta/merge.go | 88 ++++++++-- 10 files changed, 219 insertions(+), 102 deletions(-) diff --git a/docs/api-reference/landscapekit-v1alpha1.md b/docs/api-reference/landscapekit-v1alpha1.md index 0e58c23..6e87bae 100644 --- a/docs/api-reference/landscapekit-v1alpha1.md +++ b/docs/api-reference/landscapekit-v1alpha1.md @@ -81,6 +81,23 @@ _Appears in:_ +#### MergeMode + +_Underlying type:_ _string_ + +MergeMode controls how operator overwrites are handled during three-way merge. + + + +_Appears in:_ +- [LandscapeKitConfiguration](#landscapekitconfiguration) + +| Field | Description | +| --- | --- | +| `Informative` | MergeModeInformative annotates operator-overwritten values with a comment showing the current GLK default.
| +| `Silent` | MergeModeSilent retains operator overwrites without annotation.
| + + #### OCMComponent diff --git a/example/20-componentconfig-glk.yaml b/example/20-componentconfig-glk.yaml index 2eab32f..8cc3905 100644 --- a/example/20-componentconfig-glk.yaml +++ b/example/20-componentconfig-glk.yaml @@ -19,3 +19,4 @@ kind: LandscapeKitConfiguration # - component-name # versionConfig: # defaultVersionsUpdateStrategy: ReleaseBranch +# mergeMode: Informative diff --git a/pkg/apis/config/v1alpha1/types.go b/pkg/apis/config/v1alpha1/types.go index 1a79b16..076b929 100644 --- a/pkg/apis/config/v1alpha1/types.go +++ b/pkg/apis/config/v1alpha1/types.go @@ -26,8 +26,9 @@ type LandscapeKitConfiguration struct { // VersionConfig is the configuration for versioning. // +optional VersionConfig *VersionConfiguration `json:"versionConfig,omitempty"` - // MergeMode controls how operator overrides conflicting with updated GLK defaults are handled during three-way merge. - // Possible values are "Informative" (default) and "Silent". + // MergeMode determines how merge conflicts are resolved: + // - "Informative" (default): New default values from GLK are added as comments after any customized values. + // - "Silent": Operator-customized values are retained, new default values are omitted. // +optional MergeMode *MergeMode `json:"mergeMode,omitempty"` } @@ -124,13 +125,13 @@ type VersionConfiguration struct { DefaultVersionsUpdateStrategy *DefaultVersionsUpdateStrategy `json:"defaultVersionsUpdateStrategy,omitempty"` } -// MergeMode controls how operator overrides are handled during three-way merge. +// MergeMode controls how operator overwrites are handled during three-way merge. type MergeMode string const ( - // MergeModeInformative annotates operator-overridden values with a comment showing the current GLK default. + // MergeModeInformative annotates operator-overwritten values with a comment showing the current GLK default. MergeModeInformative MergeMode = "Informative" - // MergeModeSilent retains operator overrides without annotation. + // MergeModeSilent retains operator overwrites without annotation. MergeModeSilent MergeMode = "Silent" ) @@ -141,10 +142,9 @@ var AllowedMergeModes = []string{ } // GetMergeMode returns the configured MergeMode, defaulting to MergeModeInformative. -// It is safe to call on a nil receiver. func (c *LandscapeKitConfiguration) GetMergeMode() MergeMode { - if c != nil && c.MergeMode != nil { - return *c.MergeMode + if c == nil || c.MergeMode == nil { + return MergeModeInformative } - return MergeModeInformative + return *c.MergeMode } diff --git a/pkg/apis/config/v1alpha1/validation/validation.go b/pkg/apis/config/v1alpha1/validation/validation.go index c0567e9..704cee8 100644 --- a/pkg/apis/config/v1alpha1/validation/validation.go +++ b/pkg/apis/config/v1alpha1/validation/validation.go @@ -37,7 +37,7 @@ func ValidateLandscapeKitConfiguration(conf *configv1alpha1.LandscapeKitConfigur } if conf.MergeMode != nil && !slices.Contains(configv1alpha1.AllowedMergeModes, string(*conf.MergeMode)) { - allErrs = append(allErrs, field.Invalid(field.NewPath("mergeMode"), *conf.MergeMode, "allowed values are: "+strings.Join(configv1alpha1.AllowedMergeModes, ", "))) + allErrs = append(allErrs, field.NotSupported(field.NewPath("mergeMode"), *conf.MergeMode, configv1alpha1.AllowedMergeModes)) } return allErrs diff --git a/pkg/apis/config/v1alpha1/validation/validation_test.go b/pkg/apis/config/v1alpha1/validation/validation_test.go index 5ccd0e4..39be407 100644 --- a/pkg/apis/config/v1alpha1/validation/validation_test.go +++ b/pkg/apis/config/v1alpha1/validation/validation_test.go @@ -324,7 +324,7 @@ var _ = Describe("Validation", func() { errList := validation.ValidateLandscapeKitConfiguration(conf) Expect(errList).To(ConsistOf( PointTo(MatchFields(IgnoreExtras, Fields{ - "Type": Equal(field.ErrorTypeInvalid), + "Type": Equal(field.ErrorTypeNotSupported), "Field": Equal("mergeMode"), "BadValue": Equal(invalid), })), diff --git a/pkg/components/types.go b/pkg/components/types.go index 0b0c22a..17cdf45 100644 --- a/pkg/components/types.go +++ b/pkg/components/types.go @@ -36,7 +36,7 @@ type Options interface { GetFilesystem() afero.Afero // GetLogger returns the logger instance. GetLogger() logr.Logger - // GetMergeMode returns the configured merge mode for three-way merges. + // GetMergeMode returns the configured mode to solve merge conflicts. GetMergeMode() configv1alpha1.MergeMode } diff --git a/pkg/utils/files/writer.go b/pkg/utils/files/writer.go index e9f7930..755345f 100644 --- a/pkg/utils/files/writer.go +++ b/pkg/utils/files/writer.go @@ -10,7 +10,6 @@ import ( "os" "path" "path/filepath" - "strings" "github.com/spf13/afero" @@ -46,30 +45,6 @@ func isSecret(contents []byte) bool { return bytes.HasPrefix(contents, []byte(kindSecret)) || bytes.Contains(contents, []byte("\n"+kindSecret)) } -// stripGLKManagedAnnotations removes GLK-managed annotation comments from YAML bytes. -// A line is annotated when it contains both GLKDefaultPrefix and GLKManagedMarker. -func stripGLKManagedAnnotations(data []byte) []byte { - lines := strings.Split(string(data), "\n") - out := make([]string, 0, len(lines)) - for _, line := range lines { - if !strings.Contains(line, meta.GLKManagedMarker) { - out = append(out, line) - continue - } - // Strip from the start of the GLK annotation prefix. - if idx := strings.Index(line, meta.GLKDefaultPrefix); idx >= 0 { - stripped := strings.TrimRight(line[:idx], " \t") - // If the entire line was a head comment annotation, drop the line entirely. - if strings.TrimSpace(stripped) == "" { - continue - } - out = append(out, stripped) - } - // If for some reason the marker is present but the prefix is not, drop the line. - } - return []byte(strings.Join(out, "\n")) -} - // WriteObjectsToFilesystem writes the given objects to the filesystem at the specified rootDir and relativeFilePath. // If the manifest file already exists, it patches changes from the new default. // Additionally, it maintains a default version of the manifest in a separate directory for future diff checks. @@ -107,9 +82,6 @@ func WriteObjectsToFilesystem(objects map[string][]byte, rootDir, relativeFilePa object = append([]byte(secretEncryptionDisclaimer), object...) } - // Strip GLK-managed annotations from the current file before merging, so they are always re-evaluated (idempotency: the annotation reflects the *current* GLK default). - currentYaml = stripGLKManagedAnnotations(currentYaml) - output, err := meta.ThreeWayMergeManifest(oldDefaultYaml, object, currentYaml, mode) if err != nil { return err diff --git a/pkg/utils/files/writer_test.go b/pkg/utils/files/writer_test.go index 547682d..6475d64 100644 --- a/pkg/utils/files/writer_test.go +++ b/pkg/utils/files/writer_test.go @@ -140,22 +140,20 @@ spec: Not(ContainSubstring(`# SECURITY ADVISORY`)), )) }) - }) - Describe("#WriteObjectsToFilesystem - MergeModeInformative", func() { - It("should annotate operator-overridden scalar values with the GLK default and preserve user comments idempotently", func() { - initial := []byte(`apiVersion: v1 + DescribeTable("should annotate operator-overwritten values only in Informative mode", + func(mode configv1alpha1.MergeMode, expectAnnotation bool) { + initial := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test data: version: v1.0.0 `) - // First generate: establish defaults - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "manifest", fs, mode)).To(Succeed()) - // Operator pins to a custom version with a comment explaining why - Expect(fs.WriteFile("/landscape/manifest/test.yaml", []byte(`apiVersion: v1 + // Operator pins to a custom version with a comment explaining why + Expect(fs.WriteFile("/landscape/manifest/test.yaml", []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test @@ -163,46 +161,113 @@ data: version: v1.0.5 # pinned for production `), 0600)).To(Succeed()) - // GLK ships a new default with a newer version - updated := []byte(`apiVersion: v1 + // GLK ships a new default with a newer version + updated := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test data: version: v1.1.0 `) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, mode)).To(Succeed()) + + content, err := fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("version: v1.0.5")) + Expect(string(content)).To(ContainSubstring("pinned for production")) + + if expectAnnotation { + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.1.0")) + + // Re-run with the same default — annotation persists because the user did not remove it. + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, mode)).To(Succeed()) + content2, err := fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content2)).To(Equal(string(content))) + } else { + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix)) + } + }, + Entry("Silent", configv1alpha1.MergeModeSilent, false), + Entry("Informative", configv1alpha1.MergeModeInformative, true), + ) - content, err := fs.ReadFile("/landscape/manifest/test.yaml") - Expect(err).NotTo(HaveOccurred()) - // Operator's override is preserved - Expect(string(content)).To(ContainSubstring("version: v1.0.5")) - // User comment is preserved - Expect(string(content)).To(ContainSubstring("pinned for production")) - // GLK default annotation is added - Expect(string(content)).To(ContainSubstring("# glk default: v1.1.0")) - Expect(string(content)).To(ContainSubstring(meta.GLKManagedMarker)) - - // Re-run with the same inputs — annotation and user comment must not be doubled - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) - - content2, err := fs.ReadFile("/landscape/manifest/test.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(string(content2)).To(Equal(string(content))) - }) + Context("MergeMode Informative", func() { + It("should not re-add the annotation after the user removed it, until the default changes again", func() { + initial := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + // Operator pins to v1.0.5 + Expect(fs.WriteFile("/landscape/manifest/test.yaml", []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 +`), 0600)).To(Succeed()) + + // GLK ships v1.1.0 — annotation appears + v110 := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.1.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v110}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err := fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix)) + + // User acknowledges the annotation and removes it manually + Expect(fs.WriteFile("/landscape/manifest/test.yaml", []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 +`), 0600)).To(Succeed()) + + // Re-run with the same default — annotation stays removed + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v110}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("version: v1.0.5")) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix)) - It("should remove the annotation entirely when the GLK default reverts to the operator's value", func() { - initial := []byte(`apiVersion: v1 + // GLK ships v1.2.0 — annotation re-appears because the default changed + v120 := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.2.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v120}, "/landscape", "manifest", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/manifest/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("version: v1.0.5")) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.2.0")) + }) + + It("should remove the annotation entirely when the GLK default reverts to the operator's value", func() { + initial := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test data: version: v1.0.0 `) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) - // Operator pins to v1.0.5 - Expect(fs.WriteFile("/landscape/revert/test.yaml", []byte(`apiVersion: v1 + // Operator pins to v1.0.5 + Expect(fs.WriteFile("/landscape/revert/test.yaml", []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test @@ -210,32 +275,33 @@ data: version: v1.0.5 `), 0600)).To(Succeed()) - // GLK ships v1.1.0 — annotation appears - updated := []byte(`apiVersion: v1 + // GLK ships v1.1.0 — annotation appears + updated := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test data: version: v1.1.0 `) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) - content, err := fs.ReadFile("/landscape/revert/test.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(string(content)).To(ContainSubstring(meta.GLKManagedMarker)) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": updated}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err := fs.ReadFile("/landscape/revert/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix)) - // GLK reverts to v1.0.5 — operator's value now matches the default, no annotation - reverted := []byte(`apiVersion: v1 + // GLK reverts to v1.0.5 — operator's value now matches the default, no annotation + reverted := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: test data: version: v1.0.5 `) - Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": reverted}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) - content, err = fs.ReadFile("/landscape/revert/test.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(string(content)).NotTo(ContainSubstring(meta.GLKManagedMarker)) - Expect(string(content)).NotTo(ContainSubstring("# glk default:")) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": reverted}, "/landscape", "revert", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/revert/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix)) + Expect(string(content)).NotTo(ContainSubstring("# Attention - new default:")) + }) }) }) diff --git a/pkg/utils/meta/diff_test.go b/pkg/utils/meta/diff_test.go index 29b766b..8fb8c73 100644 --- a/pkg/utils/meta/diff_test.go +++ b/pkg/utils/meta/diff_test.go @@ -277,14 +277,13 @@ data: result, err := meta.ThreeWayMergeManifest(oldDefault, newDefault, current, configv1alpha1.MergeModeInformative) Expect(err).NotTo(HaveOccurred()) Expect(string(result)).To(ContainSubstring("version: v1.0.5")) - Expect(string(result)).To(ContainSubstring("# glk default: v1.1.0")) - Expect(string(result)).To(ContainSubstring(meta.GLKManagedMarker)) + Expect(string(result)).To(ContainSubstring("# Attention - new default: v1.1.0")) // No conflict: operator did not change the value → new default taken silently, no annotation result, err = meta.ThreeWayMergeManifest(oldDefault, newDefault, oldDefault, configv1alpha1.MergeModeInformative) Expect(err).NotTo(HaveOccurred()) Expect(string(result)).To(ContainSubstring("version: v1.1.0")) - Expect(string(result)).NotTo(ContainSubstring(meta.GLKManagedMarker)) + Expect(string(result)).NotTo(ContainSubstring(meta.GLKDefaultPrefix)) }) }) }) diff --git a/pkg/utils/meta/merge.go b/pkg/utils/meta/merge.go index 18d4757..189cc66 100644 --- a/pkg/utils/meta/merge.go +++ b/pkg/utils/meta/merge.go @@ -5,21 +5,54 @@ package meta import ( + "strings" + "go.yaml.in/yaml/v4" configv1alpha1 "github.com/gardener/gardener-landscape-kit/pkg/apis/config/v1alpha1" ) const ( - // GLKManagedMarker is the suffix that marks a comment as GLK-managed. - // It is used to strip GLK-owned annotations before re-running the merge (idempotency). - GLKManagedMarker = "# <-- glk-managed" - // GLKDefaultPrefix is the comment prefix for GLK-managed default annotations. // It is exported so callers can use it as the strip anchor when removing annotations. - GLKDefaultPrefix = "# glk default: " + GLKDefaultPrefix = "# Attention - new default: " ) +// stripGLKAnnotation removes a GLK-managed annotation from a single comment string. +// If the annotation is part of a multi-line comment, only the annotation line is removed. +// If the annotation is appended to a value's line comment (e.g. "# user note # Attention …"), the prefix and everything after it is stripped. +func stripGLKAnnotation(comment string) string { + if !strings.Contains(comment, GLKDefaultPrefix) { + return comment + } + lines := strings.Split(comment, "\n") + out := make([]string, 0, len(lines)) + for _, line := range lines { + idx := strings.Index(line, GLKDefaultPrefix) + if idx < 0 { + out = append(out, line) + continue + } + stripped := strings.TrimRight(line[:idx], " \t") + if stripped != "" { + out = append(out, stripped) + } + } + return strings.Join(out, "\n") +} + +// stripGLKAnnotations removes GLK-managed annotations from all comment fields of a node. +func stripGLKAnnotations(nodes ...*yaml.Node) { + for _, n := range nodes { + if n == nil { + continue + } + n.HeadComment = stripGLKAnnotation(n.HeadComment) + n.LineComment = stripGLKAnnotation(n.LineComment) + n.FootComment = stripGLKAnnotation(n.FootComment) + } +} + // threeWayMergeSection performs a three-way merge on a single YAML section func threeWayMergeSection(oldDefaultYaml, newDefaultYaml, currentYaml []byte, mode configv1alpha1.MergeMode) ([]byte, error) { // Parse all three versions @@ -127,16 +160,21 @@ func threeWayMerge(oldDefault, newDefault, current *yaml.Node, mode configv1alph // Both default and current changed: keep current (user's value wins). resultValue = currentValue mergeNodeComments(oldValue, newValueNode, resultValue) - if mode == configv1alpha1.MergeModeInformative && !nodesEqual(newValueNode, currentValue, false) { - annotateConflict(resultKeyNode, resultValue, newValueNode) + if mode == configv1alpha1.MergeModeInformative { + if !nodesEqual(newValueNode, currentValue, false) { + annotateConflict(resultKeyNode, resultValue, newValueNode) + } else { + // Values converged — strip any lingering GLK annotation. + stripGLKAnnotations(resultKeyNode, resultValue) + } } default: resultValue = currentValue if oldExists { mergeNodeComments(oldValue, newValueNode, resultValue) - if mode == configv1alpha1.MergeModeInformative && !nodesEqual(newValueNode, currentValue, false) { - // The operator's value differs from the current GLK default: annotate it. - annotateConflict(resultKeyNode, resultValue, newValueNode) + if mode == configv1alpha1.MergeModeInformative && nodesEqual(newValueNode, currentValue, false) { + // Values converged — strip any lingering GLK annotation. + stripGLKAnnotations(resultKeyNode, resultValue) } } } @@ -183,12 +221,12 @@ func annotateConflict(resultKeyNode, resultValue, newDefaultNode *yaml.Node) { // glkManagedLineComment returns a GLK-managed line comment for a scalar value conflict. func glkManagedLineComment(newValue string) string { - return GLKDefaultPrefix + newValue + " " + GLKManagedMarker + return GLKDefaultPrefix + newValue } // glkManagedHeadComment returns a GLK-managed head comment for a complex node conflict. func glkManagedHeadComment(existingHead string) string { - annotation := GLKDefaultPrefix + "(complex node changed) " + GLKManagedMarker + annotation := GLKDefaultPrefix + "(complex node changed)" if existingHead == "" { return annotation } @@ -306,7 +344,19 @@ func threeWayMergeSequence(oldDefault, newDefault, current *yaml.Node, mode conf return result } - // No identity key: fall back to full-string set-based merge. + // No identity key found. + // When all three sequences have the same length and all items are mappings, + // match items by position and three-way merge each pair. This handles cases + // where list items are modified but their count and order stay the same + // (e.g., a single scalar field in a mapping item is changed). + if len(oldDefault.Content) == len(newDefault.Content) && len(newDefault.Content) == len(current.Content) && allMappings(oldDefault, newDefault, current) { + for i := range current.Content { + result.Content = append(result.Content, threeWayMerge(oldDefault.Content[i], newDefault.Content[i], current.Content[i], mode)) + } + return result + } + + // Fall back to full-string set-based merge. oldSet := make(map[string]bool) for _, item := range oldDefault.Content { oldSet[nodeToString(item)] = true @@ -398,3 +448,15 @@ func mappingValue(node *yaml.Node, key string) string { } return "" } + +// allMappings reports whether every item in each of the given sequences is a mapping node. +func allMappings(seqs ...*yaml.Node) bool { + for _, seq := range seqs { + for _, item := range seq.Content { + if item.Kind != yaml.MappingNode { + return false + } + } + } + return true +} From 3963393b2305a8a71603345bb5c1bad3e786ae8c Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Tue, 14 Apr 2026 11:29:21 +0200 Subject: [PATCH 7/8] Strip any pre-existing GLK annotation before appending the new one --- pkg/utils/files/writer_test.go | 64 ++++++++++++++++++++++++++++++++++ pkg/utils/meta/merge.go | 14 +++++--- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/pkg/utils/files/writer_test.go b/pkg/utils/files/writer_test.go index 6475d64..65cf55c 100644 --- a/pkg/utils/files/writer_test.go +++ b/pkg/utils/files/writer_test.go @@ -256,6 +256,70 @@ data: Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.2.0")) }) + It("should replace the annotation when the GLK default changes again instead of accumulating", func() { + initial := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": initial}, "/landscape", "accum", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + + // Operator pins to v1.0.5 + Expect(fs.WriteFile("/landscape/accum/test.yaml", []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.0.5 # pinned for production +`), 0600)).To(Succeed()) + + // GLK ships v1.1.0 — annotation appears + v110 := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.1.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v110}, "/landscape", "accum", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err := fs.ReadFile("/landscape/accum/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.1.0")) + + // GLK ships v1.2.0 — annotation is replaced, not accumulated + v120 := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.2.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v120}, "/landscape", "accum", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/accum/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("pinned for production")) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.2.0")) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix + "v1.1.0")) + + // GLK ships v1.3.0 — again replaced, never more than one annotation + v130 := []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + version: v1.3.0 +`) + Expect(files.WriteObjectsToFilesystem(map[string][]byte{"test.yaml": v130}, "/landscape", "accum", fs, configv1alpha1.MergeModeInformative)).To(Succeed()) + content, err = fs.ReadFile("/landscape/accum/test.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("pinned for production")) + Expect(string(content)).To(ContainSubstring(meta.GLKDefaultPrefix + "v1.3.0")) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix + "v1.2.0")) + Expect(string(content)).NotTo(ContainSubstring(meta.GLKDefaultPrefix + "v1.1.0")) + }) + It("should remove the annotation entirely when the GLK default reverts to the operator's value", func() { initial := []byte(`apiVersion: v1 kind: ConfigMap diff --git a/pkg/utils/meta/merge.go b/pkg/utils/meta/merge.go index 189cc66..d188642 100644 --- a/pkg/utils/meta/merge.go +++ b/pkg/utils/meta/merge.go @@ -28,12 +28,12 @@ func stripGLKAnnotation(comment string) string { lines := strings.Split(comment, "\n") out := make([]string, 0, len(lines)) for _, line := range lines { - idx := strings.Index(line, GLKDefaultPrefix) - if idx < 0 { + before, _, found := strings.Cut(line, GLKDefaultPrefix) + if !found { out = append(out, line) continue } - stripped := strings.TrimRight(line[:idx], " \t") + stripped := strings.TrimRight(before, " \t") if stripped != "" { out = append(out, stripped) } @@ -209,12 +209,16 @@ func annotateConflict(resultKeyNode, resultValue, newDefaultNode *yaml.Node) { switch newDefaultNode.Kind { case yaml.ScalarNode: annotation := glkManagedLineComment(newDefaultNode.Value) - if resultValue.LineComment != "" { - resultValue.LineComment = resultValue.LineComment + " " + annotation + // Strip any pre-existing GLK annotation before appending the current one, so repeated runs replace rather than accumulate the comment. + stripped := stripGLKAnnotation(resultValue.LineComment) + if stripped != "" { + resultValue.LineComment = stripped + " " + annotation } else { resultValue.LineComment = annotation } default: + // Strip existing GLK annotation from head comment before re-annotating. + resultKeyNode.HeadComment = stripGLKAnnotation(resultKeyNode.HeadComment) resultKeyNode.HeadComment = glkManagedHeadComment(resultKeyNode.HeadComment) } } From 38709afad4293f37b191d90b193d4532c495527c Mon Sep 17 00:00:00 2001 From: Luca Bernstein Date: Tue, 14 Apr 2026 17:01:52 +0200 Subject: [PATCH 8/8] Add defaults for LandscapeKitConfig --- pkg/apis/config/v1alpha1/defaults.go | 14 +++++++++++++ pkg/apis/config/v1alpha1/types.go | 8 ------- .../config/v1alpha1/zz_generated.defaults.go | 5 +++++ pkg/cmd/resolve/plain/plain.go | 4 ++-- pkg/components/flux/component_test.go | 19 ++++++++++------- .../networking-calico/component_test.go | 3 +++ .../networking-cilium/component_test.go | 3 +++ .../os-gardenlinux/component_test.go | 3 +++ .../os-suse-chost/component_test.go | 3 +++ .../provider-alicloud/component_test.go | 3 +++ .../provider-aws/component_test.go | 3 +++ .../provider-azure/component_test.go | 3 +++ .../provider-gcp/component_test.go | 3 +++ .../provider-openstack/component_test.go | 3 +++ .../runtime-gvisor/component_test.go | 3 +++ .../shoot-cert-service/component_test.go | 3 +++ .../shoot-dns-service/component_test.go | 3 +++ .../component_test.go | 3 +++ .../shoot-oidc-service/component_test.go | 3 +++ .../gardener/garden/component_test.go | 3 +++ .../gardener/operator/component_test.go | 3 +++ .../virtual-garden-access/component_test.go | 3 +++ pkg/components/types.go | 2 +- pkg/components/types_test.go | 21 +++++++------------ .../garden-config/component_test.go | 3 +++ pkg/registry/registry_test.go | 6 +++++- pkg/utils/kustomization/kustomization_test.go | 4 ++++ pkg/utils/test/kustomize.go | 1 + 28 files changed, 105 insertions(+), 33 deletions(-) diff --git a/pkg/apis/config/v1alpha1/defaults.go b/pkg/apis/config/v1alpha1/defaults.go index 9c3a743..55c6c64 100644 --- a/pkg/apis/config/v1alpha1/defaults.go +++ b/pkg/apis/config/v1alpha1/defaults.go @@ -3,3 +3,17 @@ // SPDX-License-Identifier: Apache-2.0 package v1alpha1 + +// SetDefaults_LandscapeKitConfiguration sets default values for LandscapeKitConfiguration fields. +func SetDefaults_LandscapeKitConfiguration(obj *LandscapeKitConfiguration) { + if obj.VersionConfig == nil { + obj.VersionConfig = &VersionConfiguration{} + } + if obj.VersionConfig.DefaultVersionsUpdateStrategy == nil { + obj.VersionConfig.DefaultVersionsUpdateStrategy = new(DefaultVersionsUpdateStrategyDisabled) + } + + if obj.MergeMode == nil { + obj.MergeMode = new(MergeModeInformative) + } +} diff --git a/pkg/apis/config/v1alpha1/types.go b/pkg/apis/config/v1alpha1/types.go index 076b929..1ad665e 100644 --- a/pkg/apis/config/v1alpha1/types.go +++ b/pkg/apis/config/v1alpha1/types.go @@ -140,11 +140,3 @@ var AllowedMergeModes = []string{ string(MergeModeInformative), string(MergeModeSilent), } - -// GetMergeMode returns the configured MergeMode, defaulting to MergeModeInformative. -func (c *LandscapeKitConfiguration) GetMergeMode() MergeMode { - if c == nil || c.MergeMode == nil { - return MergeModeInformative - } - return *c.MergeMode -} diff --git a/pkg/apis/config/v1alpha1/zz_generated.defaults.go b/pkg/apis/config/v1alpha1/zz_generated.defaults.go index dce68e6..e134802 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.defaults.go +++ b/pkg/apis/config/v1alpha1/zz_generated.defaults.go @@ -17,5 +17,10 @@ import ( // Public to allow building arbitrary schemes. // All generated defaulters are covering - they call all nested defaulters. func RegisterDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&LandscapeKitConfiguration{}, func(obj interface{}) { SetObjectDefaults_LandscapeKitConfiguration(obj.(*LandscapeKitConfiguration)) }) return nil } + +func SetObjectDefaults_LandscapeKitConfiguration(in *LandscapeKitConfiguration) { + SetDefaults_LandscapeKitConfiguration(in) +} diff --git a/pkg/cmd/resolve/plain/plain.go b/pkg/cmd/resolve/plain/plain.go index 6ac366d..96ce705 100644 --- a/pkg/cmd/resolve/plain/plain.go +++ b/pkg/cmd/resolve/plain/plain.go @@ -110,7 +110,7 @@ func (o *Options) loadConfigFile(filename string) error { func run(_ context.Context, opts *Options) error { if opts.Config != nil && opts.Config.VersionConfig != nil { - if updateStrategy := opts.Config.VersionConfig.DefaultVersionsUpdateStrategy; updateStrategy != nil && *updateStrategy == configv1alpha1.DefaultVersionsUpdateStrategyReleaseBranch { + if *opts.Config.VersionConfig.DefaultVersionsUpdateStrategy == configv1alpha1.DefaultVersionsUpdateStrategyReleaseBranch { opts.Log.Info("Updating default component vector file from the release branch", "branch", utilscomponentvector.GetReleaseBranchName()) var err error // The componentvector.DefaultComponentsYAML is intentionally overridden, so that subsequently it can be used to extract the updated default component vector versions. @@ -142,7 +142,7 @@ func run(_ context.Context, opts *Options) error { }, "\n") + "\n") newDefaultBytes = append(header, newDefaultBytes...) - if err := utilsfiles.WriteObjectsToFilesystem(map[string][]byte{utilscomponentvector.ComponentVectorFilename: newDefaultBytes}, opts.TargetDirPath, "", opts.fs, opts.Config.GetMergeMode()); err != nil { + if err := utilsfiles.WriteObjectsToFilesystem(map[string][]byte{utilscomponentvector.ComponentVectorFilename: newDefaultBytes}, opts.TargetDirPath, "", opts.fs, *opts.Config.MergeMode); err != nil { return fmt.Errorf("failed to write updated component vector: %w", err) } diff --git a/pkg/components/flux/component_test.go b/pkg/components/flux/component_test.go index e0df97f..562ced8 100644 --- a/pkg/components/flux/component_test.go +++ b/pkg/components/flux/component_test.go @@ -54,6 +54,16 @@ var _ = Describe("Flux Component Generation", func() { fs = afero.Afero{Fs: afero.NewMemMapFs()} + config := &v1alpha1.LandscapeKitConfiguration{ + Git: &v1alpha1.GitRepository{ + URL: repoURL, + Paths: v1alpha1.PathConfiguration{ + Landscape: relativeLandscapePath, + }, + }, + } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(config) + var err error opts, err = components.NewLandscapeOptions( &generateoptions.Options{ @@ -61,14 +71,7 @@ var _ = Describe("Flux Component Generation", func() { Log: logr.Discard(), }, TargetDirPath: targetPath, - Config: &v1alpha1.LandscapeKitConfiguration{ - Git: &v1alpha1.GitRepository{ - URL: repoURL, - Paths: v1alpha1.PathConfiguration{ - Landscape: relativeLandscapePath, - }, - }, - }, + Config: config, }, fs, ) diff --git a/pkg/components/gardener-extensions/networking-calico/component_test.go b/pkg/components/gardener-extensions/networking-calico/component_test.go index 351e731..e1b6b34 100644 --- a/pkg/components/gardener-extensions/networking-calico/component_test.go +++ b/pkg/components/gardener-extensions/networking-calico/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/networking-cilium/component_test.go b/pkg/components/gardener-extensions/networking-cilium/component_test.go index 802045e..5093442 100644 --- a/pkg/components/gardener-extensions/networking-cilium/component_test.go +++ b/pkg/components/gardener-extensions/networking-cilium/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/os-gardenlinux/component_test.go b/pkg/components/gardener-extensions/os-gardenlinux/component_test.go index 096175c..c93dde5 100644 --- a/pkg/components/gardener-extensions/os-gardenlinux/component_test.go +++ b/pkg/components/gardener-extensions/os-gardenlinux/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/os-suse-chost/component_test.go b/pkg/components/gardener-extensions/os-suse-chost/component_test.go index fb3fa11..eb27155 100644 --- a/pkg/components/gardener-extensions/os-suse-chost/component_test.go +++ b/pkg/components/gardener-extensions/os-suse-chost/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/provider-alicloud/component_test.go b/pkg/components/gardener-extensions/provider-alicloud/component_test.go index 499a135..c94be71 100644 --- a/pkg/components/gardener-extensions/provider-alicloud/component_test.go +++ b/pkg/components/gardener-extensions/provider-alicloud/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/provider-aws/component_test.go b/pkg/components/gardener-extensions/provider-aws/component_test.go index 17a395e..678720d 100644 --- a/pkg/components/gardener-extensions/provider-aws/component_test.go +++ b/pkg/components/gardener-extensions/provider-aws/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/provider-azure/component_test.go b/pkg/components/gardener-extensions/provider-azure/component_test.go index 481f991..a65922f 100644 --- a/pkg/components/gardener-extensions/provider-azure/component_test.go +++ b/pkg/components/gardener-extensions/provider-azure/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/provider-gcp/component_test.go b/pkg/components/gardener-extensions/provider-gcp/component_test.go index 7b93256..0b1dd0f 100644 --- a/pkg/components/gardener-extensions/provider-gcp/component_test.go +++ b/pkg/components/gardener-extensions/provider-gcp/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/provider-openstack/component_test.go b/pkg/components/gardener-extensions/provider-openstack/component_test.go index bad84c5..f4119b9 100644 --- a/pkg/components/gardener-extensions/provider-openstack/component_test.go +++ b/pkg/components/gardener-extensions/provider-openstack/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/runtime-gvisor/component_test.go b/pkg/components/gardener-extensions/runtime-gvisor/component_test.go index 65aede9..9c73ecc 100644 --- a/pkg/components/gardener-extensions/runtime-gvisor/component_test.go +++ b/pkg/components/gardener-extensions/runtime-gvisor/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/shoot-cert-service/component_test.go b/pkg/components/gardener-extensions/shoot-cert-service/component_test.go index d7d21bb..35b75d9 100644 --- a/pkg/components/gardener-extensions/shoot-cert-service/component_test.go +++ b/pkg/components/gardener-extensions/shoot-cert-service/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/shoot-dns-service/component_test.go b/pkg/components/gardener-extensions/shoot-dns-service/component_test.go index 9fb02d3..747c03d 100644 --- a/pkg/components/gardener-extensions/shoot-dns-service/component_test.go +++ b/pkg/components/gardener-extensions/shoot-dns-service/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/shoot-networking-problemdetector/component_test.go b/pkg/components/gardener-extensions/shoot-networking-problemdetector/component_test.go index 1a7e7f3..7f4d2e5 100644 --- a/pkg/components/gardener-extensions/shoot-networking-problemdetector/component_test.go +++ b/pkg/components/gardener-extensions/shoot-networking-problemdetector/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener-extensions/shoot-oidc-service/component_test.go b/pkg/components/gardener-extensions/shoot-oidc-service/component_test.go index 667aedc..683bad3 100644 --- a/pkg/components/gardener-extensions/shoot-oidc-service/component_test.go +++ b/pkg/components/gardener-extensions/shoot-oidc-service/component_test.go @@ -35,7 +35,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -68,6 +70,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener/garden/component_test.go b/pkg/components/gardener/garden/component_test.go index 781adff..455d17f 100644 --- a/pkg/components/gardener/garden/component_test.go +++ b/pkg/components/gardener/garden/component_test.go @@ -30,7 +30,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -62,6 +64,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener/operator/component_test.go b/pkg/components/gardener/operator/component_test.go index 5870a6d..d080114 100644 --- a/pkg/components/gardener/operator/component_test.go +++ b/pkg/components/gardener/operator/component_test.go @@ -37,7 +37,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -86,6 +88,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/gardener/virtual-garden-access/component_test.go b/pkg/components/gardener/virtual-garden-access/component_test.go index 58c8856..ed74d8f 100644 --- a/pkg/components/gardener/virtual-garden-access/component_test.go +++ b/pkg/components/gardener/virtual-garden-access/component_test.go @@ -30,7 +30,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -70,6 +72,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/components/types.go b/pkg/components/types.go index 17cdf45..fa23a42 100644 --- a/pkg/components/types.go +++ b/pkg/components/types.go @@ -127,7 +127,7 @@ func NewOptions(opts *generateoptions.Options, fs afero.Afero) (Options, error) targetPath: path.Clean(opts.TargetDirPath), filesystem: fs, logger: opts.Log, - mergeMode: opts.Config.GetMergeMode(), + mergeMode: *opts.Config.MergeMode, }, nil } diff --git a/pkg/components/types_test.go b/pkg/components/types_test.go index cf46888..c08cd7d 100644 --- a/pkg/components/types_test.go +++ b/pkg/components/types_test.go @@ -34,7 +34,9 @@ var _ = Describe("Types", func() { opts = &options.Options{ Options: &cmd.Options{}, TargetDirPath: "", + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(opts.Config) }) Describe("#GetTargetPath", func() { @@ -105,20 +107,9 @@ var _ = Describe("Types", func() { Expect(exists).To(BeFalse()) }) - It("should return an empty component vector when config is nil", func() { - opts.Config = nil - - componentOpts, err := components.NewOptions(opts, fs) - - Expect(err).NotTo(HaveOccurred()) - Expect(componentOpts.GetComponentVector()).NotTo(BeNil()) - - _, exists := componentOpts.GetComponentVector().FindComponentVersion("test-component") - Expect(exists).To(BeFalse()) - }) - - It("should return an empty component vector when VersionConfig is nil", func() { + It("should return an empty component vector when config is empty", func() { opts.Config = &v1alpha1.LandscapeKitConfiguration{} + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(opts.Config) componentOpts, err := components.NewOptions(opts, fs) @@ -199,7 +190,9 @@ var _ = Describe("Types", func() { Log: logger, }, TargetDirPath: "/path/to/target", + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(opts.Config) result, err := components.NewOptions(opts, fs) @@ -232,6 +225,7 @@ var _ = Describe("Types", func() { }, TargetDirPath: "", } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(opts.Config) }) Describe("#GetGitRepository", func() { @@ -285,6 +279,7 @@ var _ = Describe("Types", func() { }, }, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(opts.Config) result, err := components.NewLandscapeOptions(opts, fs) diff --git a/pkg/components/virtual-garden/garden-config/component_test.go b/pkg/components/virtual-garden/garden-config/component_test.go index b17dca5..5886dc9 100644 --- a/pkg/components/virtual-garden/garden-config/component_test.go +++ b/pkg/components/virtual-garden/garden-config/component_test.go @@ -30,7 +30,9 @@ var _ = Describe("Component Generation", func() { generateOpts = &generateoptions.Options{ TargetDirPath: "/repo/baseDir", Options: cmdOpts, + Config: &v1alpha1.LandscapeKitConfiguration{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) Describe("#GenerateBase", func() { @@ -67,6 +69,7 @@ var _ = Describe("Component Generation", func() { generateOpts.Config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{Paths: v1alpha1.PathConfiguration{Landscape: "./landscapeDir", Base: "./baseDir"}}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) }) It("should generate only the flux kustomization into the landscape dir", func() { diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 396db1b..d53e0ed 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -35,10 +35,14 @@ var _ = Describe("Registry", func() { config = &v1alpha1.LandscapeKitConfiguration{ Git: &v1alpha1.GitRepository{}, } + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(config) var err error options, err = components.NewOptions( - &generateoptions.Options{Options: &cmd.Options{Log: logr.Discard()}}, + &generateoptions.Options{ + Options: &cmd.Options{Log: logr.Discard()}, + Config: config, + }, afero.Afero{Fs: afero.NewMemMapFs()}, ) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/utils/kustomization/kustomization_test.go b/pkg/utils/kustomization/kustomization_test.go index ff1815a..b7cf67a 100644 --- a/pkg/utils/kustomization/kustomization_test.go +++ b/pkg/utils/kustomization/kustomization_test.go @@ -81,11 +81,15 @@ var _ = Describe("Kustomization", func() { BeforeEach(func() { fs = afero.Afero{Fs: afero.NewMemMapFs()} + config := &configv1alpha1.LandscapeKitConfiguration{} + configv1alpha1.SetObjectDefaults_LandscapeKitConfiguration(config) + var err error opts, err = components.NewOptions(&generateoptions.Options{ Options: &cmd.Options{ Log: logr.Discard(), }, + Config: config, TargetDirPath: "/absolute/path/with/../to/repo/landscape", }, fs) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/utils/test/kustomize.go b/pkg/utils/test/kustomize.go index 780b7a5..3f5eccf 100644 --- a/pkg/utils/test/kustomize.go +++ b/pkg/utils/test/kustomize.go @@ -118,6 +118,7 @@ func KustomizeComponent( }, } ) + v1alpha1.SetObjectDefaults_LandscapeKitConfiguration(generateOpts.Config) baseOpts, err := components.NewOptions(generateOpts, fs) if err != nil {