diff --git a/examples/minimal-example-overwrite.yaml b/examples/minimal-example-overwrite.yaml new file mode 100644 index 00000000..d9bf732f --- /dev/null +++ b/examples/minimal-example-overwrite.yaml @@ -0,0 +1,7 @@ +## This is a minimal example. +## It will use your current kube context and will deploy Tiller without RBAC service account. +## For the full config spec and options, check https://github.com/Praqma/helmsman/blob/master/docs/desired_state_specification.md + +apps: + jenkins: + enabled: false diff --git a/internal/app/custom_types.go b/internal/app/custom_types.go new file mode 100644 index 00000000..e682673e --- /dev/null +++ b/internal/app/custom_types.go @@ -0,0 +1,83 @@ +package app + +import ( + "encoding/json" + "github.com/invopop/jsonschema" + "reflect" + "strconv" +) + +// truthy and falsy NullBool values +var ( + True = NullBool{HasValue: true, Value: true} + False = NullBool{HasValue: true, Value: false} +) + +// NullBool represents a bool that may be null. +type NullBool struct { + Value bool + HasValue bool // true if bool is not null +} + +func (b NullBool) MarshalJSON() ([]byte, error) { + value := b.HasValue && b.Value + return json.Marshal(value) +} + +func (b *NullBool) UnmarshalJSON(data []byte) error { + var unmarshalledJson bool + + err := json.Unmarshal(data, &unmarshalledJson) + if err != nil { + return err + } + + b.Value = unmarshalledJson + b.HasValue = true + + return nil +} + +func (b *NullBool) UnmarshalText(text []byte) error { + str := string(text) + if len(str) < 1 { + return nil + } + + value, err := strconv.ParseBool(str) + if err != nil { + return err + } + + b.HasValue = true + b.Value = value + + return nil +} + +// JSONSchema instructs the jsonschema generator to represent NullBool type as boolean +func (NullBool) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "boolean", + } +} + +type MergoTransformer func(typ reflect.Type) func(dst, src reflect.Value) error + +func (m MergoTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { + return m(typ) +} + +// NullBoolTransformer is a custom imdario/mergo transformer for the NullBool type +func NullBoolTransformer(typ reflect.Type) func(dst, src reflect.Value) error { + if typ != reflect.TypeOf(NullBool{}) { + return nil + } + + return func(dst, src reflect.Value) error { + if src.FieldByName("HasValue").Bool() { + dst.Set(src) + } + return nil + } +} diff --git a/internal/app/custom_types_test.go b/internal/app/custom_types_test.go new file mode 100644 index 00000000..079071e4 --- /dev/null +++ b/internal/app/custom_types_test.go @@ -0,0 +1,179 @@ +package app + +import ( + "bytes" + "encoding/json" + "reflect" + "testing" +) + +func TestNullBool_MarshalJSON(t *testing.T) { + tests := []struct { + name string + value NullBool + want []byte + wantErr bool + }{ + { + name: "should be false", + want: []byte(`false`), + wantErr: false, + }, + { + name: "should be true", + want: []byte(`true`), + value: NullBool{HasValue: true, Value: true}, + wantErr: false, + }, + { + name: "should be false when HasValue is false", + want: []byte(`false`), + value: NullBool{HasValue: false, Value: true}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.value.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalJSON() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNullBool_UnmarshalJSON(t *testing.T) { + type output struct { + Value NullBool `json:"value"` + } + tests := []struct { + name string + data []byte + want output + wantErr bool + }{ + { + name: "should have value set to false", + data: []byte(`{"value": false}`), + want: output{NullBool{HasValue: true, Value: false}}, + }, + { + name: "should have value set to true", + data: []byte(`{"value": true}`), + want: output{NullBool{HasValue: true, Value: true}}, + }, + { + name: "should have value unset", + data: []byte("{}"), + want: output{NullBool{HasValue: false, Value: false}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got output + if err := json.NewDecoder(bytes.NewReader(tt.data)).Decode(&got); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("UnmarshalJSON() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNullBool_UnmarshalText(t *testing.T) { + tests := []struct { + name string + text []byte + want NullBool + wantErr bool + }{ + { + name: "should have the value set to false", + text: []byte("false"), + want: NullBool{HasValue: true, Value: false}, + }, + { + name: "should have the value set to true", + text: []byte("false"), + want: NullBool{HasValue: true, Value: false}, + }, + { + name: "should have the value unset", + text: []byte(""), + want: NullBool{HasValue: false, Value: false}, + }, + { + name: "should return an error on wrong input", + text: []byte("wrong_input"), + wantErr: true, + want: NullBool{HasValue: false, Value: false}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got NullBool + if err := got.UnmarshalText(tt.text); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("UnmarshalText() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNullBoolTransformer(t *testing.T) { + type args struct { + dst NullBool + src NullBool + } + tests := []struct { + name string + args args + want NullBool + }{ + { + name: "should overwrite true to false when the dst has the value", + args: args{ + dst: NullBool{HasValue: true, Value: true}, + src: NullBool{HasValue: true, Value: false}, + }, + want: NullBool{HasValue: true, Value: false}, + }, + { + name: "shouldn't overwrite when the value is unset", + args: args{ + dst: NullBool{HasValue: true, Value: true}, + src: NullBool{HasValue: false, Value: false}, + }, + want: NullBool{HasValue: true, Value: true}, + }, + { + name: "shouldn overwrite when the value is set and equal true", + args: args{ + dst: NullBool{HasValue: true, Value: false}, + src: NullBool{HasValue: true, Value: true}, + }, + want: NullBool{HasValue: true, Value: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dst := tt.args.dst + src := tt.args.src + + transformer := NullBoolTransformer(reflect.TypeOf(NullBool{})) + + transformer(reflect.ValueOf(&dst).Elem(), reflect.ValueOf(src)) + + if !reflect.DeepEqual(dst, tt.want) { + t.Errorf("NullBoolTransformer() = %v, want %v", dst, tt.want) + } + }) + } +} diff --git a/internal/app/decision_maker.go b/internal/app/decision_maker.go index 23e4bc24..2413824a 100644 --- a/internal/app/decision_maker.go +++ b/internal/app/decision_maker.go @@ -115,7 +115,7 @@ func (cs *currentState) decide(r *Release, n *Namespace, p *plan, c *ChartInfo, return nil } - if !r.Enabled { + if !r.Enabled.Value { if ok := cs.releaseExists(r, ""); ok { p.addDecision(prefix+" is desired to be DELETED.", r.Priority, remove) r.uninstall(p) diff --git a/internal/app/decision_maker_test.go b/internal/app/decision_maker_test.go index f625c179..7a4ab16c 100644 --- a/internal/app/decision_maker_test.go +++ b/internal/app/decision_maker_test.go @@ -21,11 +21,11 @@ func Test_getValuesFiles(t *testing.T) { Name: "release1", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, // s: st, }, @@ -38,11 +38,11 @@ func Test_getValuesFiles(t *testing.T) { Name: "release1", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFiles: []string{"../../tests/values.yaml"}, - Test: true, + Test: True, }, // s: st, }, @@ -55,11 +55,11 @@ func Test_getValuesFiles(t *testing.T) { Name: "release1", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFiles: []string{"../../tests/values.yaml", "../../tests/values2.yaml"}, - Test: true, + Test: True, }, // s: st, }, @@ -93,7 +93,7 @@ func Test_inspectUpgradeScenario(t *testing.T) { Namespace: "namespace", Version: "1.0.0", Chart: "./../../tests/chart-test", - Enabled: true, + Enabled: True, }, s: &map[string]helmRelease{ "release1-namespace": { @@ -151,7 +151,7 @@ func Test_decide(t *testing.T) { "release1": { Name: "release1", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -170,7 +170,7 @@ func Test_decide(t *testing.T) { "release1": { Name: "release1", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -189,7 +189,7 @@ func Test_decide(t *testing.T) { "release4": { Name: "release4", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -208,7 +208,7 @@ func Test_decide(t *testing.T) { "thisRelease": { Name: "thisRelease", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -227,7 +227,7 @@ func Test_decide(t *testing.T) { "thisRelease": { Name: "thisRelease", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -246,7 +246,7 @@ func Test_decide(t *testing.T) { "thisRelease": { Name: "thisRelease", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -265,7 +265,7 @@ func Test_decide(t *testing.T) { "thisRelease": { Name: "thisRelease", Namespace: "namespace", - Enabled: true, + Enabled: True, Group: "myGroup", }, }, @@ -318,12 +318,12 @@ func Test_decide_skip_ignored_apps(t *testing.T) { "service1": { Name: "service1", Namespace: "namespace", - Enabled: true, + Enabled: True, }, "service2": { Name: "service2", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -340,12 +340,12 @@ func Test_decide_skip_ignored_apps(t *testing.T) { "service1": { Name: "service1", Namespace: "namespace", - Enabled: true, + Enabled: True, }, "service2": { Name: "service2", Namespace: "namespace", - Enabled: true, + Enabled: True, }, }, }, @@ -400,7 +400,7 @@ func Test_decide_group(t *testing.T) { Name: "release1", Namespace: "namespace", Group: "run-me-not", - Enabled: true, + Enabled: True, }, }, }, @@ -417,19 +417,19 @@ func Test_decide_group(t *testing.T) { Name: "release1", Namespace: "namespace", Group: "run-me", - Enabled: true, + Enabled: True, }, "release2": { Name: "release2", Namespace: "namespace", Group: "run-me-not", - Enabled: true, + Enabled: True, }, "release3": { Name: "release3", Namespace: "namespace2", Group: "run-me-not", - Enabled: true, + Enabled: True, }, }, }, diff --git a/internal/app/helm_helpers_test.go b/internal/app/helm_helpers_test.go index 49491ab0..56fb4f15 100644 --- a/internal/app/helm_helpers_test.go +++ b/internal/app/helm_helpers_test.go @@ -23,7 +23,7 @@ func Test_getChartInfo(t *testing.T) { Namespace: "namespace", Version: "1.0.0", Chart: "./../../tests/chart-test", - Enabled: true, + Enabled: True, }, }, want: &ChartInfo{Name: "chart-test", Version: "1.0.0"}, @@ -36,7 +36,7 @@ func Test_getChartInfo(t *testing.T) { Namespace: "namespace", Version: "1.0.*", Chart: "./../../tests/chart-test", - Enabled: true, + Enabled: True, }, }, want: &ChartInfo{Name: "chart-test", Version: "1.0.0"}, @@ -49,7 +49,7 @@ func Test_getChartInfo(t *testing.T) { Namespace: "namespace", Version: "1.0.0", Chart: "random-chart-name-1f8147", - Enabled: true, + Enabled: True, }, }, want: nil, @@ -62,7 +62,7 @@ func Test_getChartInfo(t *testing.T) { Namespace: "namespace", Version: "0.9.0", Chart: "./../../tests/chart-test", - Enabled: true, + Enabled: True, }, }, want: nil, diff --git a/internal/app/release.go b/internal/app/release.go index dd2a01dc..8695329f 100644 --- a/internal/app/release.go +++ b/internal/app/release.go @@ -17,9 +17,9 @@ type Release struct { // Namespace where to deploy the helm release Namespace string `json:"namespace"` // Enabled can be used to togle a helm release - Enabled bool `json:"enabled"` - Group string `json:"group,omitempty"` - Chart string `json:"chart"` + Enabled NullBool `json:"enabled"` + Group string `json:"group,omitempty"` + Chart string `json:"chart"` // Version of the helm chart to deploy Version string `json:"version"` // ValuesFile is the path for a values file for the helm release @@ -33,11 +33,11 @@ type Release struct { // PostRenderer is the path to an executable to be used for post rendering PostRenderer string `json:"postRenderer,omitempty"` // Test indicates if the chart tests should be executed - Test bool `json:"test,omitempty"` + Test NullBool `json:"test,omitempty"` // Protected defines if the release should be protected against changes - Protected bool `json:"protected,omitempty"` + Protected NullBool `json:"protected,omitempty"` // Wait defines whether helm should block execution until all k8s resources are in a ready state - Wait bool `json:"wait,omitempty"` + Wait NullBool `json:"wait,omitempty"` // Priority allows defining the execution order, releases with the same priority can be executed in parallel Priority int `json:"priority,omitempty"` // Set can be used to overwrite the chart values @@ -51,7 +51,7 @@ type Release struct { // HelmDiffFlags is a list of cli flags to pass to helm diff HelmDiffFlags []string `json:"helmDiffFlags,omitempty"` // NoHooks can be used to disable the execution of helm hooks - NoHooks bool `json:"noHooks,omitempty"` + NoHooks NullBool `json:"noHooks,omitempty"` // Timeout is the number of seconds to wait for the release to complete Timeout int `json:"timeout,omitempty"` // Hooks can be used to define lifecycle hooks specific to this release @@ -162,7 +162,7 @@ func (r *Release) test(afterCommands *[]hookCmd) { func (r *Release) install(p *plan) { before, after := r.checkHooks("install") - if r.Test { + if r.Test.Value { r.test(&after) } @@ -234,7 +234,7 @@ func (r *Release) diff() (string, error) { func (r *Release) upgrade(p *plan) { before, after := r.checkHooks("upgrade") - if r.Test { + if r.Test.Value { r.test(&after) } @@ -292,7 +292,7 @@ func (r *Release) label(storageBackend string, labels ...string) { if len(labels) == 0 { return } - if r.Enabled { + if r.Enabled.Value { args := []string{"label", "--overwrite", storageBackend, "-n", r.Namespace, "-l", "owner=helm,name=" + r.Name} args = append(args, labels...) @@ -309,7 +309,7 @@ func (r *Release) annotate(storageBackend string, annotations ...string) { if len(annotations) == 0 { return } - if r.Enabled { + if r.Enabled.Value { args := []string{"annotate", "--overwrite", storageBackend, "-n", r.Namespace, "-l", "owner=helm,name=" + r.Name} args = append(args, annotations...) @@ -330,7 +330,7 @@ func (r *Release) isProtected(cs *currentState, n *Namespace) bool { if ok := cs.releaseExists(r, ""); !ok { return false } - if n.Protected || r.Protected { + if n.Protected || r.Protected.Value { return true } return false @@ -338,7 +338,7 @@ func (r *Release) isProtected(cs *currentState, n *Namespace) bool { // getNoHooks returns the no-hooks flag for install/upgrade commands func (r *Release) getNoHooks() []string { - if r.NoHooks { + if r.NoHooks.Value { return []string{"--no-hooks"} } return []string{} @@ -383,7 +383,7 @@ func (r *Release) getSetFileValues() []string { // Otherwise, retruns an empty string func (r *Release) getWait() []string { res := []string{} - if r.Wait { + if r.Wait.Value { res = append(res, "--wait") } return res @@ -595,15 +595,15 @@ func (r Release) print() { fmt.Println("\tname: ", r.Name) fmt.Println("\tdescription: ", r.Description) fmt.Println("\tnamespace: ", r.Namespace) - fmt.Println("\tenabled: ", r.Enabled) + fmt.Println("\tenabled: ", r.Enabled.Value) fmt.Println("\tchart: ", r.Chart) fmt.Println("\tversion: ", r.Version) fmt.Println("\tvaluesFile: ", r.ValuesFile) fmt.Println("\tvaluesFiles: ", strings.Join(r.ValuesFiles, ",")) fmt.Println("\tpostRenderer: ", r.PostRenderer) - fmt.Println("\ttest: ", r.Test) - fmt.Println("\tprotected: ", r.Protected) - fmt.Println("\twait: ", r.Wait) + fmt.Println("\ttest: ", r.Test.Value) + fmt.Println("\tprotected: ", r.Protected.Value) + fmt.Println("\twait: ", r.Wait.Value) fmt.Println("\tpriority: ", r.Priority) fmt.Println("\tSuccessCondition: ", r.Hooks["successCondition"]) fmt.Println("\tSuccessTimeout: ", r.Hooks["successTimeout"]) @@ -614,7 +614,7 @@ func (r Release) print() { fmt.Println("\tpostUpgrade: ", r.Hooks[postUpgrade]) fmt.Println("\tpreDelete: ", r.Hooks[preDelete]) fmt.Println("\tpostDelete: ", r.Hooks[postDelete]) - fmt.Println("\tno-hooks: ", r.NoHooks) + fmt.Println("\tno-hooks: ", r.NoHooks.Value) fmt.Println("\ttimeout: ", r.Timeout) fmt.Println("\tvalues to override from env:") printMap(r.Set, 2) diff --git a/internal/app/release_test.go b/internal/app/release_test.go index 3b6a4b35..e03b5834 100644 --- a/internal/app/release_test.go +++ b/internal/app/release_test.go @@ -30,11 +30,11 @@ func Test_release_validate(t *testing.T) { Name: "release1", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -46,11 +46,11 @@ func Test_release_validate(t *testing.T) { Name: "release2", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "xyz.yaml", - Test: true, + Test: True, }, s: st, }, @@ -62,11 +62,11 @@ func Test_release_validate(t *testing.T) { Name: "release3", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.xml", - Test: true, + Test: True, }, s: st, }, @@ -78,11 +78,11 @@ func Test_release_validate(t *testing.T) { Name: "release1", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -94,11 +94,11 @@ func Test_release_validate(t *testing.T) { Name: "", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -110,11 +110,11 @@ func Test_release_validate(t *testing.T) { Name: "release6", Description: "", Namespace: "", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -126,11 +126,11 @@ func Test_release_validate(t *testing.T) { Name: "release7", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -142,11 +142,11 @@ func Test_release_validate(t *testing.T) { Name: "release8", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -158,11 +158,11 @@ func Test_release_validate(t *testing.T) { Name: "release9", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -174,11 +174,11 @@ func Test_release_validate(t *testing.T) { Name: "release10", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", - Test: true, + Test: True, }, s: st, }, @@ -190,12 +190,12 @@ func Test_release_validate(t *testing.T) { Name: "release11", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", ValuesFiles: []string{"xyz.yaml"}, - Test: true, + Test: True, }, s: st, }, @@ -207,11 +207,11 @@ func Test_release_validate(t *testing.T) { Name: "release12", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFiles: []string{"xyz.yaml"}, - Test: true, + Test: True, }, s: st, }, @@ -223,11 +223,11 @@ func Test_release_validate(t *testing.T) { Name: "release13", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFiles: []string{"./../../tests/values.yaml", "../../tests/values2.yaml"}, - Test: true, + Test: True, }, s: st, }, @@ -239,7 +239,7 @@ func Test_release_validate(t *testing.T) { Name: "release14", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -255,7 +255,7 @@ func Test_release_validate(t *testing.T) { Name: "release15", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -271,7 +271,7 @@ func Test_release_validate(t *testing.T) { Name: "release16", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -287,7 +287,7 @@ func Test_release_validate(t *testing.T) { Name: "release17", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -303,7 +303,7 @@ func Test_release_validate(t *testing.T) { Name: "release18", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -319,7 +319,7 @@ func Test_release_validate(t *testing.T) { Name: "release19", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -335,7 +335,7 @@ func Test_release_validate(t *testing.T) { Name: "release20", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -351,12 +351,12 @@ func Test_release_validate(t *testing.T) { Name: "release21", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", PostRenderer: "../../tests/post-renderer.sh", - Test: true, + Test: True, }, s: st, }, @@ -368,12 +368,12 @@ func Test_release_validate(t *testing.T) { Name: "release22", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", PostRenderer: "doesnt-exist.sh", - Test: true, + Test: True, }, s: st, }, @@ -385,7 +385,7 @@ func Test_release_validate(t *testing.T) { Name: "release20", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", @@ -443,7 +443,7 @@ func Test_release_inheritHooks(t *testing.T) { Name: "release1 - Global hooks correctly inherited", Description: "", Namespace: "namespace", - Enabled: true, + Enabled: True, Chart: "repo/chartX", Version: "1.0", ValuesFile: "../../tests/values.yaml", diff --git a/internal/app/state_files.go b/internal/app/state_files.go index 5de157a1..495d8a26 100644 --- a/internal/app/state_files.go +++ b/internal/app/state_files.go @@ -142,7 +142,10 @@ func (s *State) build(files fileOptionArray) error { // Merge Apps that already existed in the state for appName, app := range fileState.Apps { if _, ok := s.Apps[appName]; ok { - if err := mergo.Merge(s.Apps[appName], app, mergo.WithAppendSlice, mergo.WithOverride); err != nil { + if err := mergo.Merge(s.Apps[appName], app, + mergo.WithAppendSlice, + mergo.WithOverride, + mergo.WithTransformers(MergoTransformer(NullBoolTransformer))); err != nil { return fmt.Errorf("failed to merge %s from desired state file %s: %w", appName, f.name, err) } } diff --git a/internal/app/state_files_test.go b/internal/app/state_files_test.go index b7ec6b22..80a9fd3b 100644 --- a/internal/app/state_files_test.go +++ b/internal/app/state_files_test.go @@ -378,3 +378,29 @@ func Test_build(t *testing.T) { t.Errorf("build() - unexpected number of repos, wanted 3 got %d", len(s.Apps)) } } + +func Test_DSFMergeWithOverwrite(t *testing.T) { + teardownTestCase, err := setupStateFileTestCase(t) + if err != nil { + t.Errorf("setupStateFileTestCase(), got: %v", err) + } + defer teardownTestCase(t) + s := new(State) + files := fileOptionArray{ + fileOption{name: "../../examples/minimal-example.yaml"}, + fileOption{name: "../../examples/minimal-example-overwrite.yaml"}, + } + err = s.build(files) + if err != nil { + t.Errorf("build() - unexpected error: %v", err) + } + if len(s.Apps) != 2 { + t.Errorf("build() - unexpected number of apps, wanted 5 got %d", len(s.Apps)) + } + if len(s.HelmRepos) != 2 { + t.Errorf("build() - unexpected number of repos, wanted 3 got %d", len(s.Apps)) + } + if s.Apps["jenkins"].Enabled.Value != false { + t.Errorf("build() - unexpected status of a release, wanted 'enabled'=false got %v", s.Apps["jenkins"].Enabled.Value) + } +} diff --git a/internal/app/state_test.go b/internal/app/state_test.go index 5340b6e4..fbbf93d5 100644 --- a/internal/app/state_test.go +++ b/internal/app/state_test.go @@ -401,22 +401,22 @@ func createFullReleasePointer(name, chart, version string) *Release { Name: name, Description: "", Namespace: "", - Enabled: true, + Enabled: True, Chart: chart, Version: version, ValuesFile: "", ValuesFiles: []string{}, SecretsFile: "", SecretsFiles: []string{}, - Test: false, - Protected: false, - Wait: false, + Test: False, + Protected: False, + Wait: False, Priority: 0, Set: make(map[string]string), SetString: make(map[string]string), HelmFlags: []string{}, HelmDiffFlags: []string{}, - NoHooks: false, + NoHooks: False, Timeout: 0, PostRenderer: "", } diff --git a/internal/app/utils_test.go b/internal/app/utils_test.go index 109e0821..d9b725f5 100644 --- a/internal/app/utils_test.go +++ b/internal/app/utils_test.go @@ -198,7 +198,7 @@ func Test_eyamlSecrets(t *testing.T) { Name: "release1", Namespace: "namespace", Version: "1.0.0", - Enabled: true, + Enabled: True, SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", }, }, @@ -216,7 +216,7 @@ func Test_eyamlSecrets(t *testing.T) { Name: "release1", Namespace: "namespace", Version: "1.0.0", - Enabled: true, + Enabled: True, SecretsFile: "./../../tests/secrets/invalid_eyaml_secrets.yaml", }, }, @@ -234,7 +234,7 @@ func Test_eyamlSecrets(t *testing.T) { Name: "release1", Namespace: "namespace", Version: "1.0.0", - Enabled: true, + Enabled: True, SecretsFile: "./../../tests/secrets/valid_eyaml_secrets.yaml", }, }, diff --git a/schema.json b/schema.json index 1b6f22c7..bb10499d 100644 --- a/schema.json +++ b/schema.json @@ -205,6 +205,9 @@ ], "description": "Namespace type represents the fields of a Namespace" }, + "NullBool": { + "type": "boolean" + }, "Quotas": { "properties": { "pods": { @@ -253,7 +256,7 @@ "description": "Namespace where to deploy the helm release" }, "enabled": { - "type": "boolean", + "$ref": "#/$defs/NullBool", "description": "Enabled can be used to togle a helm release" }, "group": { @@ -293,15 +296,15 @@ "description": "PostRenderer is the path to an executable to be used for post rendering" }, "test": { - "type": "boolean", + "$ref": "#/$defs/NullBool", "description": "Test indicates if the chart tests should be executed" }, "protected": { - "type": "boolean", + "$ref": "#/$defs/NullBool", "description": "Protected defines if the release should be protected against changes" }, "wait": { - "type": "boolean", + "$ref": "#/$defs/NullBool", "description": "Wait defines whether helm should block execution until all k8s resources are in a ready state" }, "priority": { @@ -350,7 +353,7 @@ "description": "HelmDiffFlags is a list of cli flags to pass to helm diff" }, "noHooks": { - "type": "boolean", + "$ref": "#/$defs/NullBool", "description": "NoHooks can be used to disable the execution of helm hooks" }, "timeout": {