From a6b4229c33835e237e952ec2d5bcdaad36ce532a Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Mon, 10 Mar 2025 14:05:43 +0100 Subject: [PATCH 01/26] feat: add env and stage addon --- internal/project/cluster.go | 3 +- internal/project/environment.go | 38 ++- internal/project/environment_test.go | 374 +++++++++++++++++++++++++++ internal/project/stage.go | 38 ++- internal/project/stage_test.go | 374 +++++++++++++++++++++++++++ 5 files changed, 817 insertions(+), 10 deletions(-) create mode 100644 internal/project/environment_test.go create mode 100644 internal/project/stage_test.go diff --git a/internal/project/cluster.go b/internal/project/cluster.go index ba9edec..47b1019 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -50,10 +50,9 @@ func (c *Cluster) EnableAddon(addon string) { return } c.Addons[addon].Enabled = true - fmt.Println("Enabled addon option:", c.Addons[addon].Enabled) } -// DisableAddon disables the addon for the cluster by setting the enabled flag to false and removing all properties +// DisableAddon disables the addon for the cluster by setting the enabled flag to false func (c *Cluster) DisableAddon(addon string) { if _, ok := c.Addons[addon]; !ok { // already disabled or not found diff --git a/internal/project/environment.go b/internal/project/environment.go index 3593d87..036ae0d 100644 --- a/internal/project/environment.go +++ b/internal/project/environment.go @@ -1,8 +1,38 @@ package project type Environment struct { - Name string `json:"-"` - Properties map[string]string `json:"properties"` - Actions Actions `json:"actions"` - Stages map[string]*Stage `json:"stages"` + Name string `json:"-"` + Properties map[string]string `json:"properties"` + Actions Actions `json:"actions"` + Stages map[string]*Stage `json:"stages"` + Addons map[string]*ClusterAddon `json:"addons"` +} + +// IsAddonEnabled checks if the addon is enabled for the stage +func (e Environment) IsAddonEnabled(addon string) bool { + _, ok := e.Addons[addon] + if !ok { + return false + } + return e.Addons[addon].Enabled +} + +// EnableAddon enables the addon for the stage by setting the enabled flag to true +func (e *Environment) EnableAddon(addon string) { + if e.Addons[addon] == nil { + e.Addons[addon] = &ClusterAddon{ + Enabled: true, + } + return + } + e.Addons[addon].Enabled = true +} + +// DisableAddon disables the addon for the stage by setting the enabled flag to false +func (e *Environment) DisableAddon(addon string) { + if _, ok := e.Addons[addon]; !ok { + // already disabled or not found + return + } + e.Addons[addon].Enabled = false } diff --git a/internal/project/environment_test.go b/internal/project/environment_test.go new file mode 100644 index 0000000..ffc5197 --- /dev/null +++ b/internal/project/environment_test.go @@ -0,0 +1,374 @@ +package project + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEnvironment_IsAddonEnabled(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "no addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "two addons, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := e.IsAddonEnabled(tt.args.addon); got != tt.want { + t.Errorf("Environment.IsAddonEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_EnableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Environment + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + e.EnableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *e) + if diff != "" { + t.Errorf("Environment.EnableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestEnvironment_DisableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Environment + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{}, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: map[string]any{}, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: nil, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + e.DisableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *e) + if diff != "" { + t.Errorf("Environment.DisableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} diff --git a/internal/project/stage.go b/internal/project/stage.go index d3cff46..cc1e449 100644 --- a/internal/project/stage.go +++ b/internal/project/stage.go @@ -1,8 +1,38 @@ package project type Stage struct { - Name string `json:"-"` - Properties map[string]string `json:"properties"` - Actions Actions `json:"actions"` - Clusters map[string]*Cluster `json:"clusters"` + Name string `json:"-"` + Properties map[string]string `json:"properties"` + Actions Actions `json:"actions"` + Clusters map[string]*Cluster `json:"clusters"` + Addons map[string]*ClusterAddon `json:"addons"` +} + +// IsAddonEnabled checks if the addon is enabled for the stage +func (s Stage) IsAddonEnabled(addon string) bool { + _, ok := s.Addons[addon] + if !ok { + return false + } + return s.Addons[addon].Enabled +} + +// EnableAddon enables the addon for the stage by setting the enabled flag to true +func (s *Stage) EnableAddon(addon string) { + if s.Addons[addon] == nil { + s.Addons[addon] = &ClusterAddon{ + Enabled: true, + } + return + } + s.Addons[addon].Enabled = true +} + +// DisableAddon disables the addon for the stage by setting the enabled flag to false +func (s *Stage) DisableAddon(addon string) { + if _, ok := s.Addons[addon]; !ok { + // already disabled or not found + return + } + s.Addons[addon].Enabled = false } diff --git a/internal/project/stage_test.go b/internal/project/stage_test.go new file mode 100644 index 0000000..e4747d6 --- /dev/null +++ b/internal/project/stage_test.go @@ -0,0 +1,374 @@ +package project + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStage_IsAddonEnabled(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "no addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "two addons, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := s.IsAddonEnabled(tt.args.addon); got != tt.want { + t.Errorf("Stage.IsAddonEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_EnableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Stage + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + s.EnableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *s) + if diff != "" { + t.Errorf("Stage.EnableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestStage_DisableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Stage + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{}, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: map[string]any{}, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: nil, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + s.DisableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *s) + if diff != "" { + t.Errorf("Stage.DisableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} From 1996ae403895748b7b40c4b1f390b98283e0246c Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Mon, 10 Mar 2025 17:14:18 +0100 Subject: [PATCH 02/26] fix: property value validation --- internal/template/manifest.go | 28 ++++++++++++++---------- internal/template/manifest_test.go | 34 +++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/internal/template/manifest.go b/internal/template/manifest.go index ac950e0..115a8aa 100644 --- a/internal/template/manifest.go +++ b/internal/template/manifest.go @@ -5,6 +5,7 @@ import ( "io/fs" "os" "path/filepath" + "reflect" "strings" "sigs.k8s.io/yaml" @@ -38,24 +39,29 @@ const ( // checkType validates the given value against the property type // If the value is valid, it will be returned, otherwise an error is returned func (p PropertyType) checkType(value any) (any, error) { - switch v := value.(type) { - case string: + if value == nil { + return nil, nil + } + kind := reflect.TypeOf(value).Kind() + typeValue := reflect.ValueOf(value) + switch kind { + case reflect.String: if p != PropertyTypeString { - return nil, fmt.Errorf("expected type %s, got string", p) + return nil, fmt.Errorf("expected type %s, got %v", p, kind) } - return v, nil - case bool: + return typeValue.String(), nil + case reflect.Bool: if p != PropertyTypeBool { - return nil, fmt.Errorf("expected type %s, got bool", p) + return nil, fmt.Errorf("expected type %s, got %v", p, kind) } - return v, nil - case int: + return typeValue.Bool(), nil + case reflect.Int: if p != PropertyTypeInt { - return nil, fmt.Errorf("expected type %s, got int", p) + return nil, fmt.Errorf("expected type %s, got %v", p, kind) } - return v, nil + return typeValue.Int(), nil default: - return nil, fmt.Errorf("unsupported type %T", v) + return nil, fmt.Errorf("expected type %s, got %v", p, reflect.TypeOf(value).Kind()) } } diff --git a/internal/template/manifest_test.go b/internal/template/manifest_test.go index f282357..5f243f5 100644 --- a/internal/template/manifest_test.go +++ b/internal/template/manifest_test.go @@ -47,7 +47,7 @@ func TestPropertyType_checkType(t *testing.T) { args: args{ value: 42, }, - want: 42, + want: int64(42), wantErr: false, }, { @@ -86,6 +86,33 @@ func TestPropertyType_checkType(t *testing.T) { want: nil, wantErr: true, }, + { + name: "property is bool but expect string", + p: PropertyTypeString, + args: args{ + value: true, + }, + want: nil, + wantErr: true, + }, + { + name: "unsupported data", + p: PropertyTypeString, + args: args{ + value: []string{"test"}, + }, + want: nil, + wantErr: true, + }, + { + name: "nil value", + p: PropertyTypeString, + args: args{ + value: nil, + }, + want: nil, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -94,8 +121,9 @@ func TestPropertyType_checkType(t *testing.T) { t.Errorf("PropertyType.checkType() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("PropertyType.checkType() = %v, want %v", got, tt.want) + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Errorf("PropertyType.checkType() mismatch (-want +got):\n%s", diff) } }) } From dd5e5fa84c50200812922cd4c5dc2ec743bb0530 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 16:43:07 +0100 Subject: [PATCH 03/26] chore: improve type comparision and parsing --- internal/template/manifest.go | 39 +++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/internal/template/manifest.go b/internal/template/manifest.go index 115a8aa..d877839 100644 --- a/internal/template/manifest.go +++ b/internal/template/manifest.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strconv" "strings" "sigs.k8s.io/yaml" @@ -42,24 +43,36 @@ func (p PropertyType) checkType(value any) (any, error) { if value == nil { return nil, nil } + kind := reflect.TypeOf(value).Kind() typeValue := reflect.ValueOf(value) - switch kind { - case reflect.String: - if p != PropertyTypeString { - return nil, fmt.Errorf("expected type %s, got %v", p, kind) - } + switch p { + case PropertyTypeString: return typeValue.String(), nil - case reflect.Bool: - if p != PropertyTypeBool { - return nil, fmt.Errorf("expected type %s, got %v", p, kind) + case PropertyTypeBool: + if kind == reflect.String { + bl, err := strconv.ParseBool(typeValue.String()) + if err != nil { + return nil, err + } + return bl, nil + } + if kind == reflect.Bool { + return value, nil + } + return nil, fmt.Errorf("expected type %s, got %v", p, kind) + case PropertyTypeInt: + if kind == reflect.String { + i, err := strconv.Atoi(typeValue.String()) + if err != nil { + return nil, err + } + return i, nil } - return typeValue.Bool(), nil - case reflect.Int: - if p != PropertyTypeInt { - return nil, fmt.Errorf("expected type %s, got %v", p, kind) + if kind == reflect.Int { + return value, nil } - return typeValue.Int(), nil + return nil, fmt.Errorf("expected type %s, got %v", p, kind) default: return nil, fmt.Errorf("expected type %s, got %v", p, reflect.TypeOf(value).Kind()) } From 3ba00e8077e139dad4e822cdf2e516cdeea9cf1a Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 16:59:48 +0100 Subject: [PATCH 04/26] feat: add AddonHandler interface --- internal/project/addon.go | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 internal/project/addon.go diff --git a/internal/project/addon.go b/internal/project/addon.go new file mode 100644 index 0000000..5c577c5 --- /dev/null +++ b/internal/project/addon.go @@ -0,0 +1,72 @@ +package project + +import ( + "fmt" +) + +type AddonHandler interface { + // IsAddonEnabled checks if the addon is enabled + IsAddonEnabled(name string) bool + // EnableAddon enables the addon + EnableAddon(name string) + // DisableAddon disables the addon + DisableAddon(name string) + // GetAddons returns the addons + GetAddons() ClusterAddons + // GetAddon returns the addon by name + GetAddon(name string) *ClusterAddon +} + +type ClusterAddons map[string]*ClusterAddon + +func (ca ClusterAddons) AllRequiredPropertiesSet(config *ProjectConfig) error { + for addonName, addon := range ca { + if !addon.Enabled { + fmt.Printf("addon %s is disabled\n", addonName) + continue + } + err := addon.AllRequiredPropertiesSet(config, addonName) + if err != nil { + return fmt.Errorf("failed to validate addon %s: %w", addonName, err) + } + } + return nil +} + +func (ca ClusterAddons) IsEnabled(addon string) bool { + if ca[addon] == nil { + return false + } + return ca[addon].IsEnabled() +} + +type ClusterAddon struct { + Enabled bool `json:"enabled"` + Properties map[string]any `json:"properties"` +} + +// AllRequiredPropertiesSet checks if all required properties are set for the addon +func (ca *ClusterAddon) AllRequiredPropertiesSet(config *ProjectConfig, addonName string) error { + for key, property := range config.ParsedAddons[addonName].Properties { + if property.Required && ca.Properties[key] == nil { + return fmt.Errorf("[%s] property for key %s is required", addonName, key) + } + _, err := property.ParseValue(ca.Properties[key]) + if err != nil { + return fmt.Errorf("[%s] property for key %s is invalid: %w", addonName, key, err) + } + } + return nil +} + +// IsEnabled checks if the addon is enabled +func (ca ClusterAddon) IsEnabled() bool { + return ca.Enabled +} + +func (ca *ClusterAddon) SetProperty(key string, value any) { + if ca.Properties == nil { + ca.Properties = map[string]any{} + } + ca.Properties[key] = value +} From 9c4774aab24d770bffc3032de45d12c209267628 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:02:23 +0100 Subject: [PATCH 05/26] feat: implement AddonHandler for project.Cluster --- internal/project/cluster.go | 45 ++--- internal/project/cluster_test.go | 273 ------------------------------- 2 files changed, 13 insertions(+), 305 deletions(-) diff --git a/internal/project/cluster.go b/internal/project/cluster.go index 47b1019..b295827 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -7,24 +7,9 @@ import ( "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/utils" ) -type ClusterAddon struct { - Enabled bool `json:"enabled"` - Properties map[string]any `json:"properties"` -} - -// AllRequiredPropertiesSet checks if all required properties are set for the addon -func (ca *ClusterAddon) AllRequiredPropertiesSet(config *ProjectConfig, addonName string) error { - for key, property := range config.ParsedAddons[addonName].Properties { - if property.Required && ca.Properties[key] == nil { - return fmt.Errorf("property for key %s is required", key) - } - _, err := config.ParsedAddons[addonName].Properties[key].ParseValue(ca.Properties[key]) - if err != nil { - return fmt.Errorf("property for key %s is invalid: %w", key, err) - } - } - return nil -} +var ( + _ AddonHandler = &Cluster{} +) type Cluster struct { Name string `json:"-"` @@ -61,6 +46,16 @@ func (c *Cluster) DisableAddon(addon string) { c.Addons[addon].Enabled = false } +// GetAddons returns the cluster addons +func (c *Cluster) GetAddons() ClusterAddons { + return c.Addons +} + +// GetAddon returns the addon by name +func (c *Cluster) GetAddon(name string) *ClusterAddon { + return c.Addons[name] +} + // Render renders the cluster configuration using the given project templates func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { properties := utils.MergeMaps(config.EnvStageProperty(env, stage), c.Properties) @@ -114,20 +109,6 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return nil } -func (c *Cluster) AllRequiredPropertiesSet(config *ProjectConfig) error { - for addonName, addon := range c.Addons { - if !addon.Enabled { - // skip disabled addons - continue - } - err := addon.AllRequiredPropertiesSet(config, addonName) - if err != nil { - return fmt.Errorf("failed to validate properties for addon %s: %w", addonName, err) - } - } - return nil -} - // SetDefaultAddons sets the default addons for the cluster func (c *Cluster) SetDefaultAddons(config *ProjectConfig) { for addonName, addon := range config.Addons { diff --git a/internal/project/cluster_test.go b/internal/project/cluster_test.go index 532af9e..37ce188 100644 --- a/internal/project/cluster_test.go +++ b/internal/project/cluster_test.go @@ -712,276 +712,3 @@ func TestCluster_DisableAddon(t *testing.T) { }) } } - -func TestCluster_AllRequiredPropertiesSet(t *testing.T) { - type fields struct { - Name string - Addons map[string]*ClusterAddon - Properties map[string]string - } - type args struct { - config *ProjectConfig - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "one addon, no properties in cluster", - fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{}, - }, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, - Path: "examples/addons/addon1", - }, - }, - }, - }, - wantErr: true, - }, - { - name: "one addon, with properties in cluster but wrong type", - fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{ - "property1": "invalid", - }, - }, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, - Path: "examples/addons/addon1", - }, - }, - }, - }, - wantErr: true, - }, - { - name: "one addon disabled, with properties in cluster but wrong type", - fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: false, - Properties: map[string]any{ - "property1": "invalid", - }, - }, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, - Path: "examples/addons/addon1", - }, - }, - }, - }, - wantErr: false, - }, - { - name: "one addon, with properties in cluster", - fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{ - "property1": true, - }, - }, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "two addons, with properties in cluster", - fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{ - "property1": true, - }, - }, - "addon2": { - Enabled: true, - Properties: map[string]any{ - "property1": true, - }, - }, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - "addon2": { - BasePath: "examples/addons/addon2", - Name: "addon2", - Description: "addon2", - Group: "group2", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - Annotations: map[string]string{}, - Files: []string{}, - }, - }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, - }, - "addon2": { - Name: "addon2", - Group: "group2", - DefaultEnabled: true, - }, - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Cluster{ - Name: tt.fields.Name, - Addons: tt.fields.Addons, - Properties: tt.fields.Properties, - } - if err := c.AllRequiredPropertiesSet(tt.args.config); (err != nil) != tt.wantErr { - t.Errorf("Cluster.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} From 973723a0638c9099f5610cc5966b43b996d3661b Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:04:48 +0100 Subject: [PATCH 06/26] feat: implement AddonHandler for project.Environment --- internal/project/environment.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/project/environment.go b/internal/project/environment.go index 036ae0d..be1e58e 100644 --- a/internal/project/environment.go +++ b/internal/project/environment.go @@ -1,5 +1,9 @@ package project +var ( + _ AddonHandler = &Environment{} +) + type Environment struct { Name string `json:"-"` Properties map[string]string `json:"properties"` @@ -36,3 +40,13 @@ func (e *Environment) DisableAddon(addon string) { } e.Addons[addon].Enabled = false } + +// GetAddons returns the environment addons +func (e *Environment) GetAddons() ClusterAddons { + return e.Addons +} + +// GetAddon returns the addon by name +func (e *Environment) GetAddon(name string) *ClusterAddon { + return e.Addons[name] +} From 94e96f10e6c2bfa941b8eda864f7467ec2de524c Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:06:36 +0100 Subject: [PATCH 07/26] feat: implement AddonHandler for project.Stage --- internal/project/stage.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/project/stage.go b/internal/project/stage.go index cc1e449..17f9b59 100644 --- a/internal/project/stage.go +++ b/internal/project/stage.go @@ -1,5 +1,9 @@ package project +var ( + _ AddonHandler = &Stage{} +) + type Stage struct { Name string `json:"-"` Properties map[string]string `json:"properties"` @@ -36,3 +40,13 @@ func (s *Stage) DisableAddon(addon string) { } s.Addons[addon].Enabled = false } + +// GetAddons returns the environment addons +func (s *Stage) GetAddons() ClusterAddons { + return s.Addons +} + +// GetAddon returns the addon by name +func (s *Stage) GetAddon(name string) *ClusterAddon { + return s.Addons[name] +} From a871f8e713e1263a329d8b418bcbff4767ea0c6d Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:07:48 +0100 Subject: [PATCH 08/26] feat: implement text color wraping feat: implement text color wrapping --- internal/utils/color.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 internal/utils/color.go diff --git a/internal/utils/color.go b/internal/utils/color.go new file mode 100644 index 0000000..7b0e203 --- /dev/null +++ b/internal/utils/color.go @@ -0,0 +1,27 @@ +package utils + +import "fmt" + +type Color string + +var ( + reset Color = "\033[0m" + Red Color = "\033[31m" + Green Color = "\033[32m" + Yellow Color = "\033[33m" + Blue Color = "\033[34m" + Magenta Color = "\033[35m" + Cyan Color = "\033[36m" + Gray Color = "\033[37m" + White Color = "\033[97m" +) + +// Wrap wraps the given string with the color +func (c Color) Wrap(s string) string { + return fmt.Sprintf("%s%s%s", c, s, reset) +} + +// Colorize colorizes the given string with the given color +func Colorize(s string, c Color) string { + return c.Wrap(s) +} From db3f616f711a393a2b347b46832a92a979db4c18 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:15:25 +0100 Subject: [PATCH 09/26] chore: improve addon menu --- internal/menu/addon.go | 50 +++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/internal/menu/addon.go b/internal/menu/addon.go index cff7917..ca76184 100644 --- a/internal/menu/addon.go +++ b/internal/menu/addon.go @@ -17,12 +17,12 @@ type addonClusterMenu struct { config *project.ProjectConfig } -func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { +func (a *addonClusterMenu) menuManageAddons(ah project.AddonHandler) error { for { prompt := promptui.Select{ Label: "Manage Addons", Items: append(utils.SortStringSlice(utils.MapKeysToList(a.config.ParsedAddons)), "Done"), - Templates: a.templateManageAddons(cluster), + Templates: a.templateManageAddons(ah), Size: 10, } _, result, err := prompt.Run() @@ -30,15 +30,16 @@ func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { return err } if result == "Done" { - err := cluster.AllRequiredPropertiesSet(a.config) + err := ah.GetAddons().AllRequiredPropertiesSet(a.config) if err != nil { fmt.Println("Not all required properties are set", err) continue } + fmt.Println("Done managing addons") break } - err = a.menuAddonSettings(cluster, result) + err = a.menuAddonSettings(ah, result) if err != nil { return err } @@ -46,7 +47,7 @@ func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { return nil } -func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promptui.SelectTemplates { +func (a *addonClusterMenu) templateManageAddons(ah project.AddonHandler) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: "{{ . }}", Details: "{{ addon . }}", @@ -59,7 +60,7 @@ func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promp resultString := "--------------------------------\nDetails:\n" resultString += fmt.Sprintf("\tDescription: %s\n", a.config.ParsedAddons[addonName].Description) resultString += fmt.Sprintf("\tGroup: %s\n", a.config.ParsedAddons[addonName].Group) - resultString += fmt.Sprintf("\tEnabled: %v | Default: %v\n", cluster.IsAddonEnabled(addonName), a.config.Addons[addonName].DefaultEnabled) + resultString += fmt.Sprintf("\tEnabled: %v | Default: %v\n", ah.GetAddon(addonName).IsEnabled(), a.config.Addons[addonName].DefaultEnabled) return resultString } return funcmap @@ -67,10 +68,10 @@ func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promp } } -func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon string) error { +func (a *addonClusterMenu) menuAddonSettings(ah project.AddonHandler, addon string) error { for { selectOptions := []string{"Enable", "Done"} - if (*cluster).IsAddonEnabled(addon) { + if ah.IsAddonEnabled(addon) { selectOptions = []string{"Disable", "Settings", "Done"} } @@ -85,25 +86,23 @@ func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon str switch result { case "Enable": - fmt.Println("Enable addon", addon) - cluster.EnableAddon(addon) + ah.EnableAddon(addon) case "Disable": - fmt.Println("Disable addon", addon) - cluster.DisableAddon(addon) + ah.DisableAddon(addon) case "Settings": - err := a.menuAddonProperties(cluster, addon) + err := a.menuAddonProperties(ah, addon) if err != nil { return err } case "Done": - if !(*cluster).IsAddonEnabled(addon) { + if !ah.IsAddonEnabled(addon) { return nil } // check if all required properties are set - err := cluster.Addons[addon].AllRequiredPropertiesSet(a.config, addon) + err := ah.GetAddon(addon).AllRequiredPropertiesSet(a.config, addon) if err != nil { - fmt.Println("Not all required properties are set", err) + fmt.Println(utils.Red.Wrap("Not all required properties are set"), err) continue } return nil @@ -113,13 +112,13 @@ func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon str } } -func (a *addonClusterMenu) menuAddonProperties(cluster *project.Cluster, addon string) error { +func (a *addonClusterMenu) menuAddonProperties(ah project.AddonHandler, addon string) error { for { prompt := promptui.Select{ Label: "Properties", Items: append(utils.SortStringSlice(utils.MapKeysToList(a.config.ParsedAddons[addon].Properties)), "Done"), // TODO: add template to display property options - Templates: a.menuTemplateAddonProperties(cluster, addon), + Templates: a.menuTemplateAddonProperties(ah, addon), } _, result, err := prompt.Run() if err != nil { @@ -129,32 +128,29 @@ func (a *addonClusterMenu) menuAddonProperties(cluster *project.Cluster, addon s break } - value, err := cli.UntypedQuestion(a.writer, a.reader, "Value", cluster.Addons[addon].Properties[result], func(s any) error { + value, err := cli.UntypedQuestion(a.writer, a.reader, "Value", fmt.Sprintf("%v", ah.GetAddon(addon).Properties[result]), func(s any) error { if s == nil { return fmt.Errorf("value cannot be empty") } return nil }) if err != nil { - fmt.Println("Value violates requirements, please try again", err) + fmt.Println(utils.Red.Wrap("Value violates requirements, please try again"), err) continue } - if cluster.Addons[addon].Properties == nil { - cluster.Addons[addon].Properties = map[string]any{} - } value, err = a.config.ParsedAddons[addon].Properties[result].ParseValue(value) if err != nil { - fmt.Println("Value violates requirements, please try again", err) + fmt.Println(utils.Red.Wrap("Value violates requirements, please try again"), err) continue } - cluster.Addons[addon].Properties[result] = value + ah.GetAddon(addon).SetProperty(result, value) } return nil } // menuTemplateAddonProperties returns a promptui.SelectTemplates for the addon properties -func (a *addonClusterMenu) menuTemplateAddonProperties(cluster *project.Cluster, addon string) *promptui.SelectTemplates { +func (a *addonClusterMenu) menuTemplateAddonProperties(ah project.AddonHandler, addon string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: "{{ . }}", Details: "{{ properties . }}", @@ -169,7 +165,7 @@ func (a *addonClusterMenu) menuTemplateAddonProperties(cluster *project.Cluster, resultString += fmt.Sprintf("\tRequired: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Required) resultString += fmt.Sprintf("\tType: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Type) resultString += fmt.Sprintf("\tDefault: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Default) - data := cluster.Addons[addon].Properties[selectValue] + data := ah.GetAddon(addon).Properties[selectValue] if data == nil { data = a.config.ParsedAddons[addon].Properties[selectValue].Default } From e41d58bc38bff4fe9e103b9b1dfe301fb53c281b Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:16:13 +0100 Subject: [PATCH 10/26] feat: implement environment based addon menu --- internal/menu/environment.go | 65 ++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/internal/menu/environment.go b/internal/menu/environment.go index e422016..a4bacee 100644 --- a/internal/menu/environment.go +++ b/internal/menu/environment.go @@ -6,6 +6,7 @@ import ( "fmt" "io" + "github.com/k0kubun/pp/v3" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/cli" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/project" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/utils" @@ -36,28 +37,68 @@ func (e *environmentMenu) menuCreateEnvironment() (*project.Environment, error) Name: env, Stages: map[string]*project.Stage{}, Properties: map[string]string{}, + Addons: map[string]*project.ClusterAddon{}, } - // ask for properties - properties, err := e.menuEnvironmentProperties(environment) + err = e.menuSettings(environment) if err != nil { return nil, err } - environment.Properties = properties return environment, nil } func (e *environmentMenu) menuUpdateEnvironment(envName string) (*project.Environment, error) { - environment := *e.config.Environments[envName] + environment := e.config.Environments[envName] environment.Name = envName - // ask for properties - properties, err := e.menuEnvironmentProperties(&environment) + if environment.Addons == nil { + environment.Addons = map[string]*project.ClusterAddon{} + } + err := e.menuSettings(environment) if err != nil { return nil, err } - environment.Properties = properties - return &environment, nil + fmt.Println("Updated Environment") + pp.Println(environment) + return environment, nil +} + +// menuSettings creates a context menu to manage the settings of a cluster +func (e *environmentMenu) menuSettings(environment *project.Environment) error { + for { + prompt := promptui.Select{ + Label: "Settings", + Items: []string{"Addons", "Properties", "Done"}, + } + _, result, err := prompt.Run() + if err != nil { + return err + } + + switch result { + case "Addons": + addon := addonClusterMenu{ + writer: e.writer, + reader: e.reader, + config: e.config, + } + pp.Println(environment) + err := addon.menuManageAddons(environment) + if err != nil { + return err + } + case "Properties": + properties, err := e.menuEnvironmentProperties(environment) + if err != nil { + return err + } + environment.Properties = properties + case "Done": + return nil + default: + return fmt.Errorf("invalid option %s", result) + } + } } func (e *environmentMenu) menuDeleteEnvironment(envName string) (*project.Environment, error) { @@ -74,13 +115,11 @@ func (e *environmentMenu) menuDeleteEnvironment(envName string) (*project.Enviro } func (e *environmentMenu) menuEnvironmentProperties(env *project.Environment) (map[string]string, error) { - envProperties := map[string]string{} + envProperties := env.Properties for { - properties := utils.MergeMaps(env.Properties, envProperties) - prompt := promptui.SelectWithAdd{ Label: "Properties", - Items: append(utils.SortStringSlice(utils.MapKeysToList(properties)), "Done"), + Items: append(utils.SortStringSlice(utils.MapKeysToList(envProperties)), "Done"), AddLabel: "Create Property", } _, result, err := prompt.Run() @@ -95,7 +134,7 @@ func (e *environmentMenu) menuEnvironmentProperties(env *project.Environment) (m break } - val, err := cli.StringQuestion(e.writer, e.reader, "Property Value", properties[result], func(s string) error { + val, err := cli.StringQuestion(e.writer, e.reader, "Property Value", envProperties[result], func(s string) error { if s == "" { return fmt.Errorf("property value cannot be empty") } From 276cef8eaa24d95c72283934fa9648305a7848f3 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 17:25:00 +0100 Subject: [PATCH 11/26] fix: check type unit tests --- internal/template/manifest_test.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/internal/template/manifest_test.go b/internal/template/manifest_test.go index 5f243f5..b3d88ef 100644 --- a/internal/template/manifest_test.go +++ b/internal/template/manifest_test.go @@ -47,16 +47,16 @@ func TestPropertyType_checkType(t *testing.T) { args: args{ value: 42, }, - want: int64(42), + want: int(42), wantErr: false, }, { - name: "invalid string", + name: "number as string", p: PropertyTypeString, args: args{ value: 42, }, - want: nil, + want: "42", wantErr: true, }, { @@ -87,21 +87,12 @@ func TestPropertyType_checkType(t *testing.T) { wantErr: true, }, { - name: "property is bool but expect string", + name: "property is bool want bool as string", p: PropertyTypeString, args: args{ value: true, }, - want: nil, - wantErr: true, - }, - { - name: "unsupported data", - p: PropertyTypeString, - args: args{ - value: []string{"test"}, - }, - want: nil, + want: "true", wantErr: true, }, { From 5c18b65e1eb7a2dea5d464c6cf67e1d7daa334fc Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 19:08:11 +0100 Subject: [PATCH 12/26] chore: remove debug output --- internal/menu/environment.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/menu/environment.go b/internal/menu/environment.go index a4bacee..72c1478 100644 --- a/internal/menu/environment.go +++ b/internal/menu/environment.go @@ -6,7 +6,6 @@ import ( "fmt" "io" - "github.com/k0kubun/pp/v3" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/cli" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/project" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/utils" @@ -58,8 +57,6 @@ func (e *environmentMenu) menuUpdateEnvironment(envName string) (*project.Enviro if err != nil { return nil, err } - fmt.Println("Updated Environment") - pp.Println(environment) return environment, nil } @@ -82,7 +79,6 @@ func (e *environmentMenu) menuSettings(environment *project.Environment) error { reader: e.reader, config: e.config, } - pp.Println(environment) err := addon.menuManageAddons(environment) if err != nil { return err From d869e353ba9cc1dc90c096f19d6ebb2a3c5fca90 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 19:22:02 +0100 Subject: [PATCH 13/26] feat: manage addons on stage level --- internal/menu/root.go | 2 +- internal/menu/stage.go | 48 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/internal/menu/root.go b/internal/menu/root.go index bb9a515..a25e363 100644 --- a/internal/menu/root.go +++ b/internal/menu/root.go @@ -130,7 +130,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { // update stage in config config.Environments[*envName].Stages[stage.Name] = stage eventCh <- newPostUpdateEvent(EventOriginStage, *envName, stage.Name, "") - return errors.New("stage update not implemented") + return nil } delete := func() error { diff --git a/internal/menu/stage.go b/internal/menu/stage.go index d9966f4..81ea313 100644 --- a/internal/menu/stage.go +++ b/internal/menu/stage.go @@ -37,27 +37,67 @@ func (s *stageMenu) menuCreateStage(env string) (*project.Stage, error) { Properties: map[string]string{}, Actions: project.Actions{}, Clusters: map[string]*project.Cluster{}, + Addons: map[string]*project.ClusterAddon{}, } - properties, err := s.menuProperties(stage) + err = s.menuSettings(stage) if err != nil { return nil, err } - stage.Properties = properties return stage, nil } func (s *stageMenu) menuUpdateStage(envName, stageName string) (*project.Stage, error) { stage := s.config.Environments[envName].Stages[stageName] - properties, err := s.menuProperties(stage) + stage.Name = stageName + if stage.Addons == nil { + stage.Addons = map[string]*project.ClusterAddon{} + } + err := s.menuSettings(stage) if err != nil { return nil, err } - stage.Properties = properties return stage, nil } +// menuSettings creates a context menu to manage the settings of a cluster +func (s *stageMenu) menuSettings(stage *project.Stage) error { + for { + prompt := promptui.Select{ + Label: "Settings", + Items: []string{"Addons", "Properties", "Done"}, + } + _, result, err := prompt.Run() + if err != nil { + return err + } + + switch result { + case "Addons": + addon := addonClusterMenu{ + writer: s.writer, + reader: s.reader, + config: s.config, + } + err := addon.menuManageAddons(stage) + if err != nil { + return err + } + case "Properties": + properties, err := s.menuProperties(stage) + if err != nil { + return err + } + stage.Properties = properties + case "Done": + return nil + default: + return fmt.Errorf("invalid option %s", result) + } + } +} + func (s *stageMenu) menuDeleteStage(env, stage string) error { // TODO: menu is missing to delete the stage (cascade delete) return errors.New("not implemented") From 8efdb9b8726d917e1e1651b58b9fb8a9ca9c8da7 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 19:37:18 +0100 Subject: [PATCH 14/26] chore: sanitize go dependencies --- go.mod | 4 ---- go.sum | 9 --------- 2 files changed, 13 deletions(-) diff --git a/go.mod b/go.mod index 3b6fa08..1795930 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.2 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/google/go-cmp v0.7.0 - github.com/k0kubun/pp/v3 v3.4.1 github.com/manifoldco/promptui v0.9.0 sigs.k8s.io/yaml v1.4.0 ) @@ -17,13 +16,10 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 057e65c..2dace1f 100644 --- a/go.sum +++ b/go.sum @@ -23,18 +23,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= -github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -52,11 +46,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= From e862614084b9bd75663da00feee908a71572d592 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 20:10:39 +0100 Subject: [PATCH 15/26] chore: improve env, stage and cluster gathering --- cmd/main.go | 6 +++--- internal/menu/cluster.go | 4 ++-- internal/menu/environment.go | 9 ++++----- internal/menu/root.go | 10 ++++----- internal/menu/stage.go | 5 ++--- internal/project/environment.go | 11 ++++++++++ internal/project/stage.go | 5 +++++ internal/project/type.go | 36 ++++++++++++++++++++++++--------- internal/project/type_test.go | 10 ++++----- 9 files changed, 63 insertions(+), 33 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 2a2f0a0..79f5c4d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -117,7 +117,7 @@ func main() { } if event.Environment != "" && event.Stage == "" && event.Cluster == "" { - env := projectConfig.Environments[event.Environment] + env := projectConfig.GetEnvironment(event.Environment) err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, env.Actions) if err != nil { fmt.Println(err) @@ -126,7 +126,7 @@ func main() { } if event.Environment != "" && event.Stage != "" && event.Cluster == "" { - stage := projectConfig.Environments[event.Environment].Stages[event.Stage] + stage := projectConfig.GetStage(event.Environment, event.Stage) err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, stage.Actions) if err != nil { fmt.Println(err) @@ -135,7 +135,7 @@ func main() { } if event.Environment != "" && event.Stage != "" && event.Cluster != "" { - cluster := projectConfig.Cluster(event.Environment, event.Stage, event.Cluster) + cluster := projectConfig.GetCluster(event.Environment, event.Stage, event.Cluster) if event.Type == menu.EventTypeCreate || event.Type == menu.EventTypeUpdate { err := cluster.Render(projectConfig, event.Environment, event.Stage) if err != nil { diff --git a/internal/menu/cluster.go b/internal/menu/cluster.go index e8c9cfa..e1f96c0 100644 --- a/internal/menu/cluster.go +++ b/internal/menu/cluster.go @@ -87,7 +87,7 @@ func (c *clusterMenu) menuSettings(env, stage string, cluster *project.Cluster) // menuUpdateCluster creates a context menu to update an existing cluster func (c *clusterMenu) menuUpdateCluster(envName, stageName, clusterName string) (*project.Cluster, error) { - cluster := c.config.Cluster(envName, stageName, clusterName) + cluster := c.config.GetCluster(envName, stageName, clusterName) if cluster.Name == "" { cluster.Name = clusterName } @@ -108,7 +108,7 @@ func (c *clusterMenu) menuDeleteCluster(env, stage, cluster string) (*project.Cl if !confirmation { return nil, fmt.Errorf("confirmation denied") } - return c.config.Cluster(env, stage, cluster), nil + return c.config.GetCluster(env, stage, cluster), nil } func (c *clusterMenu) menuClusterSettingsProperties(env, stage string, cluster *project.Cluster) (map[string]string, error) { diff --git a/internal/menu/environment.go b/internal/menu/environment.go index 72c1478..b35930a 100644 --- a/internal/menu/environment.go +++ b/internal/menu/environment.go @@ -23,7 +23,7 @@ func (e *environmentMenu) menuCreateEnvironment() (*project.Environment, error) if s == "" { return fmt.Errorf("environment name cannot be empty") } - if _, ok := e.config.Environments[s]; ok { + if e.config.HasEnvironment(s) { return fmt.Errorf("environment already exists") } return nil @@ -48,8 +48,7 @@ func (e *environmentMenu) menuCreateEnvironment() (*project.Environment, error) } func (e *environmentMenu) menuUpdateEnvironment(envName string) (*project.Environment, error) { - environment := e.config.Environments[envName] - environment.Name = envName + environment := e.config.GetEnvironment(envName) if environment.Addons == nil { environment.Addons = map[string]*project.ClusterAddon{} } @@ -105,9 +104,9 @@ func (e *environmentMenu) menuDeleteEnvironment(envName string) (*project.Enviro if !confirmation { return nil, fmt.Errorf("confirmation denied") } - environment := *e.config.Environments[envName] + environment := e.config.GetEnvironment(envName) environment.Name = envName - return &environment, errors.New("menuDeleteEnvironment not implemented") + return environment, errors.New("menuDeleteEnvironment not implemented") } func (e *environmentMenu) menuEnvironmentProperties(env *project.Environment) (map[string]string, error) { diff --git a/internal/menu/root.go b/internal/menu/root.go index a25e363..676a52c 100644 --- a/internal/menu/root.go +++ b/internal/menu/root.go @@ -67,7 +67,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { } eventCh <- newPreUpdateEvent(EventOriginEnvironment, environment.Name, "", "") - config.Environments[*env].Properties = environment.Properties + config.GetEnvironment(environment.Name).Properties = environment.Properties eventCh <- newPostUpdateEvent(EventOriginEnvironment, environment.Name, "", "") return nil } @@ -110,7 +110,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { eventCh <- newPreCreateEvent(EventOriginStage, *envName, stage.Name, "") // add stage to config - config.Environments[*envName].Stages[stage.Name] = stage + config.GetEnvironment(*envName).Stages[stage.Name] = stage eventCh <- newPostCreateEvent(EventOriginStage, *envName, stage.Name, "") return nil } @@ -128,7 +128,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { eventCh <- newPreUpdateEvent(EventOriginStage, *envName, *stageName, "") // update stage in config - config.Environments[*envName].Stages[stage.Name] = stage + config.GetEnvironment(*envName).Stages[stage.Name] = stage eventCh <- newPostUpdateEvent(EventOriginStage, *envName, stage.Name, "") return nil } @@ -295,7 +295,7 @@ func menuSelectEnvironment(config *project.ProjectConfig) (*string, error) { func menuSelectStage(config *project.ProjectConfig, environment string) (*string, error) { prompt := promptui.Select{ Label: "Select Stage", - Items: append(utils.MapKeysToList(config.Environments[environment].Stages), rootOptionDone), + Items: append(utils.MapKeysToList(config.GetEnvironment(environment).Stages), rootOptionDone), } _, result, err := prompt.Run() if err != nil { @@ -329,7 +329,7 @@ func menuHierarchySelectEnvironmentStage(config *project.ProjectConfig) (*string func menuSelectCluster(config *project.ProjectConfig, environment, stage string) (*string, error) { prompt := promptui.Select{ Label: "Select Cluster", - Items: append(utils.MapKeysToList(config.Environments[environment].Stages[stage].Clusters), rootOptionDone), + Items: append(utils.MapKeysToList(config.GetStage(environment, stage).Clusters), rootOptionDone), } _, result, err := prompt.Run() if err != nil { diff --git a/internal/menu/stage.go b/internal/menu/stage.go index 81ea313..51cca38 100644 --- a/internal/menu/stage.go +++ b/internal/menu/stage.go @@ -23,7 +23,7 @@ func (s *stageMenu) menuCreateStage(env string) (*project.Stage, error) { if str == "" { return fmt.Errorf("stage name cannot be empty") } - if _, ok := s.config.Environments[env].Stages[str]; ok { + if s.config.GetEnvironment(env).HasStage(str) { return fmt.Errorf("stage already exists") } return nil @@ -49,8 +49,7 @@ func (s *stageMenu) menuCreateStage(env string) (*project.Stage, error) { } func (s *stageMenu) menuUpdateStage(envName, stageName string) (*project.Stage, error) { - stage := s.config.Environments[envName].Stages[stageName] - stage.Name = stageName + stage := s.config.GetStage(envName, stageName) if stage.Addons == nil { stage.Addons = map[string]*project.ClusterAddon{} } diff --git a/internal/project/environment.go b/internal/project/environment.go index be1e58e..e15f5e0 100644 --- a/internal/project/environment.go +++ b/internal/project/environment.go @@ -50,3 +50,14 @@ func (e *Environment) GetAddons() ClusterAddons { func (e *Environment) GetAddon(name string) *ClusterAddon { return e.Addons[name] } + +// HasStage checks if a stage exists in the environment +func (e *Environment) HasStage(name string) bool { + _, ok := e.Stages[name] + return ok +} + +// GetStage returns the stage by name +func (e *Environment) GetStage(name string) *Stage { + return e.Stages[name] +} diff --git a/internal/project/stage.go b/internal/project/stage.go index 17f9b59..c218922 100644 --- a/internal/project/stage.go +++ b/internal/project/stage.go @@ -50,3 +50,8 @@ func (s *Stage) GetAddons() ClusterAddons { func (s *Stage) GetAddon(name string) *ClusterAddon { return s.Addons[name] } + +// GetCluster returns the cluster by name +func (s *Stage) GetCluster(name string) *Cluster { + return s.Clusters[name] +} diff --git a/internal/project/type.go b/internal/project/type.go index 22c4dd4..c643016 100644 --- a/internal/project/type.go +++ b/internal/project/type.go @@ -22,29 +22,25 @@ type ProjectConfig struct { // HasCluster checks if a cluster exists in the given environment and stage func (p ProjectConfig) HasCluster(env, stage, cluster string) bool { - _, ok := p.Environments[env].Stages[stage].Clusters[cluster] + _, ok := p.GetStage(env, stage).Clusters[cluster] return ok } -func (p ProjectConfig) Cluster(env, stage, cluster string) *Cluster { - return p.Environments[env].Stages[stage].Clusters[cluster] -} - // SetCluster sets the cluster for the given environment and stage func (p *ProjectConfig) SetCluster(env, stage string, cluster *Cluster) { - if p.Environments[env].Stages[stage].Clusters == nil { - p.Environments[env].Stages[stage].Clusters = map[string]*Cluster{} + if p.GetStage(env, stage).Clusters == nil { + p.GetStage(env, stage).Clusters = map[string]*Cluster{} } - p.Environments[env].Stages[stage].Clusters[cluster.Name] = cluster + p.GetStage(env, stage).Clusters[cluster.Name] = cluster } func (p *ProjectConfig) DeleteCluster(env, stage, cluster string) { - delete(p.Environments[env].Stages[stage].Clusters, cluster) + delete(p.GetStage(env, stage).Clusters, cluster) } // EnvStageProperty merges the properties of the environment and stage and returns them as a map func (pc *ProjectConfig) EnvStageProperty(environment, stage string) map[string]string { - return utils.MergeMaps(pc.Environments[environment].Properties, pc.Environments[environment].Stages[stage].Properties) + return utils.MergeMaps(pc.GetEnvironment(environment).Properties, pc.GetStage(environment, stage).Properties) } // AddonGroups returns a list of addon groups that have been defined in the addons @@ -58,3 +54,23 @@ func (p ProjectConfig) AddonGroups() []string { } return utils.MapKeysToList(groups) } + +// HasEnvironment checks if an environment exists in the project +func (p ProjectConfig) HasEnvironment(name string) bool { + _, ok := p.Environments[name] + return ok +} + +func (p *ProjectConfig) GetEnvironment(name string) *Environment { + p.Environments[name].Name = name + return p.Environments[name] +} + +func (p *ProjectConfig) GetStage(env, stage string) *Stage { + p.GetEnvironment(env).GetStage(stage).Name = stage + return p.GetEnvironment(env).GetStage(stage) +} + +func (p *ProjectConfig) GetCluster(env, stage, cluster string) *Cluster { + return p.GetStage(env, stage).GetCluster(cluster) +} diff --git a/internal/project/type_test.go b/internal/project/type_test.go index 1e77d5f..7816b1d 100644 --- a/internal/project/type_test.go +++ b/internal/project/type_test.go @@ -88,7 +88,7 @@ func TestProjectConfig_HasCluster(t *testing.T) { } } -func TestProjectConfig_Cluster(t *testing.T) { +func TestProjectConfig_GetCluster(t *testing.T) { type fields struct { BasePath string TemplateBasePath string @@ -172,10 +172,10 @@ func TestProjectConfig_Cluster(t *testing.T) { Environments: tt.fields.Environments, } - got := p.Cluster(tt.args.env, tt.args.stage, tt.args.cluster) + got := p.GetCluster(tt.args.env, tt.args.stage, tt.args.cluster) diff := cmp.Diff(got, tt.want) if diff != "" { - t.Errorf("ProjectConfig.Cluster() mismatch (-got +want):\n%s", diff) + t.Errorf("ProjectConfig.GetCluster() mismatch (-got +want):\n%s", diff) return } }) @@ -241,7 +241,7 @@ func TestProjectConfig_SetCluster(t *testing.T) { } p.SetCluster(tt.args.env, tt.args.stage, tt.args.cluster) - diff := cmp.Diff(p.Environments[tt.args.env].Stages[tt.args.stage].Clusters[tt.args.cluster.Name], tt.want) + diff := cmp.Diff(p.GetCluster(tt.args.env, tt.args.stage, tt.args.cluster.Name), tt.want) if diff != "" { t.Errorf("ProjectConfig.SetCluster() mismatch (-got +want):\n%s", diff) return @@ -327,7 +327,7 @@ func TestProjectConfig_DeleteCluster(t *testing.T) { } p.DeleteCluster(tt.args.env, tt.args.stage, tt.args.cluster) - diff := cmp.Diff(p.Environments[tt.args.env].Stages[tt.args.stage].Clusters, tt.want) + diff := cmp.Diff(p.GetStage(tt.args.env, tt.args.stage).Clusters, tt.want) if diff != "" { t.Errorf("ProjectConfig.DeleteCluster() mismatch (-got +want):\n%s", diff) return From 17bdd5d137e0ef0782873b214d4bcf57596eb7f1 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 23:11:30 +0100 Subject: [PATCH 16/26] feat: add cluster properties to addon rendering --- internal/project/cluster.go | 13 ++++++++----- internal/template/addon.go | 9 +++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/project/cluster.go b/internal/project/cluster.go index b295827..fddb8bd 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -65,6 +65,8 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return fmt.Errorf("failed to load base templates: %w", err) } + envConfig := config.GetEnvironment(env) + stageConfig := envConfig.GetStage(stage) addons := map[string]template.AddonData{} for k, v := range c.Addons { if !v.Enabled { @@ -72,7 +74,7 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { } addons[k] = template.AddonData{ Annotations: config.ParsedAddons[k].Annotations, - Properties: v.Properties, + Properties: utils.MergeMaps(envConfig.GetAddon(k).Properties, stageConfig.GetAddon(k).Properties, v.Properties), } } @@ -97,10 +99,11 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return fmt.Errorf("failed to load addon %s templates: %w, value: %+v", addonName, err, config.ParsedAddons[addonName]) } err = atc.Render(config.BasePath, template.AddonTemplateData{ - Environment: env, - Stage: stage, - Cluster: c.Name, - Properties: addonValue.Properties, + Environment: env, + Stage: stage, + Cluster: c.Name, + ClusterProperties: properties, + Properties: addonValue.Properties, }) if err != nil { return fmt.Errorf("failed to render addon: %s, Error: %w", addonName, err) diff --git a/internal/template/addon.go b/internal/template/addon.go index 2d70a5d..0871b0d 100644 --- a/internal/template/addon.go +++ b/internal/template/addon.go @@ -74,10 +74,11 @@ func LoadTemplatesFromAddonManifest(source TemplateManifest) (*AddonTemplateCarr } type AddonTemplateData struct { - Environment string - Stage string - Cluster string - Properties map[string]any + Environment string + Stage string + Cluster string + ClusterProperties map[string]string + Properties map[string]any } func (a AddonTemplateCarrier) Render(basePath string, properties AddonTemplateData) error { From 5928fc4f59408c45618da7d8301ddce1a1ba0b8c Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 23:19:29 +0100 Subject: [PATCH 17/26] feat: add cluster properties to addon config --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 38c4f92..813c775 100644 --- a/README.md +++ b/README.md @@ -246,3 +246,4 @@ No matter if you define an addon or a template, you always have access to the fo | `{{ .Stage }}` | addon, tempate | The stage variable returns the name of the stage we are currently in | | `{{ .Cluster }}` | addon, tempate | The cluster variable returns the name of the cluster we are currently in | | `{{ .Properties. }}` | addon, tempate | The properties variable returns the value of the property with the key ``. The property keys in addons differ from the property keys in the template, as the addon does not currently have access to the environment, stage or cluster properties. In order for the addon to have properties available, you must define a property key in the `manifest.yaml` file. All properties defined there are then available for your addon template files. | +| `{{ .ClusterProperties. }}` | addon | The cluster properties is a map that contains all properties that are defined for the cluster. | From d30ba87ead2621a8e67bb5fe17e6ceac7d1d0035 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 23:34:15 +0100 Subject: [PATCH 18/26] chore: remove overlays folder from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7c04eea..b084e08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -overlays/ dist/ PROJECT.yaml From e44cab17dfacf3756603250d59b74d93cdfc01e5 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Sun, 16 Mar 2025 23:39:41 +0100 Subject: [PATCH 19/26] feat: add example PROJECT.yaml, templates and addons --- PROJECT.example.yaml | 69 +++++++++++++++++++ _example/README.md | 3 + .../dev/hugi/app-of-apps/kustomization.yaml | 11 +++ .../dev/dev/hugi/app-of-apps/values.yaml | 58 ++++++++++++++++ .../kyverno/kustomization.yaml | 5 ++ .../cluster-configs/kyverno/something.yaml | 1 + .../monitoring/kustomization.yaml | 5 ++ .../policies/cluster-policies/abc.yaml | 2 + .../policies/cluster-policies/abc2.yaml | 2 + .../policies/cluster-policies/config/abc.yaml | 2 + .../cluster-policies/config/sub/abc2.yaml | 2 + .../config/sub/kustomization.yaml | 5 ++ .../cluster-policies/kustomization.yaml | 5 ++ .../addons/cluster-policies/config/abc.yaml | 2 + .../cluster-policies/config/sub/abc2.yaml | 2 + .../cluster-policies/kustomization.yaml | 8 +++ .../addons/cluster-policies/manifest.yaml | 12 ++++ .../addons/disco-operator/kustomization.yaml | 8 +++ .../addons/disco-operator/manifest.yaml | 23 +++++++ .../source/addons/disco-operator/patch.yaml | 3 + .../source/addons/kyverno/kustomization.yaml | 5 ++ _example/source/addons/kyverno/manifest.yaml | 7 ++ _example/source/addons/kyverno/something.yaml | 1 + .../addons/monitoring/kustomization.yaml | 5 ++ .../source/addons/monitoring/manifest.yaml | 12 ++++ .../templates/appofapps/kustomization.yaml | 11 +++ .../source/templates/appofapps/manifest.yaml | 13 ++++ .../source/templates/appofapps/values.yaml | 45 ++++++++++++ 28 files changed, 327 insertions(+) create mode 100644 PROJECT.example.yaml create mode 100644 _example/README.md create mode 100644 _example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml create mode 100644 _example/overlays/dev/dev/hugi/app-of-apps/values.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml create mode 100644 _example/source/addons/cluster-policies/config/abc.yaml create mode 100644 _example/source/addons/cluster-policies/config/sub/abc2.yaml create mode 100644 _example/source/addons/cluster-policies/kustomization.yaml create mode 100644 _example/source/addons/cluster-policies/manifest.yaml create mode 100644 _example/source/addons/disco-operator/kustomization.yaml create mode 100644 _example/source/addons/disco-operator/manifest.yaml create mode 100644 _example/source/addons/disco-operator/patch.yaml create mode 100644 _example/source/addons/kyverno/kustomization.yaml create mode 100644 _example/source/addons/kyverno/manifest.yaml create mode 100644 _example/source/addons/kyverno/something.yaml create mode 100644 _example/source/addons/monitoring/kustomization.yaml create mode 100644 _example/source/addons/monitoring/manifest.yaml create mode 100644 _example/source/templates/appofapps/kustomization.yaml create mode 100644 _example/source/templates/appofapps/manifest.yaml create mode 100644 _example/source/templates/appofapps/values.yaml diff --git a/PROJECT.example.yaml b/PROJECT.example.yaml new file mode 100644 index 0000000..309e6c4 --- /dev/null +++ b/PROJECT.example.yaml @@ -0,0 +1,69 @@ +addons: + cluster-policies: + defaultEnabled: true + group: cluster-configs/policies + path: _example/source/addons/cluster-policies + disco-operator: + defaultEnabled: true + group: cluster-configs/policies + path: _example/source/addons/disco-operator + kyverno: + defaultEnabled: true + group: cluster-configs + path: _example/source/addons/kyverno + monitoring: + defaultEnabled: false + group: cluster-configs + path: _example/source/addons/monitoring +basePath: _example/overlays +environments: + dev: + actions: + postCreateHooks: null + postUpdateHooks: null + preCreateHooks: null + preUpdateHooks: null + addons: + cluster-policies: + enabled: true + properties: + enableNetworkPolicies: true + disco-operator: + enabled: true + properties: + isSuperCool: true + requiredDefaultNotSet: 10 + second: Hello World + properties: + gitBranch: develop + gitURL: https://github.com/leonsteinhaeuser/openshift-gitops-cli.git + stages: + dev: + actions: + postCreateHooks: null + postUpdateHooks: null + preCreateHooks: null + preUpdateHooks: null + addons: + cluster-policies: + enabled: true + properties: + enableNetworkPolicies: false + clusters: + hugi: + addons: + cluster-policies: + enabled: false + properties: {} + kyverno: + enabled: true + properties: {} + monitoring: + enabled: true + properties: + ingress_host: https://monitoring.2.external.url + properties: + destinationNamespace: openshift-gitops + properties: + destinationServer: https://kubernetes.default.svc +templateBasePath: _example/source/templates diff --git a/_example/README.md b/_example/README.md new file mode 100644 index 0000000..244834b --- /dev/null +++ b/_example/README.md @@ -0,0 +1,3 @@ +# Example + +This folder contains an example for the openshift-gitops-cli. The `PROJECT.example.yaml` file contains the configuration for the example project. The `PROJECT.example.yaml` file is an example project config referencing the templates and addons in the `_example/source` folder. The `_example/overlys` folder contains the rendered configuration defined by the cluster defined in the PROJECT.example.yaml file. diff --git a/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml b/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml new file mode 100644 index 0000000..c6e223c --- /dev/null +++ b/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmGlobals: + chartHome: ../../../../../../charts/ +helmCharts: + - name: argocd-app-of-app + version: 0.4.0 + valuesFile: values.yaml + namespace: openshift-gitops + releaseName: argocd-app-of-app-0.4.0 diff --git a/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml new file mode 100644 index 0000000..da3c754 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml @@ -0,0 +1,58 @@ +--- +appSuffix: "hugi-dev" +appSourceBasePath: overlays/dev/dev/hugi/cluster-config + +default: + app: + annotations: + argocd.argoproj.io/compare-options: IgnoreExtraneous + enabled: true + enableAutoSync: true + autoSyncPrune: true + project: hub + destination: + namespace: openshift-gitops + server: https://kubernetes.default.svc + source: + repoURL: https://git.external.url/repo/name.git + targetRevision: develop + +projects: + hub: + annotations: + argocd.argoproj.io/sync-wave: "-2" + description: Project for cluster hub + namespace: openshift-gitops + sourceRepos: + - https://git.external.url/repo/name.git + destinations: | + - namespace: '*' + server: https://kubernetes.default.svc + extraFields: | + clusterResourceWhitelist: + - group: '*' + kind: '*' + +applications: + kyverno: + annotations: + {} + source: + path: kyverno + labels: + app.kubernetes.io/managed-by: argocd + monitoring: + annotations: + {} + source: + path: monitoring + labels: + app.kubernetes.io/managed-by: argocd +-by: argocd + monitoring: + annotations: + {} + source: + path: monitoring + labels: + app.kubernetes.io/managed-by: argocd diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml new file mode 100644 index 0000000..b7e65e5 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/kyverno/" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml new file mode 100644 index 0000000..0c46315 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml @@ -0,0 +1 @@ +value: pair \ No newline at end of file diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml new file mode 100644 index 0000000..0883773 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/monitoring" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml new file mode 100644 index 0000000..190cb8d --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml new file mode 100644 index 0000000..190cb8d --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml diff --git a/_example/source/addons/cluster-policies/config/abc.yaml b/_example/source/addons/cluster-policies/config/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/source/addons/cluster-policies/config/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/source/addons/cluster-policies/config/sub/abc2.yaml b/_example/source/addons/cluster-policies/config/sub/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/source/addons/cluster-policies/config/sub/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/source/addons/cluster-policies/kustomization.yaml b/_example/source/addons/cluster-policies/kustomization.yaml new file mode 100644 index 0000000..3209548 --- /dev/null +++ b/_example/source/addons/cluster-policies/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml + {{- if .Properties.enableNetworkPolicies}} + - "../../../../../../base/base-config/cluster-policies/" + {{- end}} diff --git a/_example/source/addons/cluster-policies/manifest.yaml b/_example/source/addons/cluster-policies/manifest.yaml new file mode 100644 index 0000000..c2c8acb --- /dev/null +++ b/_example/source/addons/cluster-policies/manifest.yaml @@ -0,0 +1,12 @@ +name: cluster-policies +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + enableNetworkPolicies: + required: false + default: false + type: bool + description: "Whether to enable the cluster wide network policies" +files: + - ./ diff --git a/_example/source/addons/disco-operator/kustomization.yaml b/_example/source/addons/disco-operator/kustomization.yaml new file mode 100644 index 0000000..50f4641 --- /dev/null +++ b/_example/source/addons/disco-operator/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/disco-operator/" +patches: +- name: {{ .Properties.second }} + file: patch.yaml diff --git a/_example/source/addons/disco-operator/manifest.yaml b/_example/source/addons/disco-operator/manifest.yaml new file mode 100644 index 0000000..64f905f --- /dev/null +++ b/_example/source/addons/disco-operator/manifest.yaml @@ -0,0 +1,23 @@ +name: disco-operator +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + isSuperCool: + required: false + default: false + type: bool + description: "Is this app super cool?" + second: + required: true + default: "Hello World" + type: string + description: "Second?" + requiredDefaultNotSet: + required: true + default: null + type: int + description: "An empty property that is required" +files: + - kustomization.yaml + - patch.yaml diff --git a/_example/source/addons/disco-operator/patch.yaml b/_example/source/addons/disco-operator/patch.yaml new file mode 100644 index 0000000..b2cec3f --- /dev/null +++ b/_example/source/addons/disco-operator/patch.yaml @@ -0,0 +1,3 @@ +--- +some: yaml +key: {{ .Properties.isSuperCool }} diff --git a/_example/source/addons/kyverno/kustomization.yaml b/_example/source/addons/kyverno/kustomization.yaml new file mode 100644 index 0000000..b7e65e5 --- /dev/null +++ b/_example/source/addons/kyverno/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/kyverno/" diff --git a/_example/source/addons/kyverno/manifest.yaml b/_example/source/addons/kyverno/manifest.yaml new file mode 100644 index 0000000..7f4bec1 --- /dev/null +++ b/_example/source/addons/kyverno/manifest.yaml @@ -0,0 +1,7 @@ +name: kyverno +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: {} +files: + - ./ diff --git a/_example/source/addons/kyverno/something.yaml b/_example/source/addons/kyverno/something.yaml new file mode 100644 index 0000000..0c46315 --- /dev/null +++ b/_example/source/addons/kyverno/something.yaml @@ -0,0 +1 @@ +value: pair \ No newline at end of file diff --git a/_example/source/addons/monitoring/kustomization.yaml b/_example/source/addons/monitoring/kustomization.yaml new file mode 100644 index 0000000..0883773 --- /dev/null +++ b/_example/source/addons/monitoring/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/monitoring" diff --git a/_example/source/addons/monitoring/manifest.yaml b/_example/source/addons/monitoring/manifest.yaml new file mode 100644 index 0000000..7d4eebc --- /dev/null +++ b/_example/source/addons/monitoring/manifest.yaml @@ -0,0 +1,12 @@ +name: cluster-policies +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + ingress_host: + required: false + default: false + type: string + description: "The host to expose grafana on" +files: + - ./ diff --git a/_example/source/templates/appofapps/kustomization.yaml b/_example/source/templates/appofapps/kustomization.yaml new file mode 100644 index 0000000..c6e223c --- /dev/null +++ b/_example/source/templates/appofapps/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmGlobals: + chartHome: ../../../../../../charts/ +helmCharts: + - name: argocd-app-of-app + version: 0.4.0 + valuesFile: values.yaml + namespace: openshift-gitops + releaseName: argocd-app-of-app-0.4.0 diff --git a/_example/source/templates/appofapps/manifest.yaml b/_example/source/templates/appofapps/manifest.yaml new file mode 100644 index 0000000..877b490 --- /dev/null +++ b/_example/source/templates/appofapps/manifest.yaml @@ -0,0 +1,13 @@ +name: app-of-apps +properties: + gitURL: + required: true + default: "" + descriptionL: "Please define the git URL ArgoCD should reference" + targetRevision: + required: false + default: "develop" + description: "Please define the git target revision ArgoCD should reference" +files: + - values.yaml + - kustomization.yaml diff --git a/_example/source/templates/appofapps/values.yaml b/_example/source/templates/appofapps/values.yaml new file mode 100644 index 0000000..b7db530 --- /dev/null +++ b/_example/source/templates/appofapps/values.yaml @@ -0,0 +1,45 @@ +--- +appSuffix: "{{ .ClusterName }}-{{ .Stage }}" +appSourceBasePath: overlays/{{ .Environment }}/{{ .Stage }}/{{ .ClusterName }}/cluster-config + +default: + app: + annotations: + argocd.argoproj.io/compare-options: IgnoreExtraneous + enabled: true + enableAutoSync: true + autoSyncPrune: true + project: hub + destination: + namespace: {{ .Properties.destinationNamespace }} + server: {{ .Properties.destinationServer }} + source: + repoURL: {{ .Properties.gitURL }} + targetRevision: {{ .Properties.gitTargetRevision }} + +projects: + hub: + annotations: + argocd.argoproj.io/sync-wave: "-2" + description: Project for cluster hub + namespace: openshift-gitops + sourceRepos: + - {{ .Properties.gitURL }} + destinations: | + - namespace: '*' + server: {{ .Properties.destinationServer }} + extraFields: | + clusterResourceWhitelist: + - group: '*' + kind: '*' + +applications: + {{- range $key, $value := .Addons }} + {{ $key }}: + annotations: + {{- $value.Annotations | toYaml | nindent 6 }} + source: + path: {{ $key }} + labels: + app.kubernetes.io/managed-by: argocd + {{- end }} From e4a6c9bde07bc5a5a426bf0b825976d98577cdb7 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 10:11:46 +0100 Subject: [PATCH 20/26] fix: check type for string --- internal/template/manifest.go | 6 +++++- internal/template/manifest_test.go | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/template/manifest.go b/internal/template/manifest.go index d877839..3f9fa2c 100644 --- a/internal/template/manifest.go +++ b/internal/template/manifest.go @@ -48,7 +48,11 @@ func (p PropertyType) checkType(value any) (any, error) { typeValue := reflect.ValueOf(value) switch p { case PropertyTypeString: - return typeValue.String(), nil + v, ok := value.(string) + if !ok { + return nil, fmt.Errorf("expected type %s, got %v", p, kind) + } + return v, nil case PropertyTypeBool: if kind == reflect.String { bl, err := strconv.ParseBool(typeValue.String()) diff --git a/internal/template/manifest_test.go b/internal/template/manifest_test.go index b3d88ef..1a00ffc 100644 --- a/internal/template/manifest_test.go +++ b/internal/template/manifest_test.go @@ -56,7 +56,7 @@ func TestPropertyType_checkType(t *testing.T) { args: args{ value: 42, }, - want: "42", + want: "", wantErr: true, }, { @@ -92,7 +92,7 @@ func TestPropertyType_checkType(t *testing.T) { args: args{ value: true, }, - want: "true", + want: "", wantErr: true, }, { @@ -112,6 +112,9 @@ func TestPropertyType_checkType(t *testing.T) { t.Errorf("PropertyType.checkType() error = %v, wantErr %v", err, tt.wantErr) return } + if tt.wantErr { + return + } diff := cmp.Diff(got, tt.want) if diff != "" { t.Errorf("PropertyType.checkType() mismatch (-want +got):\n%s", diff) From cd17aa9efe82773d223f0329fe5baf54e46e408b Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 11:43:11 +0100 Subject: [PATCH 21/26] chore: add further tests for cluster --- internal/project/cluster_test.go | 433 +++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) diff --git a/internal/project/cluster_test.go b/internal/project/cluster_test.go index 37ce188..ba3e812 100644 --- a/internal/project/cluster_test.go +++ b/internal/project/cluster_test.go @@ -1,6 +1,7 @@ package project import ( + "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -712,3 +713,435 @@ func TestCluster_DisableAddon(t *testing.T) { }) } } + +func TestCluster_GetAddons(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := c.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Cluster.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCluster_GetAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := c.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Cluster.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCluster_Render(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + config *ProjectConfig + env string + stage string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if err := c.Render(tt.args.config, tt.args.env, tt.args.stage); (err != nil) != tt.wantErr { + t.Errorf("Cluster.Render() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCluster_SetDefaultAddons(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + config *ProjectConfig + } + tests := []struct { + name string + fields fields + args args + wantAddons map[string]*ClusterAddon + }{ + { + name: "one addon with bool property false", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: false, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": false}, + }, + }, + }, + { + name: "one addon with bool property nil", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, + }, + }, + }, + { + name: "one addon with int property 10", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: 10, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": 10}, + }, + }, + }, + { + name: "one addon with int property nil", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, + }, + }, + }, + { + name: "one addon with string property 'Hello World'", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: "Hello World", + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "Hello World"}, + }, + }, + }, + { + name: "one addon with string property 'nil'", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + c.SetDefaultAddons(tt.args.config) + + diff := cmp.Diff(tt.wantAddons, c.Addons) + if diff != "" { + t.Errorf("Cluster.SetDefaultAddons() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} From cfaab7c4c16bf8b59520d748b3ac4b4c6eb6c1a0 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 11:48:04 +0100 Subject: [PATCH 22/26] chore: add unit tests for environment --- internal/project/environment_test.go | 308 +++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) diff --git a/internal/project/environment_test.go b/internal/project/environment_test.go index ffc5197..d8cbaa5 100644 --- a/internal/project/environment_test.go +++ b/internal/project/environment_test.go @@ -1,6 +1,7 @@ package project import ( + "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -372,3 +373,310 @@ func TestEnvironment_DisableAddon(t *testing.T) { }) } } + +func TestEnvironment_GetAddons(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_GetAddon(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_HasStage(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "no stages", + fields: fields{ + Stages: map[string]*Stage{}, + }, + args: args{ + name: "stage1", + }, + want: false, + }, + { + name: "one stage", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: true, + }, + { + name: "two stages and found", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + "stage2": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.HasStage(tt.args.name); got != tt.want { + t.Errorf("Environment.HasStage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_GetStage(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *Stage + }{ + { + name: "no stages", + fields: fields{ + Stages: map[string]*Stage{}, + }, + args: args{ + name: "stage1", + }, + want: nil, + }, + { + name: "one stage", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: &Stage{}, + }, + { + name: "two stages and found", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + "stage2": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: &Stage{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetStage(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetStage() = %v, want %v", got, tt.want) + } + }) + } +} From 86515af1c6d79e9f0fff702e8315c95a6b98f94d Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 11:52:02 +0100 Subject: [PATCH 23/26] chore: add unit tests for stage --- internal/project/stage_test.go | 266 +++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) diff --git a/internal/project/stage_test.go b/internal/project/stage_test.go index e4747d6..d0a276b 100644 --- a/internal/project/stage_test.go +++ b/internal/project/stage_test.go @@ -1,6 +1,7 @@ package project import ( + "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -372,3 +373,268 @@ func TestStage_DisableAddon(t *testing.T) { }) } } + +func TestStage_GetAddons(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_GetAddon(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_GetCluster(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *Cluster + }{ + { + name: "no clusters", + fields: fields{ + Clusters: map[string]*Cluster{}, + }, + args: args{ + name: "cluster1", + }, + want: nil, + }, + { + name: "one cluster", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + }, + }, + args: args{ + name: "cluster1", + }, + want: &Cluster{ + Name: "cluster1", + }, + }, + { + name: "two clusters", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + "cluster2": { + Name: "cluster2", + }, + }, + }, + args: args{ + name: "cluster1", + }, + want: &Cluster{ + Name: "cluster1", + }, + }, + { + name: "two clusters no found", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + "cluster2": { + Name: "cluster2", + }, + }, + }, + args: args{ + name: "cluster5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetCluster(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetCluster() = %v, want %v", got, tt.want) + } + }) + } +} From b2bed9c417271c653d9326cdf22a0cbf492e6653 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 15:22:25 +0100 Subject: [PATCH 24/26] chore: add tests for addon handling --- internal/menu/addon.go | 4 +- internal/menu/cluster.go | 2 +- internal/menu/environment.go | 2 +- internal/menu/stage.go | 2 +- internal/project/addon.go | 4 +- internal/project/addon_test.go | 866 +++++++++++++++++++++++++++++++ internal/project/cluster.go | 28 +- internal/project/cluster_test.go | 339 ------------ internal/template/engine.go | 1 + 9 files changed, 895 insertions(+), 353 deletions(-) create mode 100644 internal/project/addon_test.go diff --git a/internal/menu/addon.go b/internal/menu/addon.go index ca76184..5db65af 100644 --- a/internal/menu/addon.go +++ b/internal/menu/addon.go @@ -17,7 +17,7 @@ type addonClusterMenu struct { config *project.ProjectConfig } -func (a *addonClusterMenu) menuManageAddons(ah project.AddonHandler) error { +func (a *addonClusterMenu) menuManageAddons(ah project.AddonHandler, skipAddonValidation bool) error { for { prompt := promptui.Select{ Label: "Manage Addons", @@ -30,7 +30,7 @@ func (a *addonClusterMenu) menuManageAddons(ah project.AddonHandler) error { return err } if result == "Done" { - err := ah.GetAddons().AllRequiredPropertiesSet(a.config) + err := ah.GetAddons().AllRequiredPropertiesSet(a.config, skipAddonValidation) if err != nil { fmt.Println("Not all required properties are set", err) continue diff --git a/internal/menu/cluster.go b/internal/menu/cluster.go index e1f96c0..a4676a8 100644 --- a/internal/menu/cluster.go +++ b/internal/menu/cluster.go @@ -67,7 +67,7 @@ func (c *clusterMenu) menuSettings(env, stage string, cluster *project.Cluster) config: c.config, } - err := addon.menuManageAddons(cluster) + err := addon.menuManageAddons(cluster, true) if err != nil { return err } diff --git a/internal/menu/environment.go b/internal/menu/environment.go index b35930a..0799f4c 100644 --- a/internal/menu/environment.go +++ b/internal/menu/environment.go @@ -78,7 +78,7 @@ func (e *environmentMenu) menuSettings(environment *project.Environment) error { reader: e.reader, config: e.config, } - err := addon.menuManageAddons(environment) + err := addon.menuManageAddons(environment, true) if err != nil { return err } diff --git a/internal/menu/stage.go b/internal/menu/stage.go index 51cca38..0e4109b 100644 --- a/internal/menu/stage.go +++ b/internal/menu/stage.go @@ -79,7 +79,7 @@ func (s *stageMenu) menuSettings(stage *project.Stage) error { reader: s.reader, config: s.config, } - err := addon.menuManageAddons(stage) + err := addon.menuManageAddons(stage, true) if err != nil { return err } diff --git a/internal/project/addon.go b/internal/project/addon.go index 5c577c5..5d5d743 100644 --- a/internal/project/addon.go +++ b/internal/project/addon.go @@ -19,14 +19,14 @@ type AddonHandler interface { type ClusterAddons map[string]*ClusterAddon -func (ca ClusterAddons) AllRequiredPropertiesSet(config *ProjectConfig) error { +func (ca ClusterAddons) AllRequiredPropertiesSet(config *ProjectConfig, skipOnFailure bool) error { for addonName, addon := range ca { if !addon.Enabled { fmt.Printf("addon %s is disabled\n", addonName) continue } err := addon.AllRequiredPropertiesSet(config, addonName) - if err != nil { + if err != nil && !skipOnFailure { return fmt.Errorf("failed to validate addon %s: %w", addonName, err) } } diff --git a/internal/project/addon_test.go b/internal/project/addon_test.go new file mode 100644 index 0000000..06995c9 --- /dev/null +++ b/internal/project/addon_test.go @@ -0,0 +1,866 @@ +package project + +import ( + "testing" + + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" +) + +func TestClusterAddons_AllRequiredPropertiesSet(t *testing.T) { + type fields struct { + Addons ClusterAddons + } + type args struct { + config *ProjectConfig + skipOnFailure bool + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no addons defined", + fields: fields{ + Addons: ClusterAddons{}, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": true, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set string, expect bool", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect bool", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set bool, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": true, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set string, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set int, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set string, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := tt.fields.Addons + if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.skipOnFailure); (err != nil) != tt.wantErr { + t.Errorf("ClusterAddons.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestClusterAddon_AllRequiredPropertiesSet(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + type args struct { + config *ProjectConfig + addonName string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no properties defined in addon properties", + fields: fields{ + Properties: map[string]any{}, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set", + fields: fields{ + Properties: map[string]any{ + "property1": true, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set string, expect bool", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect bool", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set bool, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": true, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set string, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set int, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set string, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := &ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.addonName); (err != nil) != tt.wantErr { + t.Errorf("ClusterAddon.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestClusterAddons_IsEnabled(t *testing.T) { + type args struct { + addon string + } + tests := []struct { + name string + ca ClusterAddons + args args + want bool + }{ + { + name: "addon not enabled", + ca: ClusterAddons{ + "addon1": { + Enabled: false, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "addon enabled", + ca: ClusterAddons{ + "addon1": { + Enabled: true, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ca.IsEnabled(tt.args.addon); got != tt.want { + t.Errorf("ClusterAddons.IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClusterAddon_IsEnabled(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "addon not enabled", + fields: fields{ + Enabled: false, + }, + want: false, + }, + { + name: "addon enabled", + fields: fields{ + Enabled: true, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + if got := ca.IsEnabled(); got != tt.want { + t.Errorf("ClusterAddon.IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClusterAddon_SetProperty(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + type args struct { + key string + value any + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "set property", + fields: fields{ + Properties: map[string]any{}, + }, + args: args{ + key: "property1", + value: "value", + }, + }, + { + name: "set property, override", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + key: "property1", + value: "value2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := &ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + ca.SetProperty(tt.args.key, tt.args.value) + }) + } +} diff --git a/internal/project/cluster.go b/internal/project/cluster.go index fddb8bd..200f0f3 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -65,16 +65,13 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return fmt.Errorf("failed to load base templates: %w", err) } - envConfig := config.GetEnvironment(env) - stageConfig := envConfig.GetStage(stage) + addonProperties := c.AddonProperties(config, env, stage) addons := map[string]template.AddonData{} - for k, v := range c.Addons { - if !v.Enabled { - continue - } + for k, v := range addonProperties { addons[k] = template.AddonData{ + Enabled: v.Enabled, Annotations: config.ParsedAddons[k].Annotations, - Properties: utils.MergeMaps(envConfig.GetAddon(k).Properties, stageConfig.GetAddon(k).Properties, v.Properties), + Properties: v.Properties, } } @@ -137,3 +134,20 @@ func (c *Cluster) SetDefaultAddons(config *ProjectConfig) { c.Addons[addonName] = cAddon } } + +// AddonProperties returns the addon properties for the cluster merged with the environment and stage properties +func (c *Cluster) AddonProperties(config *ProjectConfig, env, stage string) map[string]*ClusterAddon { + properties := c.Addons + for addonName, addon := range c.Addons { + if !addon.Enabled { + // addon was disabled on the cluster level, we skip it + continue + } + addonProps := map[string]any{} + for key, property := range config.ParsedAddons[addonName].Properties { + addonProps[key] = property.Default + } + properties[addonName].Properties = utils.MergeMaps(addonProps, config.GetStage(env, stage).GetAddon(addonName).Properties, config.GetEnvironment(env).GetAddon(addonName).Properties, c.GetAddon(addonName).Properties) + } + return properties +} diff --git a/internal/project/cluster_test.go b/internal/project/cluster_test.go index ba3e812..8b03934 100644 --- a/internal/project/cluster_test.go +++ b/internal/project/cluster_test.go @@ -8,345 +8,6 @@ import ( "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" ) -func TestClusterAddon_AllRequiredPropertiesSet(t *testing.T) { - type fields struct { - Enabled bool - Properties map[string]any - } - type args struct { - config *ProjectConfig - addonName string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "no properties defined in addon properties", - fields: fields{ - Properties: map[string]any{}, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set", - fields: fields{ - Properties: map[string]any{ - "property1": true, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set string, expect bool", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect bool", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set bool, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": true, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set string, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": "value", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set int, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set string, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": "value", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set int, expect int, invalid value", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect int, invalid value", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ca := &ClusterAddon{ - Enabled: tt.fields.Enabled, - Properties: tt.fields.Properties, - } - if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.addonName); (err != nil) != tt.wantErr { - t.Errorf("ClusterAddon.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestCluster_IsAddonEnabled(t *testing.T) { type fields struct { Name string diff --git a/internal/template/engine.go b/internal/template/engine.go index 8d39e17..3da3ddc 100644 --- a/internal/template/engine.go +++ b/internal/template/engine.go @@ -30,6 +30,7 @@ type TemplateData struct { } type AddonData struct { + Enabled bool Annotations map[string]string Properties map[string]any } From 36a90fb8bd27a3e1a41379ebc9f06a4ce31ef1b8 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 16:51:23 +0100 Subject: [PATCH 25/26] fix: nil pointer when one of the addons is not defined on the stage or env level --- internal/project/cluster.go | 12 +- internal/project/cluster_test.go | 218 +++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 2 deletions(-) diff --git a/internal/project/cluster.go b/internal/project/cluster.go index 200f0f3..dbf3d42 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -136,18 +136,26 @@ func (c *Cluster) SetDefaultAddons(config *ProjectConfig) { } // AddonProperties returns the addon properties for the cluster merged with the environment and stage properties -func (c *Cluster) AddonProperties(config *ProjectConfig, env, stage string) map[string]*ClusterAddon { +func (c *Cluster) AddonProperties(config *ProjectConfig, env, stg string) map[string]*ClusterAddon { properties := c.Addons for addonName, addon := range c.Addons { if !addon.Enabled { // addon was disabled on the cluster level, we skip it continue } + envAddonProps := map[string]any{} + if env := config.GetEnvironment(env).GetAddon(addonName); env != nil { + envAddonProps = env.Properties + } + stageAddonProps := map[string]any{} + if stg := config.GetStage(env, stg).GetAddon(addonName); stg != nil { + stageAddonProps = stg.Properties + } addonProps := map[string]any{} for key, property := range config.ParsedAddons[addonName].Properties { addonProps[key] = property.Default } - properties[addonName].Properties = utils.MergeMaps(addonProps, config.GetStage(env, stage).GetAddon(addonName).Properties, config.GetEnvironment(env).GetAddon(addonName).Properties, c.GetAddon(addonName).Properties) + properties[addonName].Properties = utils.MergeMaps(addonProps, envAddonProps, stageAddonProps, c.GetAddon(addonName).Properties) } return properties } diff --git a/internal/project/cluster_test.go b/internal/project/cluster_test.go index 8b03934..c510288 100644 --- a/internal/project/cluster_test.go +++ b/internal/project/cluster_test.go @@ -806,3 +806,221 @@ func TestCluster_SetDefaultAddons(t *testing.T) { }) } } + +func TestCluster_AddonProperties(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + config *ProjectConfig + env string + stg string + } + tests := []struct { + name string + fields fields + args args + want map[string]*ClusterAddon + }{ + { + name: "one addon, cluster has highest priority", + fields: fields{ + Name: "cluster1", + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: "Hello World", + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + Environments: map[string]*Environment{ + "env1": { + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + }, + }, + }, + }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, + }, + }, + { + name: "one addon, stage has highest priority", + fields: fields{ + Name: "cluster1", + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: "Hello World", + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + Environments: map[string]*Environment{ + "env1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + }, + }, + }, + }, + }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, + }, + }, + { + name: "one addon, env has highest priority", + fields: fields{ + Name: "cluster1", + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: "Hello World", + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + Environments: map[string]*Environment{ + "env1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{}, + }, + }, + }, + }, + }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + got := c.AddonProperties(tt.args.config, tt.args.env, tt.args.stg) + diff := cmp.Diff(tt.want, got) + if diff != "" { + t.Errorf("Cluster.AddonProperties() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} From 3b0c7566f366a23c394ff4c6c4813c09778feef3 Mon Sep 17 00:00:00 2001 From: leonsteinhaeuser Date: Tue, 18 Mar 2025 17:08:49 +0100 Subject: [PATCH 26/26] chore: update example --- PROJECT.example.yaml | 11 ++++-- .../dev/dev/hugi/app-of-apps/values.yaml | 34 ++++++++++++------- .../cluster-policies/kustomization.yaml | 1 + .../disco-operator/kustomization.yaml | 8 +++++ .../policies/disco-operator/patch.yaml | 3 ++ .../source/templates/appofapps/values.yaml | 3 +- 6 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml create mode 100644 _example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml diff --git a/PROJECT.example.yaml b/PROJECT.example.yaml index 309e6c4..1bf9e26 100644 --- a/PROJECT.example.yaml +++ b/PROJECT.example.yaml @@ -53,8 +53,15 @@ environments: hugi: addons: cluster-policies: - enabled: false - properties: {} + enabled: true + properties: + enableNetworkPolicies: true + disco-operator: + enabled: true + properties: + isSuperCool: false + requiredDefaultNotSet: null + second: Hello World kyverno: enabled: true properties: {} diff --git a/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml index da3c754..170b913 100644 --- a/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml +++ b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml @@ -1,6 +1,6 @@ --- appSuffix: "hugi-dev" -appSourceBasePath: overlays/dev/dev/hugi/cluster-config +appSourceBasePath: overlays/dev/dev/hugi/cluster-configs default: app: @@ -14,8 +14,8 @@ default: namespace: openshift-gitops server: https://kubernetes.default.svc source: - repoURL: https://git.external.url/repo/name.git - targetRevision: develop + repoURL: https://github.com/leonsteinhaeuser/openshift-gitops-cli.git + targetRevision: projects: hub: @@ -24,7 +24,7 @@ projects: description: Project for cluster hub namespace: openshift-gitops sourceRepos: - - https://git.external.url/repo/name.git + - https://github.com/leonsteinhaeuser/openshift-gitops-cli.git destinations: | - namespace: '*' server: https://kubernetes.default.svc @@ -34,24 +34,34 @@ projects: kind: '*' applications: - kyverno: + cluster-policies: + enabled: true annotations: - {} + argocd.argoproj.io/sync-wave: "0" source: - path: kyverno + path: cluster-policies labels: app.kubernetes.io/managed-by: argocd - monitoring: + disco-operator: + enabled: true annotations: - {} + argocd.argoproj.io/sync-wave: "0" source: - path: monitoring + path: disco-operator + labels: + app.kubernetes.io/managed-by: argocd + kyverno: + enabled: true + annotations: + argocd.argoproj.io/sync-wave: "0" + source: + path: kyverno labels: app.kubernetes.io/managed-by: argocd --by: argocd monitoring: + enabled: true annotations: - {} + argocd.argoproj.io/sync-wave: "0" source: path: monitoring labels: diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml index 190cb8d..88616d9 100644 --- a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml @@ -3,3 +3,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - config/abc.yaml + - "../../../../../../base/base-config/cluster-policies/" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml new file mode 100644 index 0000000..451929d --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/disco-operator/" +patches: +- name: Hello World + file: patch.yaml diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml new file mode 100644 index 0000000..7fee6b0 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml @@ -0,0 +1,3 @@ +--- +some: yaml +key: false diff --git a/_example/source/templates/appofapps/values.yaml b/_example/source/templates/appofapps/values.yaml index b7db530..b1d51c1 100644 --- a/_example/source/templates/appofapps/values.yaml +++ b/_example/source/templates/appofapps/values.yaml @@ -1,6 +1,6 @@ --- appSuffix: "{{ .ClusterName }}-{{ .Stage }}" -appSourceBasePath: overlays/{{ .Environment }}/{{ .Stage }}/{{ .ClusterName }}/cluster-config +appSourceBasePath: overlays/{{ .Environment }}/{{ .Stage }}/{{ .ClusterName }}/cluster-configs default: app: @@ -36,6 +36,7 @@ projects: applications: {{- range $key, $value := .Addons }} {{ $key }}: + enabled: {{ $value.Enabled }} annotations: {{- $value.Annotations | toYaml | nindent 6 }} source: