diff --git a/pkg/app/app_list_test.go b/pkg/app/app_list_test.go index 08a5cae4..38f4afe5 100644 --- a/pkg/app/app_list_test.go +++ b/pkg/app/app_list_test.go @@ -41,7 +41,7 @@ func testListWithEnvironment(t *testing.T, cfg configImpl) { environments: development: {} shared: {} - +--- releases: - name: logging chart: incubator/raw @@ -90,7 +90,7 @@ releases: environments: test: {} shared: {} - +--- repositories: - name: bitnami url: https://charts.bitnami.com/bitnami diff --git a/pkg/app/desired_state_file_loader.go b/pkg/app/desired_state_file_loader.go index f02a9a1b..b059628e 100644 --- a/pkg/app/desired_state_file_loader.go +++ b/pkg/app/desired_state_file_loader.go @@ -13,6 +13,7 @@ import ( "github.com/helmfile/helmfile/pkg/environment" "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/helmexec" + "github.com/helmfile/helmfile/pkg/policy" "github.com/helmfile/helmfile/pkg/remote" "github.com/helmfile/helmfile/pkg/runtime" "github.com/helmfile/helmfile/pkg/state" @@ -187,6 +188,13 @@ func (a *desiredStateLoader) rawLoad(yaml []byte, baseDir, file string, evaluate func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) { // Allows part-splitting to work with CLRF-ed content normalizedContent := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n")) + isStrict, err := policy.Checker(filename, normalizedContent) + if err != nil { + if isStrict { + return nil, err + } + ld.logger.Warnf("WARNING: %v", err) + } parts := bytes.Split(normalizedContent, []byte("\n---\n")) hasEnv := env != nil || overrodeEnv != nil diff --git a/pkg/app/testdata/app_list_test/default_environment_includes_all_releases b/pkg/app/testdata/app_list_test/default_environment_includes_all_releases index 8157f84d..e2212383 100644 --- a/pkg/app/testdata/app_list_test/default_environment_includes_all_releases +++ b/pkg/app/testdata/app_list_test/default_environment_includes_all_releases @@ -8,51 +8,6 @@ first-pass rendering output of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: first-pass produced: &{default map[] map[]} first-pass rendering result of "helmfile_1.yaml.part.0": {default map[] map[]} @@ -64,51 +19,106 @@ second-pass rendering result of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} + +merged environment: &{default map[] map[]} +first-pass rendering starting for "helmfile_1.yaml.part.1": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 35: chart: incubator/raw -36: namespace: kube-system -37: installed: false +36: needs: +37: - kube-system/disabled 38: -39: - name: test2 +39: - name: test3 40: chart: incubator/raw 41: needs: -42: - kube-system/disabled +42: - test2 +43: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile_1.yaml.part.1": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system + 4: + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 +35: chart: incubator/raw +36: needs: +37: - kube-system/disabled +38: +39: - name: test3 +40: chart: incubator/raw +41: needs: +42: - test2 43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: merged environment: &{default map[] map[]} changing working directory back to "/path/to" @@ -121,24 +131,6 @@ first-pass rendering output of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: first-pass produced: &{default map[] map[]} first-pass rendering result of "helmfile_2.yaml.part.0": {default map[] map[]} @@ -150,24 +142,52 @@ second-pass rendering result of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test + +merged environment: &{default map[] map[]} +first-pass rendering starting for "helmfile_2.yaml.part.1": inherited=&{default map[] map[]}, overrode= +first-pass uses: &{default map[] map[]} +first-pass rendering output of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 +16: + +first-pass produced: &{default map[] map[]} +first-pass rendering result of "helmfile_2.yaml.part.1": {default map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: merged environment: &{default map[] map[]} changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_list_test/fail_on_unknown_environment b/pkg/app/testdata/app_list_test/fail_on_unknown_environment index 49d0fa69..040b2ebe 100644 --- a/pkg/app/testdata/app_list_test/fail_on_unknown_environment +++ b/pkg/app/testdata/app_list_test/fail_on_unknown_environment @@ -8,51 +8,6 @@ first-pass rendering output of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: first-pass produced: &{staging map[] map[]} first-pass rendering result of "helmfile_1.yaml.part.0": {staging map[] map[]} @@ -64,51 +19,6 @@ second-pass rendering result of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: merged environment: &{staging map[] map[]} changing working directory back to "/path/to" @@ -121,24 +31,6 @@ first-pass rendering output of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: first-pass produced: &{staging map[] map[]} first-pass rendering result of "helmfile_2.yaml.part.0": {staging map[] map[]} @@ -150,24 +42,6 @@ second-pass rendering result of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: merged environment: &{staging map[] map[]} changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_multiple_files b/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_multiple_files index f8ddb46f..98941e29 100644 --- a/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_multiple_files +++ b/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_multiple_files @@ -8,51 +8,6 @@ first-pass rendering output of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: first-pass produced: &{shared map[] map[]} first-pass rendering result of "helmfile_1.yaml.part.0": {shared map[] map[]} @@ -64,51 +19,106 @@ second-pass rendering result of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} + +merged environment: &{shared map[] map[]} +first-pass rendering starting for "helmfile_1.yaml.part.1": inherited=&{shared map[] map[]}, overrode= +first-pass uses: &{shared map[] map[]} +first-pass rendering output of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 35: chart: incubator/raw -36: namespace: kube-system -37: installed: false +36: needs: +37: - kube-system/disabled 38: -39: - name: test2 +39: - name: test3 40: chart: incubator/raw 41: needs: -42: - kube-system/disabled +42: - test2 +43: + +first-pass produced: &{shared map[] map[]} +first-pass rendering result of "helmfile_1.yaml.part.1": {shared map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system + 4: + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 +35: chart: incubator/raw +36: needs: +37: - kube-system/disabled +38: +39: - name: test3 +40: chart: incubator/raw +41: needs: +42: - test2 43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: merged environment: &{shared map[] map[]} changing working directory back to "/path/to" @@ -121,24 +131,6 @@ first-pass rendering output of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: first-pass produced: &{shared map[] map[]} first-pass rendering result of "helmfile_2.yaml.part.0": {shared map[] map[]} @@ -150,24 +142,52 @@ second-pass rendering result of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test + +merged environment: &{shared map[] map[]} +first-pass rendering starting for "helmfile_2.yaml.part.1": inherited=&{shared map[] map[]}, overrode= +first-pass uses: &{shared map[] map[]} +first-pass rendering output of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 +16: + +first-pass produced: &{shared map[] map[]} +first-pass rendering result of "helmfile_2.yaml.part.1": {shared map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: merged environment: &{shared map[] map[]} changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_one_file_only b/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_one_file_only index a147faf3..927ee649 100644 --- a/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_one_file_only +++ b/pkg/app/testdata/app_list_test/filters_releases_for_environment_used_in_one_file_only @@ -8,51 +8,6 @@ first-pass rendering output of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: first-pass produced: &{test map[] map[]} first-pass rendering result of "helmfile_1.yaml.part.0": {test map[] map[]} @@ -64,51 +19,6 @@ second-pass rendering result of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: merged environment: &{test map[] map[]} changing working directory back to "/path/to" @@ -121,24 +31,6 @@ first-pass rendering output of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: first-pass produced: &{test map[] map[]} first-pass rendering result of "helmfile_2.yaml.part.0": {test map[] map[]} @@ -150,24 +42,52 @@ second-pass rendering result of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test + +merged environment: &{test map[] map[]} +first-pass rendering starting for "helmfile_2.yaml.part.1": inherited=&{test map[] map[]}, overrode= +first-pass uses: &{test map[] map[]} +first-pass rendering output of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 +16: + +first-pass produced: &{test map[] map[]} +first-pass rendering result of "helmfile_2.yaml.part.1": {test map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_2.yaml.part.1": + 0: repositories: + 1: - name: bitnami + 2: url: https://charts.bitnami.com/bitnami + 3: + 4: releases: + 5: - name: cache + 6: namespace: my-app + 7: chart: bitnami/redis + 8: version: 17.0.7 + 9: labels: +10: app: test +11: +12: - name: database +13: namespace: my-app +14: chart: bitnami/postgres +15: version: 11.6.22 16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: merged environment: &{test map[] map[]} changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_list_test/list_releases_matching_selector_and_environment b/pkg/app/testdata/app_list_test/list_releases_matching_selector_and_environment index 26c54e12..59e5e422 100644 --- a/pkg/app/testdata/app_list_test/list_releases_matching_selector_and_environment +++ b/pkg/app/testdata/app_list_test/list_releases_matching_selector_and_environment @@ -8,51 +8,6 @@ first-pass rendering output of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} - 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled -35: chart: incubator/raw -36: namespace: kube-system -37: installed: false -38: -39: - name: test2 -40: chart: incubator/raw -41: needs: -42: - kube-system/disabled -43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: first-pass produced: &{development map[] map[]} first-pass rendering result of "helmfile_1.yaml.part.0": {development map[] map[]} @@ -64,51 +19,106 @@ second-pass rendering result of "helmfile_1.yaml.part.0": 1: environments: 2: development: {} 3: shared: {} + +merged environment: &{development map[] map[]} +first-pass rendering starting for "helmfile_1.yaml.part.1": inherited=&{development map[] map[]}, overrode= +first-pass uses: &{development map[] map[]} +first-pass rendering output of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system 4: - 5: releases: - 6: - name: logging - 7: chart: incubator/raw - 8: namespace: kube-system - 9: -10: - name: kubernetes-external-secrets -11: chart: incubator/raw -12: namespace: kube-system -13: needs: -14: - kube-system/logging -15: -16: - name: external-secrets -17: chart: incubator/raw -18: namespace: default -19: labels: -20: app: test -21: needs: -22: - kube-system/kubernetes-external-secrets -23: -24: - name: my-release -25: chart: incubator/raw -26: namespace: default -27: labels: -28: app: test -29: needs: -30: - default/external-secrets -31: -32: -33: # Disabled releases are treated as missing -34: - name: disabled + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 35: chart: incubator/raw -36: namespace: kube-system -37: installed: false +36: needs: +37: - kube-system/disabled 38: -39: - name: test2 +39: - name: test3 40: chart: incubator/raw 41: needs: -42: - kube-system/disabled +42: - test2 +43: + +first-pass produced: &{development map[] map[]} +first-pass rendering result of "helmfile_1.yaml.part.1": {development map[] map[]} +vals: +map[] +defaultVals:[] +second-pass rendering result of "helmfile_1.yaml.part.1": + 0: releases: + 1: - name: logging + 2: chart: incubator/raw + 3: namespace: kube-system + 4: + 5: - name: kubernetes-external-secrets + 6: chart: incubator/raw + 7: namespace: kube-system + 8: needs: + 9: - kube-system/logging +10: +11: - name: external-secrets +12: chart: incubator/raw +13: namespace: default +14: labels: +15: app: test +16: needs: +17: - kube-system/kubernetes-external-secrets +18: +19: - name: my-release +20: chart: incubator/raw +21: namespace: default +22: labels: +23: app: test +24: needs: +25: - default/external-secrets +26: +27: +28: # Disabled releases are treated as missing +29: - name: disabled +30: chart: incubator/raw +31: namespace: kube-system +32: installed: false +33: +34: - name: test2 +35: chart: incubator/raw +36: needs: +37: - kube-system/disabled +38: +39: - name: test3 +40: chart: incubator/raw +41: needs: +42: - test2 43: -44: - name: test3 -45: chart: incubator/raw -46: needs: -47: - test2 -48: merged environment: &{development map[] map[]} changing working directory back to "/path/to" @@ -121,24 +131,6 @@ first-pass rendering output of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: first-pass produced: &{development map[] map[]} first-pass rendering result of "helmfile_2.yaml.part.0": {development map[] map[]} @@ -150,24 +142,6 @@ second-pass rendering result of "helmfile_2.yaml.part.0": 1: environments: 2: test: {} 3: shared: {} - 4: - 5: repositories: - 6: - name: bitnami - 7: url: https://charts.bitnami.com/bitnami - 8: - 9: releases: -10: - name: cache -11: namespace: my-app -12: chart: bitnami/redis -13: version: 17.0.7 -14: labels: -15: app: test -16: -17: - name: database -18: namespace: my-app -19: chart: bitnami/postgres -20: version: 11.6.22 -21: merged environment: &{development map[] map[]} changing working directory back to "/path/to" diff --git a/pkg/policy/checker.go b/pkg/policy/checker.go index 20972aa6..3399540e 100644 --- a/pkg/policy/checker.go +++ b/pkg/policy/checker.go @@ -2,39 +2,120 @@ package policy import ( + "bytes" "errors" - "path/filepath" + "fmt" + "regexp" + "strings" "github.com/helmfile/helmfile/pkg/runtime" ) var ( EnvironmentsAndReleasesWithinSameYamlPartErr = errors.New("environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part") + topConfigKeysRegex = regexp.MustCompile(`^[a-zA-Z]+: *$`) + separatorRegex = regexp.MustCompile(`^--- *$`) + topkeysPriority = map[string]int{ + "bases": 0, + "environments": 1, + "releases": 2, + } ) // checkerFunc is a function that checks the helmState. -type checkerFunc func(string, map[string]any) (bool, error) +type checkerFunc func(filePath string, content []byte) (bool, error) -func forbidEnvironmentsWithReleases(filePath string, releaseState map[string]any) (bool, error) { +func forbidEnvironmentsWithReleases(filePath string, content []byte) (bool, error) { // forbid environments and releases to be defined at the same yaml part - _, hasEnvironments := releaseState["environments"] - _, hasReleases := releaseState["releases"] - if hasEnvironments && hasReleases && (filepath.Ext(filePath) == ".gotmpl" || !runtime.V1Mode) { - return runtime.V1Mode, EnvironmentsAndReleasesWithinSameYamlPartErr + topKeys := TopKeys(content, true) + if len(topKeys) == 0 { + return true, fmt.Errorf("no top-level config keys are found in %s", filePath) + } + result := []string{} + resultKeys := map[string]interface{}{} + for _, k := range topKeys { + if k == "environments" || k == "releases" || k == "---" { + if _, ok := resultKeys[k]; !ok { + result = append(result, k) + resultKeys[k] = nil + } + } + } + + if len(result) < 2 { + return false, nil + } + for i := 0; i < len(result)-1; i++ { + if result[i] != "---" && result[i+1] != "---" { + return runtime.V1Mode, EnvironmentsAndReleasesWithinSameYamlPartErr + } } return false, nil } var checkerFuncs = []checkerFunc{ + TopConfigKeysVerifier, forbidEnvironmentsWithReleases, } // Checker is a policy checker for the helmfile state. -func Checker(filePath string, helmState map[string]any) (bool, error) { +func Checker(filePath string, content []byte) (bool, error) { for _, fn := range checkerFuncs { - if isStrict, err := fn(filePath, helmState); err != nil { + if isStrict, err := fn(filePath, content); err != nil { return isStrict, err } } return false, nil } + +// isTopOrderKey checks if the key is a top-level config key that must be defined in the correct order. +func isTopOrderKey(key string) bool { + _, ok := topkeysPriority[key] + return ok +} + +// TopKeys returns the top-level config keys. +func TopKeys(helmfileContent []byte, hasSeparator bool) []string { + var topKeys []string + clines := bytes.Split(helmfileContent, []byte("\n")) + + for _, line := range clines { + if topConfigKeysRegex.Match(line) { + lineStr := strings.Split(string(line), ":")[0] + topKeys = append(topKeys, lineStr) + } + if hasSeparator && separatorRegex.Match(line) { + topKeys = append(topKeys, strings.TrimSpace(string(line))) + } + } + return topKeys +} + +// TopConfigKeysVerifier verifies the top-level config keys are defined in the correct order. +func TopConfigKeysVerifier(filePath string, helmfileContent []byte) (bool, error) { + var orderKeys, topKeys []string + topKeys = TopKeys(helmfileContent, false) + + for _, k := range topKeys { + if isTopOrderKey(k) { + orderKeys = append(orderKeys, k) + } + } + + if len(topKeys) == 0 { + return true, fmt.Errorf("no top-level config keys are found in %s", filePath) + } + + if len(orderKeys) == 0 { + return false, nil + } + + for i := 1; i < len(orderKeys); i++ { + preKey := orderKeys[i-1] + currentKey := orderKeys[i] + if topkeysPriority[preKey] > topkeysPriority[currentKey] { + return runtime.V1Mode, fmt.Errorf("top-level config key %s must be defined before %s in %s", preKey, currentKey, filePath) + } + } + return false, nil +} diff --git a/pkg/policy/checker_test.go b/pkg/policy/checker_test.go index c3cfac1c..5da906df 100644 --- a/pkg/policy/checker_test.go +++ b/pkg/policy/checker_test.go @@ -13,51 +13,49 @@ func TestForbidEnvironmentsWithReleases(t *testing.T) { name string filePath string v1mode bool - helmState map[string]any + content []byte expectedErr bool isStrict bool }{ { - name: "no error when only releases", - filePath: "helmfile.yaml", - v1mode: false, - helmState: map[string]any{ - "releases": any(nil), - }, + name: "no error when only releases", + filePath: "helmfile.yaml.gotmpl", + content: []byte("releases:\n"), + v1mode: false, expectedErr: false, isStrict: false, }, { - name: "no error when only environments", - filePath: "helmfile.yaml", - v1mode: false, - helmState: map[string]any{ - "environments": map[string]any{}, - }, + name: "no error when only environments", + filePath: "helmfile.yaml.gotmpl", + content: []byte("environments:\n"), + v1mode: false, expectedErr: false, isStrict: false, }, { - name: "error when both releases and environments", - filePath: "helmfile.yaml", - v1mode: false, - helmState: map[string]any{ - "environments": any(nil), - "releases": any(nil), - }, + name: "no error when has --- between releases and environments", + filePath: "helmfile.yaml.gotmpl", + content: []byte("environments:\n---\nreleases:\n"), + v1mode: false, + expectedErr: false, + isStrict: false, + }, + { + name: "error when both releases and environments", + filePath: "helmfile.yaml.gotmpl", + content: []byte("environments:\nreleases:\n"), + v1mode: false, expectedErr: true, isStrict: false, }, { - name: "no error when both releases and environments for plain yaml on v1", - filePath: "helmfile.yaml", - v1mode: true, - helmState: map[string]any{ - "environments": any(nil), - "releases": any(nil), - }, - expectedErr: false, - isStrict: false, + name: "no error when both releases and environments for plain yaml on v1", + filePath: "helmfile.yaml.gotmpl", + content: []byte("environments:\nreleases:\n"), + v1mode: true, + expectedErr: true, + isStrict: true, }, } @@ -69,7 +67,7 @@ func TestForbidEnvironmentsWithReleases(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { runtime.V1Mode = tc.v1mode - isStrict, err := forbidEnvironmentsWithReleases(tc.filePath, tc.helmState) + isStrict, err := forbidEnvironmentsWithReleases(tc.filePath, tc.content) require.Equal(t, tc.isStrict, isStrict, "expected isStrict=%v, got=%v", tc.isStrict, isStrict) if tc.expectedErr { require.ErrorIsf(t, err, EnvironmentsAndReleasesWithinSameYamlPartErr, "expected error=%v, got=%v", EnvironmentsAndReleasesWithinSameYamlPartErr, err) @@ -79,3 +77,146 @@ func TestForbidEnvironmentsWithReleases(t *testing.T) { }) } } + +func TestIsTopOrderKey(t *testing.T) { + tests := []struct { + name string + item string + want bool + }{ + { + name: "is top order key[bases]", + item: "bases", + want: true, + }, + { + name: "is top order key[environments]", + item: "environments", + want: true, + }, + { + name: "is top order key[releases]", + item: "releases", + want: true, + }, + { + name: "not top order key[helmDefaults]", + item: "helmDefaults", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isTopOrderKey(tt.item); got != tt.want { + t.Errorf("isTopOrderKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTopConfigKeysVerifier(t *testing.T) { + tests := []struct { + name string + helmfileContent []byte + wantErr bool + wantStrict bool + }{ + { + name: "no error when correct order[full items]", + helmfileContent: []byte("bases:\nenvironments:\nreleases:\n"), + wantErr: false, + }, + { + name: "no error when correct order 00", + helmfileContent: []byte("bases:\nva:\nve:\nreleases:\n"), + wantErr: false, + }, + { + name: "no error when correct order 01", + helmfileContent: []byte("a:\ne:\n"), + wantErr: false, + }, + { + name: "error when not correct order 00", + helmfileContent: []byte("environments:\nbases:\n"), + wantErr: true, + }, + { + name: "error when not correct order 01", + helmfileContent: []byte("environments:\nhelmDefaults:\nbases:\n"), + wantErr: true, + }, + { + name: "error when not correct order 02", + helmfileContent: []byte("helmDefaults:\nenvironments:\nbases:\n"), + wantErr: true, + }, + { + name: "error when not correct order 03", + helmfileContent: []byte("environments:\nva:\nve:\nbases:\n"), + wantErr: true, + }, + { + name: "error when not correct order 04", + helmfileContent: []byte("bases:\nreleases:\nenvironments:\n"), + wantErr: true, + }, + { + name: "no error when only has bases", + helmfileContent: []byte("bases:\n"), + }, + { + name: "no error when only has environments", + helmfileContent: []byte("environments:\n"), + }, + { + name: "no error when only has releases", + helmfileContent: []byte("releases:\n"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isStrict, err := TopConfigKeysVerifier("helmfile.yaml", tt.helmfileContent) + require.Equal(t, tt.wantStrict, isStrict, "expected isStrict=%v, got=%v", tt.wantStrict, isStrict) + if tt.wantErr { + require.Error(t, err, "expected error, got=%v", err) + } else { + require.NoError(t, err, "expected no error but got error: %v", err) + } + }) + } +} + +func TestTopKeys(t *testing.T) { + tests := []struct { + name string + helmfileContent []byte + hasSeparator bool + want []string + }{ + { + name: "get top keys", + helmfileContent: []byte("bases:\nenvironments:\nreleases:\n"), + want: []string{"bases", "environments", "releases"}, + }, + { + name: "get top keys with ---", + helmfileContent: []byte("bases:\n---\nreleases:\n"), + hasSeparator: true, + want: []string{"bases", "---", "releases"}, + }, + { + name: "get empty keys", + helmfileContent: []byte(""), + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := TopKeys(tt.helmfileContent, tt.hasSeparator) + require.Equal(t, tt.want, got, "expected %v, got=%v", tt.want, got) + }) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index fc70884d..7a6368d6 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -29,7 +29,6 @@ import ( "github.com/helmfile/helmfile/pkg/event" "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/helmexec" - "github.com/helmfile/helmfile/pkg/policy" "github.com/helmfile/helmfile/pkg/remote" "github.com/helmfile/helmfile/pkg/tmpl" "github.com/helmfile/helmfile/pkg/yaml" @@ -106,14 +105,6 @@ func (hs *HelmState) UnmarshalYAML(unmarshal func(any) error) error { return err } - isStrict, err := policy.Checker(hs.FilePath, helmStateInfo) - if err != nil { - if isStrict { - return err - } - fmt.Fprintf(os.Stderr, "Warning: %v\n", err) - } - return unmarshal((*helmStateAlias)(hs)) } diff --git a/test/e2e/template/helmfile/testdata/snapshot/environments_releases_within_same_yaml_part/output.yaml b/test/e2e/template/helmfile/testdata/snapshot/environments_releases_within_same_yaml_part/output.yaml index be8b8838..2e9471ac 100644 --- a/test/e2e/template/helmfile/testdata/snapshot/environments_releases_within_same_yaml_part/output.yaml +++ b/test/e2e/template/helmfile/testdata/snapshot/environments_releases_within_same_yaml_part/output.yaml @@ -1,5 +1,4 @@ -Warning: environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part -Warning: environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part +WARNING: environments and releases cannot be defined within the same YAML part. Use --- to extract the environments into a dedicated part Building dependency release=raw, chart=../../charts/raw-0.0.1 Templating release=raw, chart=../../charts/raw-0.0.1 ---