From 9dcc49cbd5e37cc916a23a4f375f7f4214dfd515 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Mon, 1 Sep 2025 17:46:14 -0400 Subject: [PATCH] Move lint pkg to be part of each chart version Linting is specific to the chart versions. A v2 and v3 chart will lint differently. To accomplish this, packages like engine need to be able to handle different chart versions. This was accomplished by some changes: 1. The introduction of a Charter interface for charts 2. The ChartAccessor which is able to accept a chart and then provide access to its data via an interface. There is an interface, factory, and implementation for each version of chart. 3. Common packages were moved to a common and util packages. Due to some package loops, there are 2 packages which may get some consolidation in the future. The new interfaces provide the foundation to move the actions and cmd packages to be able to handle multiple apiVersions of charts. Signed-off-by: Matt Farina --- .golangci.yml | 6 +- Makefile | 13 +- internal/chart/v3/chart.go | 14 +- internal/chart/v3/chart_test.go | 14 +- internal/chart/v3/lint/lint.go | 66 ++ internal/chart/v3/lint/lint_test.go | 246 ++++++ internal/chart/v3/lint/rules/chartfile.go | 225 ++++++ .../chart/v3/lint/rules/chartfile_test.go | 276 +++++++ internal/chart/v3/lint/rules/crds.go | 113 +++ internal/chart/v3/lint/rules/crds_test.go | 36 + internal/chart/v3/lint/rules/dependencies.go | 101 +++ .../chart/v3/lint/rules/dependencies_test.go | 157 ++++ .../chart/v3}/lint/rules/deprecations.go | 8 +- .../chart/v3/lint/rules/deprecations_test.go | 41 + internal/chart/v3/lint/rules/template.go | 348 +++++++++ internal/chart/v3/lint/rules/template_test.go | 441 +++++++++++ .../lint/rules/testdata/albatross/Chart.yaml | 5 + .../testdata/albatross/templates/_helpers.tpl | 0 .../testdata/albatross/templates/fail.yaml | 0 .../testdata/albatross/templates/svc.yaml | 0 .../lint/rules/testdata/albatross/values.yaml | 0 .../testdata/anotherbadchartfile/Chart.yaml | 15 + .../rules/testdata/badchartfile/Chart.yaml | 0 .../rules/testdata/badchartfile/values.yaml | 0 .../rules/testdata/badchartname/Chart.yaml | 5 + .../rules/testdata/badchartname/values.yaml | 0 .../lint/rules/testdata/badcrdfile/Chart.yaml | 6 + .../badcrdfile/crds/bad-apiversion.yaml | 0 .../testdata/badcrdfile/crds/bad-crd.yaml | 0 .../testdata/badcrdfile/templates/.gitkeep | 0 .../rules/testdata/badcrdfile/values.yaml | 0 .../rules/testdata/badvaluesfile/Chart.yaml | 6 + .../templates/badvaluesfile.yaml | 0 .../rules/testdata/badvaluesfile/values.yaml | 0 .../v3/lint/rules/testdata/goodone/Chart.yaml | 5 + .../rules/testdata/goodone/crds/test-crd.yaml | 0 .../testdata/goodone/templates/goodone.yaml | 0 .../lint/rules/testdata/goodone/values.yaml | 0 .../testdata/invalidchartfile/Chart.yaml | 0 .../testdata/invalidchartfile/values.yaml | 0 .../rules/testdata/invalidcrdsdir/Chart.yaml | 6 + .../lint/rules/testdata/invalidcrdsdir/crds | 0 .../rules/testdata/invalidcrdsdir/values.yaml | 0 .../testdata/malformed-template/.helmignore | 0 .../testdata/malformed-template/Chart.yaml | 25 + .../malformed-template/templates/bad.yaml | 0 .../testdata/malformed-template/values.yaml | 0 .../testdata/multi-template-fail/Chart.yaml | 21 + .../templates/multi-fail.yaml | 0 .../v3/lint/rules/testdata/v3-fail/Chart.yaml | 21 + .../testdata/v3-fail/templates/_helpers.tpl | 0 .../v3-fail/templates/deployment.yaml | 0 .../testdata/v3-fail/templates/ingress.yaml | 0 .../testdata/v3-fail/templates/service.yaml | 0 .../lint/rules/testdata/v3-fail/values.yaml | 0 .../rules/testdata/withsubchart/Chart.yaml | 16 + .../withsubchart/charts/subchart/Chart.yaml | 6 + .../charts/subchart/templates/subchart.yaml | 0 .../withsubchart/charts/subchart/values.yaml | 0 .../withsubchart/templates/mainchart.yaml | 0 .../rules/testdata/withsubchart/values.yaml | 0 internal/chart/v3/lint/rules/values.go | 79 ++ .../chart/v3}/lint/rules/values_test.go | 0 .../errors_test.go => lint/support/doc.go} | 26 +- .../chart/v3}/lint/support/message.go | 0 .../chart/v3}/lint/support/message_test.go | 0 internal/chart/v3/loader/load.go | 9 +- internal/chart/v3/loader/load_test.go | 5 +- internal/chart/v3/util/capabilities.go | 122 --- internal/chart/v3/util/capabilities_test.go | 84 -- internal/chart/v3/util/coalesce.go | 308 -------- internal/chart/v3/util/coalesce_test.go | 723 ------------------ internal/chart/v3/util/create.go | 5 +- internal/chart/v3/util/dependencies.go | 38 +- internal/chart/v3/util/dependencies_test.go | 11 +- internal/chart/v3/util/errors.go | 43 -- internal/chart/v3/util/jsonschema.go | 113 --- internal/chart/v3/util/jsonschema_test.go | 247 ------ internal/chart/v3/util/save.go | 5 +- internal/chart/v3/util/save_test.go | 11 +- internal/chart/v3/util/values.go | 220 ------ internal/chart/v3/util/values_test.go | 293 ------- pkg/action/action.go | 21 +- pkg/action/action_test.go | 20 +- pkg/action/get_values.go | 6 +- pkg/action/hooks_test.go | 9 +- pkg/action/install.go | 12 +- pkg/action/install_test.go | 7 +- pkg/action/lint.go | 9 +- pkg/action/show.go | 3 +- pkg/action/show_test.go | 9 +- pkg/action/upgrade.go | 12 +- pkg/chart/common.go | 219 ++++++ pkg/chart/{v2/util => common}/capabilities.go | 2 +- .../{v2/util => common}/capabilities_test.go | 2 +- pkg/chart/{v2/util => common}/errors.go | 2 +- pkg/chart/{v2/util => common}/errors_test.go | 2 +- .../chart/v3 => pkg/chart/common}/file.go | 2 +- .../util => common}/testdata/coleridge.yaml | 0 pkg/chart/{v2 => common}/util/coalesce.go | 80 +- .../{v2 => common}/util/coalesce_test.go | 18 +- pkg/chart/{v2 => common}/util/jsonschema.go | 21 +- .../{v2 => common}/util/jsonschema_test.go | 7 +- .../testdata/test-values-invalid.schema.json | 0 .../util/testdata/test-values-negative.yaml | 0 .../util/testdata/test-values.schema.json | 0 .../util/testdata/test-values.yaml | 0 pkg/chart/common/util/values.go | 70 ++ pkg/chart/common/util/values_test.go | 111 +++ pkg/chart/{v2/util => common}/values.go | 47 +- pkg/chart/{v2/util => common}/values_test.go | 90 +-- pkg/chart/{v2/file.go => interfaces.go} | 28 +- pkg/chart/v2/chart.go | 14 +- pkg/chart/v2/chart_test.go | 14 +- pkg/{ => chart/v2}/lint/lint.go | 12 +- pkg/{ => chart/v2}/lint/lint_test.go | 2 +- pkg/{ => chart/v2}/lint/rules/chartfile.go | 4 +- .../v2}/lint/rules/chartfile_test.go | 2 +- pkg/{ => chart/v2}/lint/rules/crds.go | 2 +- pkg/{ => chart/v2}/lint/rules/crds_test.go | 2 +- pkg/{ => chart/v2}/lint/rules/dependencies.go | 4 +- .../v2}/lint/rules/dependencies_test.go | 2 +- pkg/chart/v2/lint/rules/deprecations.go | 106 +++ .../v2}/lint/rules/deprecations_test.go | 2 +- pkg/{ => chart/v2}/lint/rules/template.go | 16 +- .../v2}/lint/rules/template_test.go | 9 +- .../lint/rules/testdata/albatross/Chart.yaml | 0 .../testdata/albatross/templates/_helpers.tpl | 16 + .../testdata/albatross/templates/fail.yaml | 1 + .../testdata/albatross/templates/svc.yaml | 19 + .../lint/rules/testdata/albatross/values.yaml | 1 + .../testdata/anotherbadchartfile/Chart.yaml | 0 .../rules/testdata/badchartfile/Chart.yaml | 11 + .../rules/testdata/badchartfile/values.yaml | 1 + .../rules/testdata/badchartname/Chart.yaml | 0 .../rules/testdata/badchartname/values.yaml | 1 + .../lint/rules/testdata/badcrdfile/Chart.yaml | 0 .../badcrdfile/crds/bad-apiversion.yaml | 2 + .../testdata/badcrdfile/crds/bad-crd.yaml | 2 + .../testdata/badcrdfile/templates/.gitkeep | 0 .../rules/testdata/badcrdfile/values.yaml | 1 + .../rules/testdata/badvaluesfile/Chart.yaml | 0 .../templates/badvaluesfile.yaml | 2 + .../rules/testdata/badvaluesfile/values.yaml | 2 + .../lint/rules/testdata/goodone/Chart.yaml | 0 .../rules/testdata/goodone/crds/test-crd.yaml | 19 + .../testdata/goodone/templates/goodone.yaml | 2 + .../lint/rules/testdata/goodone/values.yaml | 1 + .../testdata/invalidchartfile/Chart.yaml | 6 + .../testdata/invalidchartfile/values.yaml | 0 .../rules/testdata/invalidcrdsdir/Chart.yaml | 0 .../lint/rules/testdata/invalidcrdsdir/crds | 0 .../rules/testdata/invalidcrdsdir/values.yaml | 1 + .../testdata/malformed-template/.helmignore | 23 + .../testdata/malformed-template/Chart.yaml | 0 .../malformed-template/templates/bad.yaml | 1 + .../testdata/malformed-template/values.yaml | 82 ++ .../testdata/multi-template-fail/Chart.yaml | 0 .../templates/multi-fail.yaml | 13 + .../lint/rules/testdata/v3-fail/Chart.yaml | 0 .../testdata/v3-fail/templates/_helpers.tpl | 63 ++ .../v3-fail/templates/deployment.yaml | 56 ++ .../testdata/v3-fail/templates/ingress.yaml | 62 ++ .../testdata/v3-fail/templates/service.yaml | 17 + .../lint/rules/testdata/v3-fail/values.yaml | 66 ++ .../rules/testdata/withsubchart/Chart.yaml | 0 .../withsubchart/charts/subchart/Chart.yaml | 0 .../charts/subchart/templates/subchart.yaml | 2 + .../withsubchart/charts/subchart/values.yaml | 2 + .../withsubchart/templates/mainchart.yaml | 2 + .../rules/testdata/withsubchart/values.yaml | 0 pkg/{ => chart/v2}/lint/rules/values.go | 13 +- pkg/chart/v2/lint/rules/values_test.go | 169 ++++ pkg/{ => chart/v2}/lint/support/doc.go | 2 +- pkg/chart/v2/lint/support/message.go | 76 ++ pkg/chart/v2/lint/support/message_test.go | 79 ++ pkg/chart/v2/loader/load.go | 13 +- pkg/chart/v2/loader/load_test.go | 5 +- pkg/chart/v2/util/create.go | 5 +- pkg/chart/v2/util/dependencies.go | 38 +- pkg/chart/v2/util/dependencies_test.go | 11 +- pkg/chart/v2/util/save.go | 5 +- pkg/chart/v2/util/save_test.go | 11 +- pkg/cli/values/options_test.go | 2 +- pkg/cmd/helpers_test.go | 4 +- pkg/cmd/lint.go | 6 +- pkg/cmd/status.go | 4 +- pkg/cmd/template.go | 6 +- pkg/cmd/upgrade_test.go | 5 +- pkg/engine/engine.go | 61 +- pkg/engine/engine_test.go | 203 ++--- pkg/engine/files.go | 4 +- pkg/engine/lookup_func.go | 2 +- pkg/release/v1/mock.go | 3 +- pkg/release/v1/util/manifest_sorter.go | 4 +- 195 files changed, 4090 insertions(+), 2702 deletions(-) create mode 100644 internal/chart/v3/lint/lint.go create mode 100644 internal/chart/v3/lint/lint_test.go create mode 100644 internal/chart/v3/lint/rules/chartfile.go create mode 100644 internal/chart/v3/lint/rules/chartfile_test.go create mode 100644 internal/chart/v3/lint/rules/crds.go create mode 100644 internal/chart/v3/lint/rules/crds_test.go create mode 100644 internal/chart/v3/lint/rules/dependencies.go create mode 100644 internal/chart/v3/lint/rules/dependencies_test.go rename {pkg => internal/chart/v3}/lint/rules/deprecations.go (95%) create mode 100644 internal/chart/v3/lint/rules/deprecations_test.go create mode 100644 internal/chart/v3/lint/rules/template.go create mode 100644 internal/chart/v3/lint/rules/template_test.go create mode 100644 internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/albatross/templates/_helpers.tpl (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/albatross/templates/fail.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/albatross/templates/svc.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/albatross/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/badchartfile/Chart.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/badchartfile/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/badchartname/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/badcrdfile/templates/.gitkeep (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/badcrdfile/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/badvaluesfile/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/goodone/crds/test-crd.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/goodone/templates/goodone.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/goodone/values.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/invalidchartfile/Chart.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/invalidchartfile/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/invalidcrdsdir/crds (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/invalidcrdsdir/values.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/malformed-template/.helmignore (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/malformed-template/templates/bad.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/malformed-template/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/v3-fail/templates/_helpers.tpl (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/v3-fail/templates/deployment.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/v3-fail/templates/ingress.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/v3-fail/templates/service.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/v3-fail/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml create mode 100644 internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml rename {pkg => internal/chart/v3}/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/withsubchart/charts/subchart/values.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/withsubchart/templates/mainchart.yaml (100%) rename {pkg => internal/chart/v3}/lint/rules/testdata/withsubchart/values.yaml (100%) create mode 100644 internal/chart/v3/lint/rules/values.go rename {pkg => internal/chart/v3}/lint/rules/values_test.go (100%) rename internal/chart/v3/{util/errors_test.go => lint/support/doc.go} (67%) rename {pkg => internal/chart/v3}/lint/support/message.go (100%) rename {pkg => internal/chart/v3}/lint/support/message_test.go (100%) delete mode 100644 internal/chart/v3/util/capabilities.go delete mode 100644 internal/chart/v3/util/capabilities_test.go delete mode 100644 internal/chart/v3/util/coalesce.go delete mode 100644 internal/chart/v3/util/coalesce_test.go delete mode 100644 internal/chart/v3/util/errors.go delete mode 100644 internal/chart/v3/util/jsonschema.go delete mode 100644 internal/chart/v3/util/jsonschema_test.go delete mode 100644 internal/chart/v3/util/values.go delete mode 100644 internal/chart/v3/util/values_test.go create mode 100644 pkg/chart/common.go rename pkg/chart/{v2/util => common}/capabilities.go (99%) rename pkg/chart/{v2/util => common}/capabilities_test.go (99%) rename pkg/chart/{v2/util => common}/errors.go (98%) rename pkg/chart/{v2/util => common}/errors_test.go (98%) rename {internal/chart/v3 => pkg/chart/common}/file.go (98%) rename pkg/chart/{v2/util => common}/testdata/coleridge.yaml (100%) rename pkg/chart/{v2 => common}/util/coalesce.go (81%) rename pkg/chart/{v2 => common}/util/coalesce_test.go (97%) rename pkg/chart/{v2 => common}/util/jsonschema.go (89%) rename pkg/chart/{v2 => common}/util/jsonschema_test.go (96%) rename pkg/chart/{v2 => common}/util/testdata/test-values-invalid.schema.json (100%) rename pkg/chart/{v2 => common}/util/testdata/test-values-negative.yaml (100%) rename pkg/chart/{v2 => common}/util/testdata/test-values.schema.json (100%) rename pkg/chart/{v2 => common}/util/testdata/test-values.yaml (100%) create mode 100644 pkg/chart/common/util/values.go create mode 100644 pkg/chart/common/util/values_test.go rename pkg/chart/{v2/util => common}/values.go (74%) rename pkg/chart/{v2/util => common}/values_test.go (66%) rename pkg/chart/{v2/file.go => interfaces.go} (60%) rename pkg/{ => chart/v2}/lint/lint.go (83%) rename pkg/{ => chart/v2}/lint/lint_test.go (99%) rename pkg/{ => chart/v2}/lint/rules/chartfile.go (98%) rename pkg/{ => chart/v2}/lint/rules/chartfile_test.go (99%) rename pkg/{ => chart/v2}/lint/rules/crds.go (98%) rename pkg/{ => chart/v2}/lint/rules/crds_test.go (95%) rename pkg/{ => chart/v2}/lint/rules/dependencies.go (96%) rename pkg/{ => chart/v2}/lint/rules/dependencies_test.go (98%) create mode 100644 pkg/chart/v2/lint/rules/deprecations.go rename pkg/{ => chart/v2}/lint/rules/deprecations_test.go (94%) rename pkg/{ => chart/v2}/lint/rules/template.go (95%) rename pkg/{ => chart/v2}/lint/rules/template_test.go (98%) rename pkg/{ => chart/v2}/lint/rules/testdata/albatross/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl create mode 100644 pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/albatross/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/anotherbadchartfile/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/badchartname/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/badcrdfile/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep create mode 100644 pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/badvaluesfile/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/goodone/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/goodone/values.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/invalidcrdsdir/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds create mode 100644 pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore rename pkg/{ => chart/v2}/lint/rules/testdata/malformed-template/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/multi-template-fail/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/v3-fail/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl create mode 100644 pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml rename pkg/{ => chart/v2}/lint/rules/testdata/withsubchart/Chart.yaml (100%) rename pkg/{ => chart/v2}/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml (100%) create mode 100644 pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml create mode 100644 pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml rename pkg/{ => chart/v2}/lint/rules/values.go (84%) create mode 100644 pkg/chart/v2/lint/rules/values_test.go rename pkg/{ => chart/v2}/lint/support/doc.go (91%) create mode 100644 pkg/chart/v2/lint/support/message.go create mode 100644 pkg/chart/v2/lint/support/message_test.go diff --git a/.golangci.yml b/.golangci.yml index a9b13c35f..3df31b997 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,6 +33,7 @@ linters: - usetesting exclusions: + generated: lax presets: @@ -41,7 +42,10 @@ linters: - legacy - std-error-handling - rules: [] + rules: + - linters: + - revive + text: 'var-naming: avoid meaningless package names' warn-unused: true diff --git a/Makefile b/Makefile index 5e424bf05..5e1bfc6c2 100644 --- a/Makefile +++ b/Makefile @@ -63,10 +63,12 @@ K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s. K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1))) K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER)) -LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) -LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) .PHONY: all all: build @@ -112,7 +114,8 @@ test-unit: # based on older versions, this is run separately. When run without the ldflags in the unit test (above) or coverage # test, it still passes with a false-positive result as the resources shouldn’t be deprecated in the older Kubernetes # version if it only starts failing with the latest. - go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' + go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/chart/v2/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' + go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./internal/chart/v3/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' .PHONY: test-coverage diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go index 4d59fa5ec..2edc6c339 100644 --- a/internal/chart/v3/chart.go +++ b/internal/chart/v3/chart.go @@ -19,6 +19,8 @@ import ( "path/filepath" "regexp" "strings" + + "helm.sh/helm/v4/pkg/chart/common" ) // APIVersionV3 is the API version number for version 3. @@ -34,20 +36,20 @@ type Chart struct { // // This should not be used except in special cases like `helm show values`, // where we want to display the raw values, comments and all. - Raw []*File `json:"-"` + Raw []*common.File `json:"-"` // Metadata is the contents of the Chartfile. Metadata *Metadata `json:"metadata"` // Lock is the contents of Chart.lock. Lock *Lock `json:"lock"` // Templates for this chart. - Templates []*File `json:"templates"` + Templates []*common.File `json:"templates"` // Values are default config for this chart. Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. - Files []*File `json:"files"` + Files []*common.File `json:"files"` parent *Chart dependencies []*Chart @@ -59,7 +61,7 @@ type CRD struct { // Filename is the File obj Name including (sub-)chart.ChartFullPath Filename string // File is the File obj for the crd - File *File + File *common.File } // SetDependencies replaces the chart dependencies. @@ -134,8 +136,8 @@ func (ch *Chart) AppVersion() string { // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // Deprecated: use CRDObjects() -func (ch *Chart) CRDs() []*File { - files := []*File{} +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} // Find all resources in the crds/ directory for _, f := range ch.Files { if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index f93b3356b..b1820ac0a 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -20,11 +20,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/chart/common" ) func TestCRDs(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -57,7 +59,7 @@ func TestCRDs(t *testing.T) { func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ - Raw: []*File{ + Raw: []*common.File{ { Name: "fhqwhgads.yaml", Data: []byte("Everybody to the Limit"), @@ -76,7 +78,7 @@ func TestSaveChartNoRawData(t *testing.T) { t.Fatal(err) } - is.Equal([]*File(nil), res.Raw) + is.Equal([]*common.File(nil), res.Raw) } func TestMetadata(t *testing.T) { @@ -162,7 +164,7 @@ func TestChartFullPath(t *testing.T) { func TestCRDObjects(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -190,7 +192,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo.yaml", Data: []byte("hello"), }, @@ -198,7 +200,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo/bar/baz.yaml", Data: []byte("hello"), }, diff --git a/internal/chart/v3/lint/lint.go b/internal/chart/v3/lint/lint.go new file mode 100644 index 000000000..231bb6803 --- /dev/null +++ b/internal/chart/v3/lint/lint.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint // import "helm.sh/helm/v4/internal/chart/v3/lint" + +import ( + "path/filepath" + + "helm.sh/helm/v4/internal/chart/v3/lint/rules" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/pkg/chart/common" +) + +type linterOptions struct { + KubeVersion *common.KubeVersion + SkipSchemaValidation bool +} + +type LinterOption func(lo *linterOptions) + +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { + return func(lo *linterOptions) { + lo.KubeVersion = kubeVersion + } +} + +func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { + return func(lo *linterOptions) { + lo.SkipSchemaValidation = skipSchemaValidation + } +} + +func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter { + + chartDir, _ := filepath.Abs(baseDir) + + lo := linterOptions{} + for _, option := range options { + option(&lo) + } + + result := support.Linter{ + ChartDir: chartDir, + } + + rules.Chartfile(&result) + rules.ValuesWithOverrides(&result, values) + rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) + rules.Dependencies(&result) + rules.Crds(&result) + + return result +} diff --git a/internal/chart/v3/lint/lint_test.go b/internal/chart/v3/lint/lint_test.go new file mode 100644 index 000000000..af44cc58d --- /dev/null +++ b/internal/chart/v3/lint/lint_test.go @@ -0,0 +1,246 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +var values map[string]interface{} + +const namespace = "testNamespace" + +const badChartDir = "rules/testdata/badchartfile" +const badValuesFileDir = "rules/testdata/badvaluesfile" +const badYamlFileDir = "rules/testdata/albatross" +const badCrdFileDir = "rules/testdata/badcrdfile" +const goodChartDir = "rules/testdata/goodone" +const subChartValuesDir = "rules/testdata/withsubchart" +const malformedTemplate = "rules/testdata/malformed-template" +const invalidChartFileDir = "rules/testdata/invalidchartfile" + +func TestBadChartV3(t *testing.T) { + m := RunAll(badChartDir, values, namespace).Messages + if len(m) != 8 { + t.Errorf("Number of errors %v", len(m)) + t.Errorf("All didn't fail with expected errors, got %#v", m) + } + // There should be one INFO, one WARNING, and 2 ERROR messages, check for them + var i, w, e, e2, e3, e4, e5, e6 bool + for _, msg := range m { + if msg.Severity == support.InfoSev { + if strings.Contains(msg.Err.Error(), "icon is recommended") { + i = true + } + } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "does not exist") { + w = true + } + } + if msg.Severity == support.ErrorSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + e = true + } + if strings.Contains(msg.Err.Error(), "name is required") { + e2 = true + } + + if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") { + e3 = true + } + + if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { + e4 = true + } + + if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + e5 = true + } + // This comes from the dependency check, which loads dependency info from the Chart.yaml + if strings.Contains(msg.Err.Error(), "unable to load chart") { + e6 = true + } + } + } + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w { + t.Errorf("Didn't find all the expected errors, got %#v", m) + } +} + +func TestInvalidYaml(t *testing.T) { + m := RunAll(badYamlFileDir, values, namespace).Messages + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("All didn't have the error for deliberateSyntaxError") + } +} + +func TestInvalidChartYamlV3(t *testing.T) { + m := RunAll(invalidChartFileDir, values, namespace).Messages + t.Log(m) + if len(m) != 3 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") { + t.Errorf("All didn't have the error for duplicate YAML keys") + } +} + +func TestBadValuesV3(t *testing.T) { + m := RunAll(badValuesFileDir, values, namespace).Messages + if len(m) < 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { + t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) + } +} + +func TestBadCrdFileV3(t *testing.T) { + m := RunAll(badCrdFileDir, values, namespace).Messages + assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) + assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") + assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'") +} + +func TestGoodChart(t *testing.T) { + m := RunAll(goodChartDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + dir := t.TempDir() + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) + } +} + +// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws +// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags). +// +// See https://github.com/helm/helm/issues/11495 +// +// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent +// of the `--set` flag. +// +// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive +// results. +// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor= +// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor= +// or directly use '$(LDFLAGS)' in Makefile. +// +// When run without ldflags, the test passes giving a false-positive result. This is because the variables +// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there +// might not be the deprecation warning. +func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { + createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) + if err != nil { + t.Error(err) + return + } + + // Add values to enable hpa, and ingress which are disabled by default. + // This is the equivalent of: + // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' + updatedValues := map[string]interface{}{ + "autoscaling": map[string]interface{}{ + "enabled": true, + }, + "ingress": map[string]interface{}{ + "enabled": true, + }, + } + + linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true)) + for _, msg := range linterRunDetails.Messages { + if strings.HasPrefix(msg.Error(), "[WARNING]") && + strings.Contains(msg.Error(), "deprecated") { + // When there is a deprecation warning for an object created + // by `helm create` for the current Kubernetes version, fail. + t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error()) + } + } +} + +// lint ignores import-values +// See https://github.com/helm/helm/issues/9658 +func TestSubChartValuesChart(t *testing.T) { + m := RunAll(subChartValuesDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// lint stuck with malformed template object +// See https://github.com/helm/helm/issues/11391 +func TestMalformedTemplate(t *testing.T) { + c := time.After(3 * time.Second) + ch := make(chan int, 1) + var m []support.Message + go func() { + m = RunAll(malformedTemplate, values, namespace).Messages + ch <- 1 + }() + select { + case <-c: + t.Fatalf("lint malformed template timeout") + case <-ch: + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { + t.Errorf("All didn't have the error for invalid character '{'") + } + } +} diff --git a/internal/chart/v3/lint/rules/chartfile.go b/internal/chart/v3/lint/rules/chartfile.go new file mode 100644 index 000000000..e72a0d3b2 --- /dev/null +++ b/internal/chart/v3/lint/rules/chartfile.go @@ -0,0 +1,225 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/asaskevich/govalidator" + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +// Chartfile runs a set of linter rules related to Chart.yaml file +func Chartfile(linter *support.Linter) { + chartFileName := "Chart.yaml" + chartPath := filepath.Join(linter.ChartDir, chartFileName) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) + + chartFile, err := chartutil.LoadChartfile(chartPath) + validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) + + // Guard clause. Following linter rules require a parsable ChartFile + if !validChartFile { + return + } + + _, err = chartutil.StrictLoadChartfile(chartPath) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err)) + + // type check for Chart.yaml . ignoring error as any parse + // errors would already be caught in the above load function + chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) + + // Chart metadata + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) + linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) +} + +func validateChartVersionType(data map[string]interface{}) error { + return isStringValue(data, "version") +} + +func validateChartAppVersionType(data map[string]interface{}) error { + return isStringValue(data, "appVersion") +} + +func isStringValue(data map[string]interface{}, key string) error { + value, ok := data[key] + if !ok { + return nil + } + valueType := fmt.Sprintf("%T", value) + if valueType != "string" { + return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType) + } + return nil +} + +func validateChartYamlNotDirectory(chartPath string) error { + fi, err := os.Stat(chartPath) + + if err == nil && fi.IsDir() { + return errors.New("should be a file, not a directory") + } + return nil +} + +func validateChartYamlFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError) + } + return nil +} + +func validateChartYamlStrictFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError) + } + return nil +} + +func validateChartName(cf *chart.Metadata) error { + if cf.Name == "" { + return errors.New("name is required") + } + name := filepath.Base(cf.Name) + if name != cf.Name { + return fmt.Errorf("chart name %q is invalid", cf.Name) + } + return nil +} + +func validateChartAPIVersion(cf *chart.Metadata) error { + if cf.APIVersion == "" { + return errors.New("apiVersion is required. The value must be \"v3\"") + } + + if cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion) + } + + return nil +} + +func validateChartVersion(cf *chart.Metadata) error { + if cf.Version == "" { + return errors.New("version is required") + } + + version, err := semver.NewVersion(cf.Version) + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) + } + + c, err := semver.NewConstraint(">0.0.0-0") + if err != nil { + return err + } + valid, msg := c.Validate(version) + + if !valid && len(msg) > 0 { + return fmt.Errorf("version %v", msg[0]) + } + + return nil +} + +func validateChartMaintainer(cf *chart.Metadata) error { + for _, maintainer := range cf.Maintainers { + if maintainer == nil { + return errors.New("a maintainer entry is empty") + } + if maintainer.Name == "" { + return errors.New("each maintainer requires a name") + } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { + return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) + } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { + return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) + } + } + return nil +} + +func validateChartSources(cf *chart.Metadata) error { + for _, source := range cf.Sources { + if source == "" || !govalidator.IsRequestURL(source) { + return fmt.Errorf("invalid source URL '%s'", source) + } + } + return nil +} + +func validateChartIconPresence(cf *chart.Metadata) error { + if cf.Icon == "" { + return errors.New("icon is recommended") + } + return nil +} + +func validateChartIconURL(cf *chart.Metadata) error { + if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { + return fmt.Errorf("invalid icon URL '%s'", cf.Icon) + } + return nil +} + +func validateChartDependencies(cf *chart.Metadata) error { + if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +func validateChartType(cf *chart.Metadata) error { + if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +// loadChartFileForTypeCheck loads the Chart.yaml +// in a generic form of a map[string]interface{}, so that the type +// of the values can be checked +func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := make(map[string]interface{}) + err = yaml.Unmarshal(b, &y) + return y, err +} diff --git a/internal/chart/v3/lint/rules/chartfile_test.go b/internal/chart/v3/lint/rules/chartfile_test.go new file mode 100644 index 000000000..070cc244d --- /dev/null +++ b/internal/chart/v3/lint/rules/chartfile_test.go @@ -0,0 +1,276 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +const ( + badChartNameDir = "testdata/badchartname" + badChartDir = "testdata/badchartfile" + anotherBadChartDir = "testdata/anotherbadchartfile" +) + +var ( + badChartNamePath = filepath.Join(badChartNameDir, "Chart.yaml") + badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") + nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") +) + +var badChart, _ = chartutil.LoadChartfile(badChartFilePath) +var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) + +// Validation functions Test +func TestValidateChartYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) + defer os.Remove(nonExistingChartFilePath) + + err := validateChartYamlNotDirectory(nonExistingChartFilePath) + if err == nil { + t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") + } +} + +func TestValidateChartYamlFormat(t *testing.T) { + err := validateChartYamlFormat(errors.New("Read error")) + if err == nil { + t.Errorf("validateChartYamlFormat to return a linter error, got no error") + } + + err = validateChartYamlFormat(nil) + if err != nil { + t.Errorf("validateChartYamlFormat to return no error, got a linter error") + } +} + +func TestValidateChartName(t *testing.T) { + err := validateChartName(badChart) + if err == nil { + t.Errorf("validateChartName to return a linter error, got no error") + } + + err = validateChartName(badChartName) + if err == nil { + t.Error("expected validateChartName to return a linter error for an invalid name, got no error") + } +} + +func TestValidateChartVersion(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version is required"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, + {"waps", "'waps' is not a valid SemVer"}, + {"-3", "'-3' is not a valid SemVer"}, + } + + var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersion(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersion(badChart) + if err != nil { + t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + } + } +} + +func TestValidateChartMaintainer(t *testing.T) { + var failTest = []struct { + Name string + Email string + ErrorMsg string + }{ + {"", "", "each maintainer requires a name"}, + {"", "test@test.com", "each maintainer requires a name"}, + {"John Snow", "wrongFormatEmail.com", "invalid email"}, + } + + var successTest = []struct { + Name string + Email string + }{ + {"John Snow", ""}, + {"John Snow", "john@winterfell.com"}, + } + + for _, test := range failTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg) + } + } + + for _, test := range successTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err != nil { + t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) + } + } + + // Testing for an empty maintainer + badChart.Maintainers = []*chart.Maintainer{nil} + err := validateChartMaintainer(badChart) + if err == nil { + t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected") + } + if err.Error() != "a maintainer entry is empty" { + t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error()) + } +} + +func TestValidateChartSources(t *testing.T) { + var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} + for _, test := range failTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid source URL") { + t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestValidateChartIconPresence(t *testing.T) { + t.Run("Icon absent", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "", + } + + err := validateChartIconPresence(testChart) + + if err == nil { + t.Errorf("validateChartIconPresence to return a linter error, got no error") + } else if !strings.Contains(err.Error(), "icon is recommended") { + t.Errorf("expected %q, got %q", "icon is recommended", err.Error()) + } + }) + t.Run("Icon present", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "http://example.org/icon.png", + } + + err := validateChartIconPresence(testChart) + + if err != nil { + t.Errorf("Unexpected error: %q", err.Error()) + } + }) +} + +func TestValidateChartIconURL(t *testing.T) { + var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"} + for _, test := range failTest { + badChart.Icon = test + err := validateChartIconURL(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid icon URL") { + t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Icon = test + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestV3Chartfile(t *testing.T) { + t.Run("Chart.yaml basic validity issues", func(t *testing.T) { + linter := support.Linter{ChartDir: badChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 6 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } + }) + + t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { + linter := support.Linter{ChartDir: anotherBadChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 3 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + }) +} diff --git a/internal/chart/v3/lint/rules/crds.go b/internal/chart/v3/lint/rules/crds.go new file mode 100644 index 000000000..6bafb52eb --- /dev/null +++ b/internal/chart/v3/lint/rules/crds.go @@ -0,0 +1,113 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" +) + +// Crds lints the CRDs in the Linter. +func Crds(linter *support.Linter) { + fpath := "crds/" + crdsPath := filepath.Join(linter.ChartDir, fpath) + + // crds directory is optional + if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) { + return + } + + crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath)) + if !crdsDirValid { + return + } + + // Load chart and parse CRDs + chart, err := loader.Load(linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + /* Iterate over all the CRDs to check: + 1. It is a YAML file and not a template + 2. The API version is apiextensions.k8s.io + 3. The kind is CustomResourceDefinition + */ + for _, crd := range chart.CRDObjects() { + fileName := crd.Name + fpath = fileName + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096) + for { + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } + + // If YAML parsing fails here, it will always fail in the next block as well, so we should return here. + // This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct)) + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + fi, err := os.Stat(crdsPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateCrdAPIVersion(obj *k8sYamlStruct) error { + if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { + return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") + } + return nil +} + +func validateCrdKind(obj *k8sYamlStruct) error { + if obj.Kind != "CustomResourceDefinition" { + return fmt.Errorf("object kind is not 'CustomResourceDefinition'") + } + return nil +} diff --git a/internal/chart/v3/lint/rules/crds_test.go b/internal/chart/v3/lint/rules/crds_test.go new file mode 100644 index 000000000..d93e3d978 --- /dev/null +++ b/internal/chart/v3/lint/rules/crds_test.go @@ -0,0 +1,36 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" +) + +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + assert.Len(t, res, 1) + assert.ErrorContains(t, res[0].Err, "not a directory") +} diff --git a/internal/chart/v3/lint/rules/dependencies.go b/internal/chart/v3/lint/rules/dependencies.go new file mode 100644 index 000000000..f45153728 --- /dev/null +++ b/internal/chart/v3/lint/rules/dependencies.go @@ -0,0 +1,101 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import ( + "fmt" + "strings" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" +) + +// Dependencies runs lints against a chart's dependencies +// +// See https://github.com/helm/helm/issues/7910 +func Dependencies(linter *support.Linter) { + c, err := loader.LoadDir(linter.ChartDir) + if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c)) + linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) +} + +func validateChartFormat(chartError error) error { + if chartError != nil { + return fmt.Errorf("unable to load chart\n\t%w", chartError) + } + return nil +} + +func validateDependencyInChartsDir(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Dependencies() { + dependencies[dep.Metadata.Name] = struct{}{} + } + for _, dep := range c.Metadata.Dependencies { + if _, ok := dependencies[dep.Name]; !ok { + missing = append(missing, dep.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependencyInMetadata(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Metadata.Dependencies { + dependencies[dep.Name] = struct{}{} + } + for _, dep := range c.Dependencies() { + if _, ok := dependencies[dep.Metadata.Name]; !ok { + missing = append(missing, dep.Metadata.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependenciesUnique(c *chart.Chart) (err error) { + dependencies := map[string]*chart.Dependency{} + shadowing := []string{} + + for _, dep := range c.Metadata.Dependencies { + key := dep.Name + if dep.Alias != "" { + key = dep.Alias + } + if dependencies[key] != nil { + shadowing = append(shadowing, key) + } + dependencies[key] = dep + } + if len(shadowing) > 0 { + err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ",")) + } + return err +} diff --git a/internal/chart/v3/lint/rules/dependencies_test.go b/internal/chart/v3/lint/rules/dependencies_test.go new file mode 100644 index 000000000..b80e4b8a9 --- /dev/null +++ b/internal/chart/v3/lint/rules/dependencies_test.go @@ -0,0 +1,157 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package rules + +import ( + "path/filepath" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" +) + +func chartWithBadDependencies() chart.Chart { + badChartDeps := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "sub2", + }, + { + Name: "sub3", + }, + }, + }, + } + + badChartDeps.SetDependencies( + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub1", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub2", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + ) + return badChartDeps +} + +func TestValidateDependencyInChartsDir(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInChartsDir(&c); err == nil { + t.Error("chart should have been flagged for missing deps in chart directory") + } +} + +func TestValidateDependencyInMetadata(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInMetadata(&c); err == nil { + t.Errorf("chart should have been flagged for missing deps in chart metadata") + } +} + +func TestValidateDependenciesUnique(t *testing.T) { + tests := []struct { + chart chart.Chart + }{ + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + }, + { + Name: "foo", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "bar", + }, + { + Name: "bar", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "baz", + }, + { + Name: "bar", + Alias: "baz", + }, + }, + }, + }}, + } + + for _, tt := range tests { + if err := validateDependenciesUnique(&tt.chart); err == nil { + t.Errorf("chart should have been flagged for dependency shadowing") + } + } +} + +func TestDependencies(t *testing.T) { + tmp := t.TempDir() + + c := chartWithBadDependencies() + err := chartutil.SaveDir(&c, tmp) + if err != nil { + t.Fatal(err) + } + linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} + + Dependencies(&linter) + if l := len(linter.Messages); l != 2 { + t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) + for i, msg := range linter.Messages { + t.Logf("Message: %d, Error: %#v", i, msg) + } + } +} diff --git a/pkg/lint/rules/deprecations.go b/internal/chart/v3/lint/rules/deprecations.go similarity index 95% rename from pkg/lint/rules/deprecations.go rename to internal/chart/v3/lint/rules/deprecations.go index c6d635a5e..6f86bdbbd 100644 --- a/pkg/lint/rules/deprecations.go +++ b/internal/chart/v3/lint/rules/deprecations.go @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" import ( "fmt" "strconv" + "helm.sh/helm/v4/pkg/chart/common" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/deprecation" kscheme "k8s.io/client-go/kubernetes/scheme" - - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) var ( @@ -47,7 +47,7 @@ func (e deprecatedAPIError) Error() string { return msg } -func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *chartutil.KubeVersion) error { +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation if resource.APIVersion == "" { return nil diff --git a/internal/chart/v3/lint/rules/deprecations_test.go b/internal/chart/v3/lint/rules/deprecations_test.go new file mode 100644 index 000000000..35e541e5c --- /dev/null +++ b/internal/chart/v3/lint/rules/deprecations_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &k8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated, nil) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + depErr := err.(deprecatedAPIError) + if depErr.Message == "" { + t.Fatalf("Expected error message to be non-blank: %v", err) + } + + if err := validateNoDeprecations(&k8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }, nil); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go new file mode 100644 index 000000000..d4c62839f --- /dev/null +++ b/internal/chart/v3/lint/rules/template.go @@ -0,0 +1,348 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/api/validation" + apipath "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/internal/chart/v3/loader" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/engine" +) + +// Templates lints the templates in the Linter. +func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { + TemplatesWithKubeVersion(linter, values, namespace, nil) +} + +// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. +func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { + TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) +} + +// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. +func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { + fpath := "templates/" + templatesPath := filepath.Join(linter.ChartDir, fpath) + + // Templates directory is optional for now + templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) + if !templatesDirExists { + return + } + + validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) + if !validTemplatesDir { + return + } + + // Load chart and parse templates + chart, err := loader.Load(linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + options := common.ReleaseOptions{ + Name: "test-release", + Namespace: namespace, + } + + caps := common.DefaultCapabilities.Copy() + if kubeVersion != nil { + caps.KubeVersion = *kubeVersion + } + + // lint ignores import-values + // See https://github.com/helm/helm/issues/9658 + if err := chartutil.ProcessDependencies(chart, values); err != nil { + return + } + + cvals, err := util.CoalesceValues(chart, values) + if err != nil { + return + } + + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + if err != nil { + linter.RunLinterRule(support.ErrorSev, fpath, err) + return + } + var e engine.Engine + e.LintMode = true + renderedContentMap, err := e.Render(chart, valuesToRender) + + renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !renderOk { + return + } + + /* Iterate over all the templates to check: + - It is a .yaml file + - All the values in the template file is defined + - {{}} include | quote + - Generated content is a valid Yaml file + - Metadata.Namespace is not set + */ + for _, template := range chart.Templates { + fileName := template.Name + fpath = fileName + + linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) + + // We only apply the following lint rules to yaml files + if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { + continue + } + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 + // Check that all the templates have a matching value + // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) + + renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] + if strings.TrimSpace(renderedContent) != "" { + linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) + + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) + + // Lint all resources if the file contains multiple documents separated by --- + for { + // Even though k8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } + + // If YAML linting fails here, it will always fail in the next block as well, so we should return here. + // fix https://github.com/helm/helm/issues/11391 + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + if yamlStruct != nil { + // NOTE: set to warnings to allow users to support out-of-date kubernetes + // Refs https://github.com/helm/helm/issues/8596 + linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) + linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) + + linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) + linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) + } + } + } + } +} + +// validateTopIndentLevel checks that the content does not start with an indent level > 0. +// +// This error can occur when a template accidentally inserts space. It can cause +// unpredictable errors depending on whether the text is normalized before being passed +// into the YAML parser. So we trap it here. +// +// See https://github.com/helm/helm/issues/8467 +func validateTopIndentLevel(content string) error { + // Read lines until we get to a non-empty one + scanner := bufio.NewScanner(bytes.NewBufferString(content)) + for scanner.Scan() { + line := scanner.Text() + // If line is empty, skip + if strings.TrimSpace(line) == "" { + continue + } + // If it starts with one or more spaces, this is an error + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { + return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) + } + // Any other condition passes. + return nil + } + return scanner.Err() +} + +// Validation functions +func templatesDirExists(templatesPath string) error { + _, err := os.Stat(templatesPath) + if errors.Is(err, os.ErrNotExist) { + return errors.New("directory does not exist") + } + return nil +} + +func validateTemplatesDir(templatesPath string) error { + fi, err := os.Stat(templatesPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateAllowedExtension(fileName string) error { + ext := filepath.Ext(fileName) + validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"} + + if slices.Contains(validExtensions, ext) { + return nil + } + + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) +} + +func validateYamlContent(err error) error { + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + return nil +} + +// validateMetadataName uses the correct validation function for the object +// Kind, or if not set, defaults to the standard definition of a subdomain in +// DNS (RFC 1123), used by most resources. +func validateMetadataName(obj *k8sYamlStruct) error { + fn := validateMetadataNameFunc(obj) + allErrs := field.ErrorList{} + for _, msg := range fn(obj.Metadata.Name, false) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) + } + if len(allErrs) > 0 { + return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate()) + } + return nil +} + +// validateMetadataNameFunc will return a name validation function for the +// object kind, if defined below. +// +// Rules should match those set in the various api validations: +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 +// ... +// +// Implementing here to avoid importing k/k. +// +// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object +// kinds that don't have special requirements, so is the most likely to work if +// new kinds are added. +func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { + switch strings.ToLower(obj.Kind) { + case "pod", "node", "secret", "endpoints", "resourcequota", // core + "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps + "autoscaler", // autoscaler + "cronjob", "job", // batch + "lease", // coordination + "endpointslice", // discovery + "networkpolicy", "ingress", // networking + "podsecuritypolicy", // policy + "priorityclass", // scheduling + "podpreset", // settings + "storageclass", "volumeattachment", "csinode": // storage + return validation.NameIsDNSSubdomain + case "service": + return validation.NameIsDNS1035Label + case "namespace": + return validation.ValidateNamespaceName + case "serviceaccount": + return validation.ValidateServiceAccountName + case "certificatesigningrequest": + // No validation. + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 + return func(_ string, _ bool) []string { return nil } + case "role", "clusterrole", "rolebinding", "clusterrolebinding": + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 + return func(name string, _ bool) []string { + return apipath.IsValidPathSegmentName(name) + } + default: + return validation.NameIsDNSSubdomain + } +} + +// validateMatchSelector ensures that template specs have a selector declared. +// See https://github.com/helm/helm/issues/1990 +func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error { + switch yamlStruct.Kind { + case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": + // verify that matchLabels or matchExpressions is present + if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") { + return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) + } + } + return nil +} + +func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { + if yamlStruct.Kind == "List" { + m := struct { + Items []struct { + Metadata struct { + Annotations map[string]string + } + } + }{} + + if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { + return validateYamlContent(err) + } + + for _, i := range m.Items { + if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { + return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored") + } + } + } + return nil +} + +// k8sYamlStruct stubs a Kubernetes YAML file. +type k8sYamlStruct struct { + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string +} diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go new file mode 100644 index 000000000..40bcfa26b --- /dev/null +++ b/internal/chart/v3/lint/rules/template_test.go @@ -0,0 +1,441 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/lint/support" + chartutil "helm.sh/helm/v4/internal/chart/v3/util" + "helm.sh/helm/v4/pkg/chart/common" +) + +const templateTestBasedir = "./testdata/albatross" + +func TestValidateAllowedExtension(t *testing.T) { + var failTest = []string{"/foo", "/test.toml"} + for _, test := range failTest { + err := validateAllowedExtension(test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test) + } + } + var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} + for _, test := range successTest { + err := validateAllowedExtension(test) + if err != nil { + t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error()) + } + } +} + +var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} + +const namespace = "testNamespace" +const strict = false + +func TestTemplateParsing(t *testing.T) { + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("Unexpected error: %s", res[0]) + } +} + +var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") + +// Test a template with all the existing features: +// namespaces, partial templates +func TestTemplateIntegrationHappyPath(t *testing.T) { + // Rename file so it gets ignored by the linter + os.Rename(wrongTemplatePath, ignoredTemplatePath) + defer os.Rename(ignoredTemplatePath, wrongTemplatePath) + + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 0 { + t.Fatalf("Expected no error, got %d, %v", len(res), res) + } +} + +func TestMultiTemplateFail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected 1 error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("Unexpected error: %s", res[0].Err) + } +} + +func TestValidateMetadataName(t *testing.T) { + tests := []struct { + obj *k8sYamlStruct + wantErr bool + }{ + // Most kinds use IsDNS1123Subdomain. + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, + + // Service uses IsDNS1035Label. + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + + // Namespace uses IsDNS1123Label. + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, + + // CertificateSigningRequest has no validation. + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, + + // RBAC uses path validation. + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + + // Unknown Kind + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + + // No kind + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { + if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/baddeployment.yaml", + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + }, + { + Name: "templates/goodsecret.yaml", + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestStrictTemplateParsingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*common.File{ + { + Name: "templates/configmap.yaml", + Data: []byte(manifest), + }, + }, + } + dir := t.TempDir() + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates(linter, ch.Values, namespace, strict) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} + +func TestValidateMatchSelector(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: k8sYamlMetadata{ + Name: "mydeployment", + }, + } + manifest := ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchExpressions: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err == nil { + t.Error("expected Deployment with no selector to fail") + } +} + +func TestValidateTopIndentLevel(t *testing.T) { + for doc, shouldFail := range map[string]bool{ + // Should not fail + "\n\n\n\t\n \t\n": false, + "apiVersion:foo\n bar:baz": false, + "\n\n\napiVersion:foo\n\n\n": false, + // Should fail + " apiVersion:foo": true, + "\n\n apiVersion:foo\n\n": true, + } { + if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { + t.Errorf("Expected %t for %q", shouldFail, doc) + } + } + +} + +// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments +// See https://github.com/helm/helm/issues/8621 +func TestEmptyWithCommentsManifests(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "emptymanifests", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/empty-with-comments.yaml", + Data: []byte("#@formatter:off\n"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l > 0 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 0 lint errors, got %d", l) + } +} +func TestValidateListAnnotations(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "v1", + Kind: "List", + Metadata: k8sYamlMetadata{ + Name: "list", + }, + } + manifest := ` +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + annotations: + helm.sh/resource-policy: keep +` + + if err := validateListAnnotations(md, manifest); err == nil { + t.Fatal("expected list with nested keep annotations to fail") + } + + manifest = ` +apiVersion: v1 +kind: List +metadata: + annotations: + helm.sh/resource-policy: keep +items: + - apiVersion: v1 + kind: ConfigMap +` + + if err := validateListAnnotations(md, manifest); err != nil { + t.Fatalf("List objects keep annotations should pass. got: %s", err) + } +} diff --git a/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml b/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..5e1ed515c --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/albatross/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: albatross +description: testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl b/internal/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/_helpers.tpl rename to internal/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl diff --git a/pkg/lint/rules/testdata/albatross/templates/fail.yaml b/internal/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/fail.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml diff --git a/pkg/lint/rules/testdata/albatross/templates/svc.yaml b/internal/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/templates/svc.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml diff --git a/pkg/lint/rules/testdata/albatross/values.yaml b/internal/chart/v3/lint/rules/testdata/albatross/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/values.yaml rename to internal/chart/v3/lint/rules/testdata/albatross/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml new file mode 100644 index 000000000..8a598473b --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml @@ -0,0 +1,15 @@ +name: "some-chart" +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 72445e2 +home: "" +type: application +appVersion: 72225e2 +icon: "https://some-url.com/icon.jpeg" +dependencies: + - name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/lint/rules/testdata/badchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartfile/Chart.yaml rename to internal/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml diff --git a/pkg/lint/rules/testdata/badchartfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badchartfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badchartfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml new file mode 100644 index 000000000..41f452354 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badchartname/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: "../badchartname" +type: application diff --git a/pkg/lint/rules/testdata/badchartname/values.yaml b/internal/chart/v3/lint/rules/testdata/badchartname/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartname/values.yaml rename to internal/chart/v3/lint/rules/testdata/badchartname/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml new file mode 100644 index 000000000..3bf007393 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: badcrdfile +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml diff --git a/pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml diff --git a/pkg/lint/rules/testdata/badcrdfile/templates/.gitkeep b/internal/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/templates/.gitkeep rename to internal/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep diff --git a/pkg/lint/rules/testdata/badcrdfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badcrdfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badcrdfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml new file mode 100644 index 000000000..aace27e21 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: badvaluesfile +description: A Helm chart for Kubernetes +version: 0.0.1 +home: "" +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml rename to internal/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml diff --git a/pkg/lint/rules/testdata/badvaluesfile/values.yaml b/internal/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml b/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml new file mode 100644 index 000000000..bf8f5e309 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/goodone/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: goodone +description: good testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/goodone/crds/test-crd.yaml b/internal/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/crds/test-crd.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml diff --git a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml b/internal/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/templates/goodone.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml diff --git a/pkg/lint/rules/testdata/goodone/values.yaml b/internal/chart/v3/lint/rules/testdata/goodone/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/values.yaml rename to internal/chart/v3/lint/rules/testdata/goodone/values.yaml diff --git a/pkg/lint/rules/testdata/invalidchartfile/Chart.yaml b/internal/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidchartfile/Chart.yaml rename to internal/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml diff --git a/pkg/lint/rules/testdata/invalidchartfile/values.yaml b/internal/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidchartfile/values.yaml rename to internal/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml new file mode 100644 index 000000000..0f6d1ee98 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: invalidcrdsdir +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/crds b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/crds similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/crds rename to internal/chart/v3/lint/rules/testdata/invalidcrdsdir/crds diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml b/internal/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/values.yaml rename to internal/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml diff --git a/pkg/lint/rules/testdata/malformed-template/.helmignore b/internal/chart/v3/lint/rules/testdata/malformed-template/.helmignore similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/.helmignore rename to internal/chart/v3/lint/rules/testdata/malformed-template/.helmignore diff --git a/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml new file mode 100644 index 000000000..d46b98cb5 --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v3 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +icon: https://riverrun.io \ No newline at end of file diff --git a/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/templates/bad.yaml rename to internal/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml diff --git a/pkg/lint/rules/testdata/malformed-template/values.yaml b/internal/chart/v3/lint/rules/testdata/malformed-template/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/values.yaml rename to internal/chart/v3/lint/rules/testdata/malformed-template/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml b/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml new file mode 100644 index 000000000..bfb580bea --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: multi-template-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/internal/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml similarity index 100% rename from pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml rename to internal/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml diff --git a/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml new file mode 100644 index 000000000..2a29c33fa --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: v3-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl diff --git a/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/templates/service.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/templates/service.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml diff --git a/pkg/lint/rules/testdata/v3-fail/values.yaml b/internal/chart/v3/lint/rules/testdata/v3-fail/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/values.yaml rename to internal/chart/v3/lint/rules/testdata/v3-fail/values.yaml diff --git a/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml new file mode 100644 index 000000000..fa15eabaf --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v3 +name: withsubchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" +icon: http://riverrun.io + +dependencies: + - name: subchart + version: 0.1.16 + repository: "file://../subchart" + import-values: + - child: subchart + parent: subchart + diff --git a/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml new file mode 100644 index 000000000..35b13e70d --- /dev/null +++ b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: subchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/values.yaml b/internal/chart/v3/lint/rules/testdata/withsubchart/values.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/values.yaml rename to internal/chart/v3/lint/rules/testdata/withsubchart/values.yaml diff --git a/internal/chart/v3/lint/rules/values.go b/internal/chart/v3/lint/rules/values.go new file mode 100644 index 000000000..adf2e2c52 --- /dev/null +++ b/internal/chart/v3/lint/rules/values.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + + "helm.sh/helm/v4/internal/chart/v3/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" +) + +// ValuesWithOverrides tests the values.yaml file. +// +// If a schema is present in the chart, values are tested against that. Otherwise, +// they are only tested for well-formedness. +// +// If additional values are supplied, they are coalesced into the values in values.yaml. +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) { + file := "values.yaml" + vf := filepath.Join(linter.ChartDir, file) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) + + if !fileExists { + return + } + + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides)) +} + +func validateValuesFileExistence(valuesPath string) error { + _, err := os.Stat(valuesPath) + if err != nil { + return fmt.Errorf("file does not exist") + } + return nil +} + +func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { + values, err := common.ReadValuesFile(valuesPath) + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + + // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top + // level values against the top-level expectations. Subchart values are not linted. + // We could change that. For now, though, we retain that strategy, and thus can + // coalesce tables (like reuse-values does) instead of doing the full chart + // CoalesceValues + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) + + ext := filepath.Ext(valuesPath) + schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" + schema, err := os.ReadFile(schemaPath) + if len(schema) == 0 { + return nil + } + if err != nil { + return err + } + return util.ValidateAgainstSingleSchema(coalescedValues, schema) +} diff --git a/pkg/lint/rules/values_test.go b/internal/chart/v3/lint/rules/values_test.go similarity index 100% rename from pkg/lint/rules/values_test.go rename to internal/chart/v3/lint/rules/values_test.go diff --git a/internal/chart/v3/util/errors_test.go b/internal/chart/v3/lint/support/doc.go similarity index 67% rename from internal/chart/v3/util/errors_test.go rename to internal/chart/v3/lint/support/doc.go index b8ae86384..2d54a9b7d 100644 --- a/internal/chart/v3/util/errors_test.go +++ b/internal/chart/v3/lint/support/doc.go @@ -14,24 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util - -import ( - "testing" -) - -func TestErrorNoTableDoesNotPanic(t *testing.T) { - x := "empty" - - y := ErrNoTable{x} - - t.Logf("error is: %s", y) -} - -func TestErrorNoValueDoesNotPanic(t *testing.T) { - x := "empty" - - y := ErrNoValue{x} +/* +Package support contains tools for linting charts. - t.Logf("error is: %s", y) -} +Linting is the process of testing charts for errors or warnings regarding +formatting, compilation, or standards compliance. +*/ +package support // import "helm.sh/helm/v4/internal/chart/v3/lint/support" diff --git a/pkg/lint/support/message.go b/internal/chart/v3/lint/support/message.go similarity index 100% rename from pkg/lint/support/message.go rename to internal/chart/v3/lint/support/message.go diff --git a/pkg/lint/support/message_test.go b/internal/chart/v3/lint/support/message_test.go similarity index 100% rename from pkg/lint/support/message_test.go rename to internal/chart/v3/lint/support/message_test.go diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 30bafdad4..2959fc71d 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) // ChartLoader loads a chart. @@ -79,7 +80,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -115,10 +116,10 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { c.Schema = f.Data case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) continue } @@ -126,7 +127,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { cname := strings.SplitN(fname, "/", 2)[0] subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) default: - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } } diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index e770923ff..1d8ca836a 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -31,6 +31,7 @@ import ( "time" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) func TestLoadDir(t *testing.T) { @@ -491,7 +492,7 @@ foo: } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesV3(t *testing.T) { nestedMap := map[string]interface{}{ "foo": "bar", "baz": map[string]string{ @@ -701,7 +702,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { } } -func verifyBomStripped(t *testing.T, files []*chart.File) { +func verifyBomStripped(t *testing.T, files []*common.File) { t.Helper() for _, file := range files { if bytes.HasPrefix(file.Data, utf8bom) { diff --git a/internal/chart/v3/util/capabilities.go b/internal/chart/v3/util/capabilities.go deleted file mode 100644 index 23b6d46fa..000000000 --- a/internal/chart/v3/util/capabilities.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" - "slices" - "strconv" - - "github.com/Masterminds/semver/v3" - "k8s.io/client-go/kubernetes/scheme" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - - helmversion "helm.sh/helm/v4/internal/version" -) - -var ( - // The Kubernetes version can be set by LDFLAGS. In order to do that the value - // must be a string. - k8sVersionMajor = "1" - k8sVersionMinor = "20" - - // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). - DefaultVersionSet = allKnownVersions() - - // DefaultCapabilities is the default set of capabilities. - DefaultCapabilities = &Capabilities{ - KubeVersion: KubeVersion{ - Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor), - Major: k8sVersionMajor, - Minor: k8sVersionMinor, - }, - APIVersions: DefaultVersionSet, - HelmVersion: helmversion.Get(), - } -) - -// Capabilities describes the capabilities of the Kubernetes cluster. -type Capabilities struct { - // KubeVersion is the Kubernetes version. - KubeVersion KubeVersion - // APIVersions are supported Kubernetes API versions. - APIVersions VersionSet - // HelmVersion is the build information for this helm version - HelmVersion helmversion.BuildInfo -} - -func (capabilities *Capabilities) Copy() *Capabilities { - return &Capabilities{ - KubeVersion: capabilities.KubeVersion, - APIVersions: capabilities.APIVersions, - HelmVersion: capabilities.HelmVersion, - } -} - -// KubeVersion is the Kubernetes version. -type KubeVersion struct { - Version string // Kubernetes version - Major string // Kubernetes major version - Minor string // Kubernetes minor version -} - -// String implements fmt.Stringer -func (kv *KubeVersion) String() string { return kv.Version } - -// GitVersion returns the Kubernetes version string. -// -// Deprecated: use KubeVersion.Version. -func (kv *KubeVersion) GitVersion() string { return kv.Version } - -// ParseKubeVersion parses kubernetes version from string -func ParseKubeVersion(version string) (*KubeVersion, error) { - sv, err := semver.NewVersion(version) - if err != nil { - return nil, err - } - return &KubeVersion{ - Version: "v" + sv.String(), - Major: strconv.FormatUint(sv.Major(), 10), - Minor: strconv.FormatUint(sv.Minor(), 10), - }, nil -} - -// VersionSet is a set of Kubernetes API versions. -type VersionSet []string - -// Has returns true if the version string is in the set. -// -// vs.Has("apps/v1") -func (v VersionSet) Has(apiVersion string) bool { - return slices.Contains(v, apiVersion) -} - -func allKnownVersions() VersionSet { - // We should register the built in extension APIs as well so CRDs are - // supported in the default version set. This has caused problems with `helm - // template` in the past, so let's be safe - apiextensionsv1beta1.AddToScheme(scheme.Scheme) - apiextensionsv1.AddToScheme(scheme.Scheme) - - groups := scheme.Scheme.PrioritizedVersionsAllGroups() - vs := make(VersionSet, 0, len(groups)) - for _, gv := range groups { - vs = append(vs, gv.String()) - } - return vs -} diff --git a/internal/chart/v3/util/capabilities_test.go b/internal/chart/v3/util/capabilities_test.go deleted file mode 100644 index aa9be9db8..000000000 --- a/internal/chart/v3/util/capabilities_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "testing" -) - -func TestVersionSet(t *testing.T) { - vs := VersionSet{"v1", "apps/v1"} - if d := len(vs); d != 2 { - t.Errorf("Expected 2 versions, got %d", d) - } - - if !vs.Has("apps/v1") { - t.Error("Expected to find apps/v1") - } - - if vs.Has("Spanish/inquisition") { - t.Error("No one expects the Spanish/inquisition") - } -} - -func TestDefaultVersionSet(t *testing.T) { - if !DefaultVersionSet.Has("v1") { - t.Error("Expected core v1 version set") - } -} - -func TestDefaultCapabilities(t *testing.T) { - kv := DefaultCapabilities.KubeVersion - if kv.String() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) - } - if kv.Version != "v1.20.0" { - t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version) - } - if kv.GitVersion() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version) - } - if kv.Major != "1" { - t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "20" { - t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) - } -} - -func TestDefaultCapabilitiesHelmVersion(t *testing.T) { - hv := DefaultCapabilities.HelmVersion - - if hv.Version != "v4.0" { - t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version) - } -} - -func TestParseKubeVersion(t *testing.T) { - kv, err := ParseKubeVersion("v1.16.0") - if err != nil { - t.Errorf("Expected v1.16.0 to parse successfully") - } - if kv.Version != "v1.16.0" { - t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String()) - } - if kv.Major != "1" { - t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "16" { - t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor) - } -} diff --git a/internal/chart/v3/util/coalesce.go b/internal/chart/v3/util/coalesce.go deleted file mode 100644 index caea2e119..000000000 --- a/internal/chart/v3/util/coalesce.go +++ /dev/null @@ -1,308 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" - "log" - "maps" - - "github.com/mitchellh/copystructure" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func concatPrefix(a, b string) string { - if a == "" { - return b - } - return fmt.Sprintf("%s.%s", a, b) -} - -// CoalesceValues coalesces all of the values in a chart (and its subcharts). -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", false) -} - -// MergeValues is used to merge the values in a chart and its subcharts. This -// is different from Coalescing as nil/null values are preserved. -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -// -// Retaining Nils is useful when processes early in a Helm action or business -// logic need to retain them for when Coalescing will happen again later in the -// business logic. -func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", true) -} - -func copyValues(vals map[string]interface{}) (Values, error) { - v, err := copystructure.Copy(vals) - if err != nil { - return vals, err - } - - valsCopy := v.(map[string]interface{}) - // if we have an empty map, make sure it is initialized - if valsCopy == nil { - valsCopy = make(map[string]interface{}) - } - - return valsCopy, nil -} - -type printFn func(format string, v ...interface{}) - -// coalesce coalesces the dest values and the chart values, giving priority to the dest values. -// -// This is a helper function for CoalesceValues and MergeValues. -// -// Note, the merge argument specifies whether this is being used by MergeValues -// or CoalesceValues. Coalescing removes null values and their keys in some -// situations while merging keeps the null values. -func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - coalesceValues(printf, ch, dest, prefix, merge) - return coalesceDeps(printf, ch, dest, prefix, merge) -} - -// coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies() { - if c, ok := dest[subchart.Name()]; !ok { - // If dest doesn't already have the key, create it. - dest[subchart.Name()] = make(map[string]interface{}) - } else if !istable(c) { - return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c) - } - if dv, ok := dest[subchart.Name()]; ok { - dvmap := dv.(map[string]interface{}) - subPrefix := concatPrefix(prefix, chrt.Metadata.Name) - // Get globals out of dest and merge them into dvmap. - coalesceGlobals(printf, dvmap, dest, subPrefix, merge) - // Now coalesce the rest of the values. - var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) - if err != nil { - return dest, err - } - } - } - return dest, nil -} - -// coalesceGlobals copies the globals out of src and merges them into dest. -// -// For convenience, returns dest. -func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { - var dg, sg map[string]interface{} - - if destglob, ok := dest[GlobalKey]; !ok { - dg = make(map[string]interface{}) - } else if dg, ok = destglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because destination %s is not a table.", GlobalKey) - return - } - - if srcglob, ok := src[GlobalKey]; !ok { - sg = make(map[string]interface{}) - } else if sg, ok = srcglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because source %s is not a table.", GlobalKey) - return - } - - // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This - // reverses that decision. It may somehow be possible to introduce a loop - // here, but I haven't found a way. So for the time being, let's allow - // tables in globals. - for key, val := range sg { - if istable(val) { - vv := copyMap(val.(map[string]interface{})) - if destv, ok := dg[key]; !ok { - // Here there is no merge. We're just adding. - dg[key] = vv - } else { - if destvmap, ok := destv.(map[string]interface{}); !ok { - printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) - } else { - // Basically, we reverse order of coalesce here to merge - // top-down. - subPrefix := concatPrefix(prefix, key) - // In this location coalesceTablesFullKey should always have - // merge set to true. The output of coalesceGlobals is run - // through coalesce where any nils will be removed. - coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true) - dg[key] = vv - } - } - } else if dv, ok := dg[key]; ok && istable(dv) { - // It's not clear if this condition can actually ever trigger. - printf("key %s is table. Skipping", key) - } else { - // TODO: Do we need to do any additional checking on the value? - dg[key] = val - } - } - dest[GlobalKey] = dg -} - -func copyMap(src map[string]interface{}) map[string]interface{} { - m := make(map[string]interface{}, len(src)) - maps.Copy(m, src) - return m -} - -// coalesceValues builds up a values map for a particular chart. -// -// Values in v will override the values in the chart. -func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { - subPrefix := concatPrefix(prefix, c.Metadata.Name) - - // Using c.Values directly when coalescing a table can cause problems where - // the original c.Values is altered. Creating a deep copy stops the problem. - // This section is fault-tolerant as there is no ability to return an error. - valuesCopy, err := copystructure.Copy(c.Values) - var vc map[string]interface{} - var ok bool - if err != nil { - // If there is an error something is wrong with copying c.Values it - // means there is a problem in the deep copying package or something - // wrong with c.Values. In this case we will use c.Values and report - // an error. - printf("warning: unable to copy values, err: %s", err) - vc = c.Values - } else { - vc, ok = valuesCopy.(map[string]interface{}) - if !ok { - // c.Values has a map[string]interface{} structure. If the copy of - // it cannot be treated as map[string]interface{} there is something - // strangely wrong. Log it and use c.Values - printf("warning: unable to convert values copy to values type") - vc = c.Values - } - } - - for key, val := range vc { - if value, ok := v[key]; ok { - if value == nil && !merge { - // When the YAML value is null and we are coalescing instead of - // merging, we remove the value's key. - // This allows Helm's various sources of values (value files or --set) to - // remove incompatible keys from any previous chart, file, or set values. - delete(v, key) - } else if dest, ok := value.(map[string]interface{}); ok { - // if v[key] is a table, merge nv's val table into v[key]. - src, ok := val.(map[string]interface{}) - if !ok { - // If the original value is nil, there is nothing to coalesce, so we don't print - // the warning - if val != nil { - printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key) - } - } else { - // If the key is a child chart, coalesce tables with Merge set to true - merge := childChartMergeTrue(c, key, merge) - - // Because v has higher precedence than nv, dest values override src - // values. - coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) - } - } - } else { - // If the key is not in v, copy it from nv. - v[key] = val - } - } -} - -func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool { - for _, subchart := range chrt.Dependencies() { - if subchart.Name() == key { - return true - } - } - return merge -} - -// CoalesceTables merges a source map into a destination map. -// -// dest is considered authoritative. -func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", false) -} - -func MergeTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", true) -} - -// coalesceTablesFullKey merges a source map into a destination map. -// -// dest is considered authoritative. -func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} { - // When --reuse-values is set but there are no modifications yet, return new values - if src == nil { - return dst - } - if dst == nil { - return src - } - for key, val := range dst { - if val == nil { - src[key] = nil - } - } - // Because dest has higher precedence than src, dest values override src - // values. - for key, val := range src { - fullkey := concatPrefix(prefix, key) - if dv, ok := dst[key]; ok && !merge && dv == nil { - delete(dst, key) - } else if !ok { - dst[key] = val - } else if istable(val) { - if istable(dv) { - coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) - } else { - printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) - } - } else if istable(dv) && val != nil { - printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val) - } - } - return dst -} diff --git a/internal/chart/v3/util/coalesce_test.go b/internal/chart/v3/util/coalesce_test.go deleted file mode 100644 index 4770b601d..000000000 --- a/internal/chart/v3/util/coalesce_test.go +++ /dev/null @@ -1,723 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "encoding/json" - "fmt" - "maps" - "testing" - - "github.com/stretchr/testify/assert" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 -var testCoalesceValuesYaml = []byte(` -top: yup -bottom: null -right: Null -left: NULL -front: ~ -back: "" -nested: - boat: null - -global: - name: Ishmael - subject: Queequeg - nested: - boat: true - -pequod: - boat: null - global: - name: Stinky - harpooner: Tashtego - nested: - boat: false - sail: true - foo2: null - ahab: - scope: whale - boat: null - nested: - foo: true - boat: null - object: null -`) - -func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { - c.AddDependency(deps...) - return c -} - -func TestCoalesceValues(t *testing.T) { - is := assert.New(t) - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "moby"}, - Values: map[string]interface{}{ - "back": "exists", - "bottom": "exists", - "front": "exists", - "left": "exists", - "name": "moby", - "nested": map[string]interface{}{"boat": true}, - "override": "bad", - "right": "exists", - "scope": "moby", - "top": "nope", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l0": "moby"}, - }, - "pequod": map[string]interface{}{ - "boat": "maybe", - "ahab": map[string]interface{}{ - "boat": "maybe", - "nested": map[string]interface{}{"boat": "maybe"}, - }, - }, - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "pequod"}, - Values: map[string]interface{}{ - "name": "pequod", - "scope": "pequod", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "pequod"}, - }, - "boat": false, - "ahab": map[string]interface{}{ - "boat": false, - "nested": map[string]interface{}{"boat": false}, - }, - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "ahab"}, - Values: map[string]interface{}{ - "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"}, - "nested2": map[string]interface{}{"l2": "ahab"}, - }, - "scope": "ahab", - "name": "ahab", - "boat": true, - "nested": map[string]interface{}{"foo": false, "boat": true}, - "object": map[string]interface{}{"foo": "bar"}, - }, - }, - ), - &chart.Chart{ - Metadata: &chart.Metadata{Name: "spouter"}, - Values: map[string]interface{}{ - "scope": "spouter", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "spouter"}, - }, - }, - }, - ) - - vals, err := ReadValues(testCoalesceValuesYaml) - if err != nil { - t.Fatal(err) - } - - // taking a copy of the values before passing it - // to CoalesceValues as argument, so that we can - // use it for asserting later - valsCopy := make(Values, len(vals)) - maps.Copy(valsCopy, vals) - - v, err := CoalesceValues(c, vals) - if err != nil { - t.Fatal(err) - } - j, _ := json.MarshalIndent(v, "", " ") - t.Logf("Coalesced Values: %s", string(j)) - - tests := []struct { - tpl string - expect string - }{ - {"{{.top}}", "yup"}, - {"{{.back}}", ""}, - {"{{.name}}", "moby"}, - {"{{.global.name}}", "Ishmael"}, - {"{{.global.subject}}", "Queequeg"}, - {"{{.global.harpooner}}", ""}, - {"{{.pequod.name}}", "pequod"}, - {"{{.pequod.ahab.name}}", "ahab"}, - {"{{.pequod.ahab.scope}}", "whale"}, - {"{{.pequod.ahab.nested.foo}}", "true"}, - {"{{.pequod.ahab.global.name}}", "Ishmael"}, - {"{{.pequod.ahab.global.nested.foo}}", "bar"}, - {"{{.pequod.ahab.global.nested.foo2}}", ""}, - {"{{.pequod.ahab.global.subject}}", "Queequeg"}, - {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, - {"{{.pequod.global.name}}", "Ishmael"}, - {"{{.pequod.global.nested.foo}}", ""}, - {"{{.pequod.global.subject}}", "Queequeg"}, - {"{{.spouter.global.name}}", "Ishmael"}, - {"{{.spouter.global.harpooner}}", ""}, - - {"{{.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.boat}}", "true"}, - {"{{.spouter.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.sail}}", "true"}, - {"{{.spouter.global.nested.sail}}", ""}, - - {"{{.global.nested2.l0}}", "moby"}, - {"{{.global.nested2.l1}}", ""}, - {"{{.global.nested2.l2}}", ""}, - {"{{.pequod.global.nested2.l0}}", "moby"}, - {"{{.pequod.global.nested2.l1}}", "pequod"}, - {"{{.pequod.global.nested2.l2}}", ""}, - {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, - {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, - {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, - {"{{.spouter.global.nested2.l0}}", "moby"}, - {"{{.spouter.global.nested2.l1}}", "spouter"}, - {"{{.spouter.global.nested2.l2}}", ""}, - } - - for _, tt := range tests { - if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { - t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) - } - } - - nullKeys := []string{"bottom", "right", "left", "front"} - for _, nullKey := range nullKeys { - if _, ok := v[nullKey]; ok { - t.Errorf("Expected key %q to be removed, still present", nullKey) - } - } - - if _, ok := v["nested"].(map[string]interface{})["boat"]; ok { - t.Error("Expected nested boat key to be removed, still present") - } - - subchart := v["pequod"].(map[string]interface{}) - if _, ok := subchart["boat"]; ok { - t.Error("Expected subchart boat key to be removed, still present") - } - - subsubchart := subchart["ahab"].(map[string]interface{}) - if _, ok := subsubchart["boat"]; ok { - t.Error("Expected sub-subchart ahab boat key to be removed, still present") - } - - if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok { - t.Error("Expected sub-subchart nested boat key to be removed, still present") - } - - if _, ok := subsubchart["object"]; ok { - t.Error("Expected sub-subchart object map to be removed, still present") - } - - // CoalesceValues should not mutate the passed arguments - is.Equal(valsCopy, vals) -} - -func TestMergeValues(t *testing.T) { - is := assert.New(t) - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "moby"}, - Values: map[string]interface{}{ - "back": "exists", - "bottom": "exists", - "front": "exists", - "left": "exists", - "name": "moby", - "nested": map[string]interface{}{"boat": true}, - "override": "bad", - "right": "exists", - "scope": "moby", - "top": "nope", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l0": "moby"}, - }, - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "pequod"}, - Values: map[string]interface{}{ - "name": "pequod", - "scope": "pequod", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "pequod"}, - }, - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "ahab"}, - Values: map[string]interface{}{ - "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar"}, - "nested2": map[string]interface{}{"l2": "ahab"}, - }, - "scope": "ahab", - "name": "ahab", - "boat": true, - "nested": map[string]interface{}{"foo": false, "bar": true}, - }, - }, - ), - &chart.Chart{ - Metadata: &chart.Metadata{Name: "spouter"}, - Values: map[string]interface{}{ - "scope": "spouter", - "global": map[string]interface{}{ - "nested2": map[string]interface{}{"l1": "spouter"}, - }, - }, - }, - ) - - vals, err := ReadValues(testCoalesceValuesYaml) - if err != nil { - t.Fatal(err) - } - - // taking a copy of the values before passing it - // to MergeValues as argument, so that we can - // use it for asserting later - valsCopy := make(Values, len(vals)) - maps.Copy(valsCopy, vals) - - v, err := MergeValues(c, vals) - if err != nil { - t.Fatal(err) - } - j, _ := json.MarshalIndent(v, "", " ") - t.Logf("Coalesced Values: %s", string(j)) - - tests := []struct { - tpl string - expect string - }{ - {"{{.top}}", "yup"}, - {"{{.back}}", ""}, - {"{{.name}}", "moby"}, - {"{{.global.name}}", "Ishmael"}, - {"{{.global.subject}}", "Queequeg"}, - {"{{.global.harpooner}}", ""}, - {"{{.pequod.name}}", "pequod"}, - {"{{.pequod.ahab.name}}", "ahab"}, - {"{{.pequod.ahab.scope}}", "whale"}, - {"{{.pequod.ahab.nested.foo}}", "true"}, - {"{{.pequod.ahab.global.name}}", "Ishmael"}, - {"{{.pequod.ahab.global.nested.foo}}", "bar"}, - {"{{.pequod.ahab.global.subject}}", "Queequeg"}, - {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, - {"{{.pequod.global.name}}", "Ishmael"}, - {"{{.pequod.global.nested.foo}}", ""}, - {"{{.pequod.global.subject}}", "Queequeg"}, - {"{{.spouter.global.name}}", "Ishmael"}, - {"{{.spouter.global.harpooner}}", ""}, - - {"{{.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.boat}}", "true"}, - {"{{.spouter.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.sail}}", "true"}, - {"{{.spouter.global.nested.sail}}", ""}, - - {"{{.global.nested2.l0}}", "moby"}, - {"{{.global.nested2.l1}}", ""}, - {"{{.global.nested2.l2}}", ""}, - {"{{.pequod.global.nested2.l0}}", "moby"}, - {"{{.pequod.global.nested2.l1}}", "pequod"}, - {"{{.pequod.global.nested2.l2}}", ""}, - {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, - {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, - {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, - {"{{.spouter.global.nested2.l0}}", "moby"}, - {"{{.spouter.global.nested2.l1}}", "spouter"}, - {"{{.spouter.global.nested2.l2}}", ""}, - } - - for _, tt := range tests { - if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { - t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) - } - } - - // nullKeys is different from coalescing. Here the null/nil values are not - // removed. - nullKeys := []string{"bottom", "right", "left", "front"} - for _, nullKey := range nullKeys { - if vv, ok := v[nullKey]; !ok { - t.Errorf("Expected key %q to be present but it was removed", nullKey) - } else if vv != nil { - t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv) - } - } - - if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok { - t.Error("Expected nested boat key to be present but it was removed") - } - - subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) - if _, ok := subchart["boat"]; !ok { - t.Error("Expected subchart boat key to be present but it was removed") - } - - if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok { - t.Error("Expected subchart nested bar key to be present but it was removed") - } - - // CoalesceValues should not mutate the passed arguments - is.Equal(valsCopy, vals) -} - -func TestCoalesceTables(t *testing.T) { - dst := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": nil, - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": nil, - } - src := map[string]interface{}{ - "occupation": "whaler", - "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", - "country": "US", - }, - "details": "empty", - "boat": map[string]interface{}{ - "mast": true, - }, - "hole": "black", - } - - // What we expect is that anything in dst overrides anything in src, but that - // otherwise the values are coalesced. - CoalesceTables(dst, src) - - if dst["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst["name"]) - } - if dst["occupation"] != "whaler" { - t.Errorf("Unexpected occupation: %s", dst["occupation"]) - } - - addr, ok := dst["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr["street"]) - } - - if addr["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr["city"]) - } - - if addr["state"].(string) != "MA" { - t.Errorf("Unexpected state: %v", addr["state"]) - } - - if _, ok = addr["country"]; ok { - t.Error("The country is not left out.") - } - - if det, ok := dst["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst["details"]) - } else if _, ok := det["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst["boat"]) - } - - if _, ok = dst["hole"]; ok { - t.Error("The hole still exists.") - } - - dst2 := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": "US", - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": "black", - } - - // What we expect is that anything in dst should have all values set, - // this happens when the --reuse-values flag is set but the chart has no modifications yet - CoalesceTables(dst2, nil) - - if dst2["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst2["name"]) - } - - addr2, ok := dst2["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr2["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr2["street"]) - } - - if addr2["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr2["city"]) - } - - if addr2["country"].(string) != "US" { - t.Errorf("Unexpected Country: %v", addr2["country"]) - } - - if det2, ok := dst2["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst2["details"]) - } else if _, ok := det2["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst2["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst2["boat"]) - } - - if dst2["hole"].(string) != "black" { - t.Errorf("Expected hole string, got %v", dst2["boat"]) - } -} - -func TestMergeTables(t *testing.T) { - dst := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": nil, - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": nil, - } - src := map[string]interface{}{ - "occupation": "whaler", - "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", - "country": "US", - }, - "details": "empty", - "boat": map[string]interface{}{ - "mast": true, - }, - "hole": "black", - } - - // What we expect is that anything in dst overrides anything in src, but that - // otherwise the values are coalesced. - MergeTables(dst, src) - - if dst["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst["name"]) - } - if dst["occupation"] != "whaler" { - t.Errorf("Unexpected occupation: %s", dst["occupation"]) - } - - addr, ok := dst["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr["street"]) - } - - if addr["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr["city"]) - } - - if addr["state"].(string) != "MA" { - t.Errorf("Unexpected state: %v", addr["state"]) - } - - // This is one test that is different from CoalesceTables. Because country - // is a nil value and it's not removed it's still present. - if _, ok = addr["country"]; !ok { - t.Error("The country is left out.") - } - - if det, ok := dst["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst["details"]) - } else if _, ok := det["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst["boat"]) - } - - // This is one test that is different from CoalesceTables. Because hole - // is a nil value and it's not removed it's still present. - if _, ok = dst["hole"]; !ok { - t.Error("The hole no longer exists.") - } - - dst2 := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - "country": "US", - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - "hole": "black", - "nilval": nil, - } - - // What we expect is that anything in dst should have all values set, - // this happens when the --reuse-values flag is set but the chart has no modifications yet - MergeTables(dst2, nil) - - if dst2["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst2["name"]) - } - - addr2, ok := dst2["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr2["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr2["street"]) - } - - if addr2["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr2["city"]) - } - - if addr2["country"].(string) != "US" { - t.Errorf("Unexpected Country: %v", addr2["country"]) - } - - if det2, ok := dst2["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst2["details"]) - } else if _, ok := det2["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst2["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst2["boat"]) - } - - if dst2["hole"].(string) != "black" { - t.Errorf("Expected hole string, got %v", dst2["boat"]) - } - - if dst2["nilval"] != nil { - t.Error("Expected nilvalue to have nil value but it does not") - } -} - -func TestCoalesceValuesWarnings(t *testing.T) { - - c := withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "level1"}, - Values: map[string]interface{}{ - "name": "moby", - }, - }, - withDeps(&chart.Chart{ - Metadata: &chart.Metadata{Name: "level2"}, - Values: map[string]interface{}{ - "name": "pequod", - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{Name: "level3"}, - Values: map[string]interface{}{ - "name": "ahab", - "boat": true, - "spear": map[string]interface{}{ - "tip": true, - "sail": map[string]interface{}{ - "cotton": true, - }, - }, - }, - }, - ), - ) - - vals := map[string]interface{}{ - "level2": map[string]interface{}{ - "level3": map[string]interface{}{ - "boat": map[string]interface{}{"mast": true}, - "spear": map[string]interface{}{ - "tip": map[string]interface{}{ - "sharp": true, - }, - "sail": true, - }, - }, - }, - } - - warnings := make([]string, 0) - printf := func(format string, v ...interface{}) { - t.Logf(format, v...) - warnings = append(warnings, fmt.Sprintf(format, v...)) - } - - _, err := coalesce(printf, c, vals, "", false) - if err != nil { - t.Fatal(err) - } - - t.Logf("vals: %v", vals) - assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.") - assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)") - assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])") - -} - -func TestConcatPrefix(t *testing.T) { - assert.Equal(t, "b", concatPrefix("", "b")) - assert.Equal(t, "a.b", concatPrefix("a", "b")) -} diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index 6a28f99d4..9f742e646 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -28,6 +28,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) // chartName is a regular expression for testing the supplied name of a chart. @@ -655,11 +656,11 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { schart.Metadata = chartfile - var updatedTemplates []*chart.File + var updatedTemplates []*common.File for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) } schart.Templates = updatedTemplates diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index 129c46372..489772115 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -23,10 +23,12 @@ import ( "github.com/mitchellh/copystructure" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" ) // ProcessDependencies checks through this chart's dependencies, processing accordingly. -func ProcessDependencies(c *chart.Chart, v Values) error { +func ProcessDependencies(c *chart.Chart, v common.Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } @@ -34,7 +36,7 @@ func ProcessDependencies(c *chart.Chart, v Values) error { } // processDependencyConditions disables charts based on condition path value in values -func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { if reqs == nil { return } @@ -50,7 +52,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if _, ok := err.(ErrNoValue); !ok { + } else if _, ok := err.(common.ErrNoValue); !ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } @@ -60,7 +62,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s } // processDependencyTags disables charts based on tags in values -func processDependencyTags(reqs []*chart.Dependency, cvals Values) { +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { if reqs == nil { return } @@ -177,7 +179,7 @@ Loop: for _, lr := range c.Metadata.Dependencies { lr.Enabled = true } - cvals, err := CoalesceValues(c, v) + cvals, err := util.CoalesceValues(c, v) if err != nil { return err } @@ -232,6 +234,8 @@ func pathToMap(path string, data map[string]interface{}) map[string]interface{} return set(parsePath(path), data) } +func parsePath(key string) []string { return strings.Split(key, ".") } + func set(path []string, data map[string]interface{}) map[string]interface{} { if len(path) == 0 { return nil @@ -249,12 +253,12 @@ func processImportValues(c *chart.Chart, merge bool) error { return nil } // combine chart values and empty config to get Values - var cvals Values + var cvals common.Values var err error if merge { - cvals, err = MergeValues(c, nil) + cvals, err = util.MergeValues(c, nil) } else { - cvals, err = CoalesceValues(c, nil) + cvals, err = util.CoalesceValues(c, nil) } if err != nil { return err @@ -282,9 +286,9 @@ func processImportValues(c *chart.Chart, merge bool) error { } // create value map from child to be merged into parent if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) } case string: child := "exports." + iv @@ -298,9 +302,9 @@ func processImportValues(c *chart.Chart, merge bool) error { continue } if merge { - b = MergeTables(b, vm.AsMap()) + b = util.MergeTables(b, vm.AsMap()) } else { - b = CoalesceTables(b, vm.AsMap()) + b = util.CoalesceTables(b, vm.AsMap()) } } } @@ -315,14 +319,14 @@ func processImportValues(c *chart.Chart, merge bool) error { // deep copying the cvals as there are cases where pointers can end // up in the cvals when they are copied onto b in ways that break things. cvals = deepCopyMap(cvals) - c.Values = MergeTables(cvals, b) + c.Values = util.MergeTables(cvals, b) } else { // Trimming the nil values from cvals is needed for backwards compatibility. // Previously, the b value had been populated with cvals along with some // overrides. This caused the coalescing functionality to remove the // nil/null values. This trimming is for backwards compat. cvals = trimNilValues(cvals) - c.Values = CoalesceTables(cvals, b) + c.Values = util.CoalesceTables(cvals, b) } return nil @@ -355,6 +359,12 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} { return valsCopyMap } +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + // processDependencyImportValues imports specified chart values from child to parent. func processDependencyImportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go index 55839fe65..3c5bb96f7 100644 --- a/internal/chart/v3/util/dependencies_test.go +++ b/internal/chart/v3/util/dependencies_test.go @@ -23,6 +23,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) func loadChart(t *testing.T, path string) *chart.Chart { @@ -221,7 +222,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { @@ -251,7 +252,7 @@ func TestProcessDependencyImportValues(t *testing.T) { t.Error("expect nil value not found but found it") } switch xerr := err.(type) { - case ErrNoValue: + case common.ErrNoValue: // We found what we expected default: t.Errorf("expected an ErrNoValue but got %q instead", xerr) @@ -261,7 +262,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc = Values(c.Values) + cc = common.Values(c.Values) val, err := cc.PathValue("ensurenull") if err != nil { t.Error("expect value but ensurenull was not found") @@ -291,7 +292,7 @@ func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T e["foo.grandchild.defaults.defaultValue"] = "42" e["bar.grandchild.defaults.defaultValue"] = "42" - cValues := Values(c.Values) + cValues := common.Values(c.Values) for kk, vv := range e { pv, err := cValues.PathValue(kk) if err != nil { @@ -329,7 +330,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { diff --git a/internal/chart/v3/util/errors.go b/internal/chart/v3/util/errors.go deleted file mode 100644 index a175b9758..000000000 --- a/internal/chart/v3/util/errors.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "fmt" -) - -// ErrNoTable indicates that a chart does not have a matching table. -type ErrNoTable struct { - Key string -} - -func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) } - -// ErrNoValue indicates that Values does not contain a key with a value -type ErrNoValue struct { - Key string -} - -func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } - -type ErrInvalidChartName struct { - Name string -} - -func (e ErrInvalidChartName) Error() string { - return fmt.Sprintf("%q is not a valid chart name", e.Name) -} diff --git a/internal/chart/v3/util/jsonschema.go b/internal/chart/v3/util/jsonschema.go deleted file mode 100644 index 9fe35904e..000000000 --- a/internal/chart/v3/util/jsonschema.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "bytes" - "errors" - "fmt" - "log/slog" - "strings" - - "github.com/santhosh-tekuri/jsonschema/v6" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// ValidateAgainstSchema checks that values does not violate the structure laid out in schema -func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { - var sb strings.Builder - if chrt.Schema != nil { - slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema) - if err != nil { - sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) - sb.WriteString(err.Error()) - } - } - slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies())) - // For each dependency, recursively call this function with the coalesced values - for _, subchart := range chrt.Dependencies() { - subchartValues := values[subchart.Name()].(map[string]interface{}) - if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { - sb.WriteString(err.Error()) - } - } - - if sb.Len() > 0 { - return errors.New(sb.String()) - } - - return nil -} - -// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { - defer func() { - if r := recover(); r != nil { - reterr = fmt.Errorf("unable to validate schema: %s", r) - } - }() - - // This unmarshal function leverages UseNumber() for number precision. The parser - // used for values does this as well. - schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON)) - if err != nil { - return err - } - slog.Debug("unmarshalled JSON schema", "schema", schemaJSON) - - compiler := jsonschema.NewCompiler() - err = compiler.AddResource("file:///values.schema.json", schema) - if err != nil { - return err - } - - validator, err := compiler.Compile("file:///values.schema.json") - if err != nil { - return err - } - - err = validator.Validate(values.AsMap()) - if err != nil { - return JSONSchemaValidationError{err} - } - - return nil -} - -// Note, JSONSchemaValidationError is used to wrap the error from the underlying -// validation package so that Helm has a clean interface and the validation package -// could be replaced without changing the Helm SDK API. - -// JSONSchemaValidationError is the error returned when there is a schema validation -// error. -type JSONSchemaValidationError struct { - embeddedErr error -} - -// Error prints the error message -func (e JSONSchemaValidationError) Error() string { - errStr := e.embeddedErr.Error() - - // This string prefixes all of our error details. Further up the stack of helm error message - // building more detail is provided to users. This is removed. - errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n") - - // The extra new line is needed for when there are sub-charts. - return errStr + "\n" -} diff --git a/internal/chart/v3/util/jsonschema_test.go b/internal/chart/v3/util/jsonschema_test.go deleted file mode 100644 index 0a3820377..000000000 --- a/internal/chart/v3/util/jsonschema_test.go +++ /dev/null @@ -1,247 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "os" - "testing" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func TestValidateAgainstSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - if err := ValidateAgainstSingleSchema(values, schema); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstInvalidSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#' -- at '': got number, want boolean or object` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -func TestValidateAgainstSingleSchemaNegative(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values-negative.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading JSON file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `- at '': missing property 'employmentInfo' -- at '/age': minimum: got -5, want 0 -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -const subchartSchema = `{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Values", - "type": "object", - "properties": { - "age": { - "description": "Age", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "age" - ] -} -` - -const subchartSchema2020 = `{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Values", - "type": "object", - "properties": { - "data": { - "type": "array", - "contains": { "type": "string" }, - "unevaluatedItems": { "type": "number" } - } - }, - "required": ["data"] -} -` - -func TestValidateAgainstSchema(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "age": 25, - }, - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstSchemaNegative(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{}, - } - - var errString string - if err := ValidateAgainstSchema(chrt, vals); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `subchart: -- at '': missing property 'age' -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -func TestValidateAgainstSchema2020(t *testing.T) { - subchartJSON := []byte(subchartSchema2020) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "data": []any{"hello", 12}, - }, - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstSchema2020Negative(t *testing.T) { - subchartJSON := []byte(subchartSchema2020) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "data": []any{12}, - }, - } - - var errString string - if err := ValidateAgainstSchema(chrt, vals); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `subchart: -- at '/data': no items match contains schema - - at '/data/0': got number, want string -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index 3125cc3c9..49d93bf40 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/pkg/chart/common" ) var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") @@ -76,7 +77,7 @@ func SaveDir(c *chart.Chart, dest string) error { } // Save templates and files - for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, o := range [][]*common.File{c.Templates, c.Files} { for _, f := range o { n := filepath.Join(outdir, f.Name) if err := writeFile(n, f.Data); err != nil { @@ -246,7 +247,7 @@ func validateName(name string) error { nname := filepath.Base(name) if nname != name { - return ErrInvalidChartName{name} + return common.ErrInvalidChartName{Name: name} } return nil diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 852675bb0..9b1b14a4c 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -31,6 +31,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/common" ) func TestSave(t *testing.T) { @@ -47,7 +48,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -113,7 +114,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, } @@ -153,7 +154,7 @@ func TestSavePreservesTimestamps(t *testing.T) { "imageName": "testimage", "imageId": 42, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -219,10 +220,10 @@ func TestSaveDir(t *testing.T) { Name: "ahab", Version: "1.2.3", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } diff --git a/internal/chart/v3/util/values.go b/internal/chart/v3/util/values.go deleted file mode 100644 index 8e1a14b45..000000000 --- a/internal/chart/v3/util/values.go +++ /dev/null @@ -1,220 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "sigs.k8s.io/yaml" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -// GlobalKey is the name of the Values key that is used for storing global vars. -const GlobalKey = "global" - -// Values represents a collection of chart values. -type Values map[string]interface{} - -// YAML encodes the Values into a YAML string. -func (v Values) YAML() (string, error) { - b, err := yaml.Marshal(v) - return string(b), err -} - -// Table gets a table (YAML subsection) from a Values object. -// -// The table is returned as a Values. -// -// Compound table names may be specified with dots: -// -// foo.bar -// -// The above will be evaluated as "The table bar inside the table -// foo". -// -// An ErrNoTable is returned if the table does not exist. -func (v Values) Table(name string) (Values, error) { - table := v - var err error - - for _, n := range parsePath(name) { - if table, err = tableLookup(table, n); err != nil { - break - } - } - return table, err -} - -// AsMap is a utility function for converting Values to a map[string]interface{}. -// -// It protects against nil map panics. -func (v Values) AsMap() map[string]interface{} { - if len(v) == 0 { - return map[string]interface{}{} - } - return v -} - -// Encode writes serialized Values information to the given io.Writer. -func (v Values) Encode(w io.Writer) error { - out, err := yaml.Marshal(v) - if err != nil { - return err - } - _, err = w.Write(out) - return err -} - -func tableLookup(v Values, simple string) (Values, error) { - v2, ok := v[simple] - if !ok { - return v, ErrNoTable{simple} - } - if vv, ok := v2.(map[string]interface{}); ok { - return vv, nil - } - - // This catches a case where a value is of type Values, but doesn't (for some - // reason) match the map[string]interface{}. This has been observed in the - // wild, and might be a result of a nil map of type Values. - if vv, ok := v2.(Values); ok { - return vv, nil - } - - return Values{}, ErrNoTable{simple} -} - -// ReadValues will parse YAML byte data into a Values. -func ReadValues(data []byte) (vals Values, err error) { - err = yaml.Unmarshal(data, &vals) - if len(vals) == 0 { - vals = Values{} - } - return vals, err -} - -// ReadValuesFile will parse a YAML file into a map of values. -func ReadValuesFile(filename string) (Values, error) { - data, err := os.ReadFile(filename) - if err != nil { - return map[string]interface{}{}, err - } - return ReadValues(data) -} - -// ReleaseOptions represents the additional release options needed -// for the composition of the final values struct -type ReleaseOptions struct { - Name string - Namespace string - Revision int - IsUpgrade bool - IsInstall bool -} - -// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { - return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) -} - -// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) { - if caps == nil { - caps = DefaultCapabilities - } - top := map[string]interface{}{ - "Chart": chrt.Metadata, - "Capabilities": caps, - "Release": map[string]interface{}{ - "Name": options.Name, - "Namespace": options.Namespace, - "IsUpgrade": options.IsUpgrade, - "IsInstall": options.IsInstall, - "Revision": options.Revision, - "Service": "Helm", - }, - } - - vals, err := CoalesceValues(chrt, chrtVals) - if err != nil { - return top, err - } - - if !skipSchemaValidation { - if err := ValidateAgainstSchema(chrt, vals); err != nil { - return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) - } - } - - top["Values"] = vals - return top, nil -} - -// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. -func istable(v interface{}) bool { - _, ok := v.(map[string]interface{}) - return ok -} - -// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. -// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. -// Given the following YAML data the value at path "chapter.one.title" is "Loomings". -// -// chapter: -// one: -// title: "Loomings" -func (v Values) PathValue(path string) (interface{}, error) { - if path == "" { - return nil, errors.New("YAML path cannot be empty") - } - return v.pathValue(parsePath(path)) -} - -func (v Values) pathValue(path []string) (interface{}, error) { - if len(path) == 1 { - // if exists must be root key not table - if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { - return v[path[0]], nil - } - return nil, ErrNoValue{path[0]} - } - - key, path := path[len(path)-1], path[:len(path)-1] - // get our table for table path - t, err := v.Table(joinPath(path...)) - if err != nil { - return nil, ErrNoValue{key} - } - // check table for key and ensure value is not a table - if k, ok := t[key]; ok && !istable(k) { - return k, nil - } - return nil, ErrNoValue{key} -} - -func parsePath(key string) []string { return strings.Split(key, ".") } - -func joinPath(path ...string) string { return strings.Join(path, ".") } diff --git a/internal/chart/v3/util/values_test.go b/internal/chart/v3/util/values_test.go deleted file mode 100644 index 34c664581..000000000 --- a/internal/chart/v3/util/values_test.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "bytes" - "fmt" - "testing" - "text/template" - - chart "helm.sh/helm/v4/internal/chart/v3" -) - -func TestReadValues(t *testing.T) { - doc := `# Test YAML parse -poet: "Coleridge" -title: "Rime of the Ancient Mariner" -stanza: - - "at" - - "length" - - "did" - - cross - - an - - Albatross - -mariner: - with: "crossbow" - shot: "ALBATROSS" - -water: - water: - where: "everywhere" - nor: "any drop to drink" -` - - data, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Error parsing bytes: %s", err) - } - matchValues(t, data) - - tests := []string{`poet: "Coleridge"`, "# Just a comment", ""} - - for _, tt := range tests { - data, err = ReadValues([]byte(tt)) - if err != nil { - t.Fatalf("Error parsing bytes (%s): %s", tt, err) - } - if data == nil { - t.Errorf(`YAML string "%s" gave a nil map`, tt) - } - } -} - -func TestToRenderValues(t *testing.T) { - - chartValues := map[string]interface{}{ - "name": "al Rashid", - "where": map[string]interface{}{ - "city": "Basrah", - "title": "caliph", - }, - } - - overrideValues := map[string]interface{}{ - "name": "Haroun", - "where": map[string]interface{}{ - "city": "Baghdad", - "date": "809 CE", - }, - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.File{}, - Values: chartValues, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - c.AddDependency(&chart.Chart{ - Metadata: &chart.Metadata{Name: "where"}, - }) - - o := ReleaseOptions{ - Name: "Seven Voyages", - Namespace: "default", - Revision: 1, - IsInstall: true, - } - - res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) - if err != nil { - t.Fatal(err) - } - - // Ensure that the top-level values are all set. - if name := res["Chart"].(*chart.Metadata).Name; name != "test" { - t.Errorf("Expected chart name 'test', got %q", name) - } - relmap := res["Release"].(map[string]interface{}) - if name := relmap["Name"]; name.(string) != "Seven Voyages" { - t.Errorf("Expected release name 'Seven Voyages', got %q", name) - } - if namespace := relmap["Namespace"]; namespace.(string) != "default" { - t.Errorf("Expected namespace 'default', got %q", namespace) - } - if revision := relmap["Revision"]; revision.(int) != 1 { - t.Errorf("Expected revision '1', got %d", revision) - } - if relmap["IsUpgrade"].(bool) { - t.Error("Expected upgrade to be false.") - } - if !relmap["IsInstall"].(bool) { - t.Errorf("Expected install to be true.") - } - if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { - t.Error("Expected Capabilities to have v1 as an API") - } - if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { - t.Error("Expected Capabilities to have a Kube version") - } - - vals := res["Values"].(Values) - if vals["name"] != "Haroun" { - t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) - } - where := vals["where"].(map[string]interface{}) - expects := map[string]string{ - "city": "Baghdad", - "date": "809 CE", - "title": "caliph", - } - for field, expect := range expects { - if got := where[field]; got != expect { - t.Errorf("Expected %q, got %q (%v)", expect, got, where) - } - } -} - -func TestReadValuesFile(t *testing.T) { - data, err := ReadValuesFile("./testdata/coleridge.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - matchValues(t, data) -} - -func ExampleValues() { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - panic(err) - } - ch1, err := d.Table("chapter.one") - if err != nil { - panic("could not find chapter one") - } - fmt.Print(ch1["title"]) - // Output: - // Loomings -} - -func TestTable(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if _, err := d.Table("title"); err == nil { - t.Fatalf("Title is not a table.") - } - - if _, err := d.Table("chapter"); err != nil { - t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) - } - - if v, err := d.Table("chapter.one"); err != nil { - t.Errorf("Failed to get chapter.one: %s", err) - } else if v["title"] != "Loomings" { - t.Errorf("Unexpected title: %s", v["title"]) - } - - if _, err := d.Table("chapter.three"); err != nil { - t.Errorf("Chapter three is missing: %s\n%v", err, d) - } - - if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { - t.Errorf("I think you mean 'Epilogue'") - } -} - -func matchValues(t *testing.T, data map[string]interface{}) { - t.Helper() - if data["poet"] != "Coleridge" { - t.Errorf("Unexpected poet: %s", data["poet"]) - } - - if o, err := ttpl("{{len .stanza}}", data); err != nil { - t.Errorf("len stanza: %s", err) - } else if o != "6" { - t.Errorf("Expected 6, got %s", o) - } - - if o, err := ttpl("{{.mariner.shot}}", data); err != nil { - t.Errorf(".mariner.shot: %s", err) - } else if o != "ALBATROSS" { - t.Errorf("Expected that mariner shot ALBATROSS") - } - - if o, err := ttpl("{{.water.water.where}}", data); err != nil { - t.Errorf(".water.water.where: %s", err) - } else if o != "everywhere" { - t.Errorf("Expected water water everywhere") - } -} - -func ttpl(tpl string, v map[string]interface{}) (string, error) { - var b bytes.Buffer - tt := template.Must(template.New("t").Parse(tpl)) - err := tt.Execute(&b, v) - return b.String(), err -} - -func TestPathValue(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if v, err := d.PathValue("chapter.one.title"); err != nil { - t.Errorf("Got error instead of title: %s\n%v", err, d) - } else if v != "Loomings" { - t.Errorf("No error but got wrong value for title: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil { - t.Errorf("Non-existent key should return error: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil { - t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d) - } - if _, err := d.PathValue(""); err == nil { - t.Error("Asking for the value from an empty path should yield an error") - } - if v, err := d.PathValue("title"); err == nil { - if v != "Moby Dick" { - t.Errorf("Failed to return values for root key title") - } - } -} diff --git a/pkg/action/action.go b/pkg/action/action.go index 522226a1a..bcf6ca8ef 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/kio" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/engine" @@ -84,7 +85,7 @@ type Configuration struct { RegistryClient *registry.Client // Capabilities describes the capabilities of the Kubernetes cluster. - Capabilities *chartutil.Capabilities + Capabilities *common.Capabilities // CustomTemplateFuncs is defined by users to provide custom template funcs CustomTemplateFuncs template.FuncMap @@ -176,7 +177,7 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -337,7 +338,7 @@ type RESTClientGetter interface { } // capabilities builds a Capabilities from discovery information. -func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { +func (cfg *Configuration) getCapabilities() (*common.Capabilities, error) { if cfg.Capabilities != nil { return cfg.Capabilities, nil } @@ -366,14 +367,14 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { } } - cfg.Capabilities = &chartutil.Capabilities{ + cfg.Capabilities = &common.Capabilities{ APIVersions: apiVersions, - KubeVersion: chartutil.KubeVersion{ + KubeVersion: common.KubeVersion{ Version: kubeVersion.GitVersion, Major: kubeVersion.Major, Minor: kubeVersion.Minor, }, - HelmVersion: chartutil.DefaultCapabilities.HelmVersion, + HelmVersion: common.DefaultCapabilities.HelmVersion, } return cfg.Capabilities, nil } @@ -409,10 +410,10 @@ func (cfg *Configuration) releaseContent(name string, version int) (*release.Rel } // GetVersionSet retrieves a set of available k8s API versions -func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { +func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet, error) { groups, resources, err := client.ServerGroupsAndResources() if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { - return chartutil.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) + return common.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) } // FIXME: The Kubernetes test fixture for cli appears to always return nil @@ -420,7 +421,7 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version // return the default API list. This is also a safe value to return in any // other odd-ball case. if len(groups) == 0 && len(resources) == 0 { - return chartutil.DefaultVersionSet, nil + return common.DefaultVersionSet, nil } versionMap := make(map[string]interface{}) @@ -453,7 +454,7 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version versions = append(versions, k) } - return chartutil.VersionSet(versions), nil + return common.VersionSet(versions), nil } // recordRelease with an update operation in case reuse has been set. diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 7a510ace6..b65e40024 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -30,8 +30,8 @@ import ( fakeclientset "k8s.io/client-go/kubernetes/fake" "helm.sh/helm/v4/internal/logging" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/registry" @@ -64,7 +64,7 @@ func actionConfigFixtureWithDummyResources(t *testing.T, dummyResources kube.Res return &Configuration{ Releases: storage.Init(driver.NewMemory()), KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: dummyResources}, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, RegistryClient: registryClient, } } @@ -122,14 +122,14 @@ type chartOptions struct { type chartOption func(*chartOptions) func buildChart(opts ...chartOption) *chart.Chart { - defaultTemplates := []*chart.File{ + defaultTemplates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifestWithHook)}, } return buildChartWithTemplates(defaultTemplates, opts...) } -func buildChartWithTemplates(templates []*chart.File, opts ...chartOption) *chart.Chart { +func buildChartWithTemplates(templates []*common.File, opts ...chartOption) *chart.Chart { c := &chartOptions{ Chart: &chart.Chart{ // TODO: This should be more complete. @@ -179,7 +179,7 @@ func withValues(values map[string]interface{}) chartOption { func withNotes(notes string) chartOption { return func(opts *chartOptions) { - opts.Templates = append(opts.Templates, &chart.File{ + opts.Templates = append(opts.Templates, &common.File{ Name: "templates/NOTES.txt", Data: []byte(notes), }) @@ -200,7 +200,7 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { func withSampleTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ // This adds basic templates and partials. {Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/empty", Data: []byte("")}, @@ -213,14 +213,14 @@ func withSampleTemplates() chartOption { func withSampleSecret() chartOption { return func(opts *chartOptions) { - sampleSecret := &chart.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} + sampleSecret := &common.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} opts.Templates = append(opts.Templates, sampleSecret) } } func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ // This adds basic templates and partials. {Name: "templates/goodbye", Data: []byte("goodbye: world")}, {Name: "templates/empty", Data: []byte("")}, @@ -234,7 +234,7 @@ func withSampleIncludingIncorrectTemplates() chartOption { func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + sampleTemplates := []*common.File{ {Name: "templates/rbac", Data: []byte(rbacManifests)}, } opts.Templates = append(opts.Templates, sampleTemplates...) @@ -851,7 +851,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { Name: "test-chart", Version: "0.1.0", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/invalid", Data: []byte("invalid: yaml: content:")}, }, } diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go index 18b8b4838..a0b5d92c1 100644 --- a/pkg/action/get_values.go +++ b/pkg/action/get_values.go @@ -16,9 +16,7 @@ limitations under the License. package action -import ( - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" -) +import "helm.sh/helm/v4/pkg/chart/common/util" // GetValues is the action for checking a given release's values. // @@ -50,7 +48,7 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) { // If the user wants all values, compute the values and return. if g.AllValues { - cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) + cfg, err := util.CoalesceValues(rel.Chart, rel.Config) if err != nil { return nil, err } diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index e3a2c0808..091155bc2 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -29,8 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/resource" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -178,7 +177,7 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - templates := []*chart.File{ + templates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifest)}, } @@ -205,7 +204,7 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} - templates := []*chart.File{ + templates := []*common.File{ {Name: "templates/hello", Data: []byte("hello: world")}, {Name: "templates/hooks", Data: []byte(manifest)}, } @@ -382,7 +381,7 @@ data: configuration := &Configuration{ Releases: storage.Init(driver.NewMemory()), KubeClient: kubeClient, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, } serverSideApply := true diff --git a/pkg/action/install.go b/pkg/action/install.go index b2330d551..0fe3ebc4b 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -41,6 +41,8 @@ import ( "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/cli" @@ -113,8 +115,8 @@ type Install struct { // KubeVersion allows specifying a custom kubernetes version to use and // APIVersions allows a manual set of supported API Versions to be passed // (for things like templating). These are ignored if ClientOnly is false - KubeVersion *chartutil.KubeVersion - APIVersions chartutil.VersionSet + KubeVersion *common.KubeVersion + APIVersions common.VersionSet // Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false IsUpgrade bool // Enable DNS lookups when rendering templates @@ -292,7 +294,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma if i.ClientOnly { // Add mock objects in here so it doesn't use Kube API server // NOTE(bacongobbler): used for `helm template` - i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy() + i.cfg.Capabilities = common.DefaultCapabilities.Copy() if i.KubeVersion != nil { i.cfg.Capabilities.KubeVersion = *i.KubeVersion } @@ -319,14 +321,14 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma // special case for helm template --is-upgrade isUpgrade := i.IsUpgrade && i.isDryRun() - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: i.ReleaseName, Namespace: i.Namespace, Revision: 1, IsInstall: !isUpgrade, IsUpgrade: isUpgrade, } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chrt, vals, options, caps, i.SkipSchemaValidation) if err != nil { return nil, err } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index f567b3df4..92bb64b4d 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -45,8 +45,7 @@ import ( "k8s.io/client-go/rest/fake" "helm.sh/helm/v4/internal/test" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -258,7 +257,7 @@ func TestInstallReleaseClientOnly(t *testing.T) { instAction.ClientOnly = true instAction.Run(buildChart(), nil) // disregard output - is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities) + is.Equal(instAction.cfg.Capabilities, common.DefaultCapabilities) is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard}) } @@ -429,7 +428,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { vals := map[string]interface{}{} mockChart := buildChart(withSampleTemplates()) - mockChart.Templates = append(mockChart.Templates, &chart.File{ + mockChart.Templates = append(mockChart.Templates, &common.File{ Name: "templates/lookup", Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), }) diff --git a/pkg/action/lint.go b/pkg/action/lint.go index 7b3c00ad2..208fd4637 100644 --- a/pkg/action/lint.go +++ b/pkg/action/lint.go @@ -22,9 +22,10 @@ import ( "path/filepath" "strings" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint" - "helm.sh/helm/v4/pkg/lint/support" ) // Lint is the action for checking that the semantics of a chart are well-formed. @@ -36,7 +37,7 @@ type Lint struct { WithSubcharts bool Quiet bool SkipSchemaValidation bool - KubeVersion *chartutil.KubeVersion + KubeVersion *common.KubeVersion } // LintResult is the result of Lint @@ -86,7 +87,7 @@ func HasWarningsOrErrors(result *LintResult) bool { return len(result.Errors) > 0 } -func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) (support.Linter, error) { +func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) (support.Linter, error) { var chartPath string linter := support.Linter{} diff --git a/pkg/action/show.go b/pkg/action/show.go index 6d6e10d24..4195d69a5 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -24,6 +24,7 @@ import ( "k8s.io/cli-runtime/pkg/printers" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -140,7 +141,7 @@ func (s *Show) Run(chartpath string) (string, error) { return out.String(), nil } -func findReadme(files []*chart.File) (file *chart.File) { +func findReadme(files []*common.File) (file *common.File) { for _, file := range files { for _, n := range readmeFileNames { if file == nil { diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index 67eba2338..faf306f2a 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -19,6 +19,7 @@ package action import ( "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -27,14 +28,14 @@ func TestShow(t *testing.T) { client := NewShow(ShowAll, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "README.md", Data: []byte("README\n")}, {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, {Name: "crds/baz.yaml", Data: []byte("baz\n")}, }, - Raw: []*chart.File{ + Raw: []*common.File{ {Name: "values.yaml", Data: []byte("VALUES\n")}, }, Values: map[string]interface{}{}, @@ -105,7 +106,7 @@ func TestShowCRDs(t *testing.T) { client := NewShow(ShowCRDs, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, @@ -138,7 +139,7 @@ func TestShowNoReadme(t *testing.T) { client := NewShow(ShowAll, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ + Files: []*common.File{ {Name: "crds/ignoreme.txt", Data: []byte("error")}, {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index c00a59079..3688adf0e 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -28,6 +28,8 @@ import ( "k8s.io/cli-runtime/pkg/resource" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" @@ -260,7 +262,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin // the release object. revision := lastRelease.Version + 1 - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: name, Namespace: currentRelease.Namespace, Revision: revision, @@ -271,7 +273,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin if err != nil { return nil, nil, false, err } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) if err != nil { return nil, nil, false, err } @@ -588,12 +590,12 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV slog.Debug("reusing the old release's values") // We have to regenerate the old coalesced values: - oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) + oldVals, err := util.CoalesceValues(current.Chart, current.Config) if err != nil { return nil, fmt.Errorf("failed to rebuild old values: %w", err) } - newVals = chartutil.CoalesceTables(newVals, current.Config) + newVals = util.CoalesceTables(newVals, current.Config) chart.Values = oldVals @@ -604,7 +606,7 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV if u.ResetThenReuseValues { slog.Debug("merging values from old release to new values") - newVals = chartutil.CoalesceTables(newVals, current.Config) + newVals = util.CoalesceTables(newVals, current.Config) return newVals, nil } diff --git a/pkg/chart/common.go b/pkg/chart/common.go new file mode 100644 index 000000000..8b1dd58c3 --- /dev/null +++ b/pkg/chart/common.go @@ -0,0 +1,219 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chart + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strings" + + v3chart "helm.sh/helm/v4/internal/chart/v3" + common "helm.sh/helm/v4/pkg/chart/common" + v2chart "helm.sh/helm/v4/pkg/chart/v2" +) + +var NewAccessor func(chrt Charter) (Accessor, error) = NewDefaultAccessor //nolint:revive + +func NewDefaultAccessor(chrt Charter) (Accessor, error) { + switch v := chrt.(type) { + case v2chart.Chart: + return &v2Accessor{&v}, nil + case *v2chart.Chart: + return &v2Accessor{v}, nil + case v3chart.Chart: + return &v3Accessor{&v}, nil + case *v3chart.Chart: + return &v3Accessor{v}, nil + default: + return nil, errors.New("unsupported chart type") + } +} + +type v2Accessor struct { + chrt *v2chart.Chart +} + +func (r *v2Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v2Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v2Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v2Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v2Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v2Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v2Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v2Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v2Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v2Accessor) Schema() []byte { + return r.chrt.Schema +} + +type v3Accessor struct { + chrt *v3chart.Chart +} + +func (r *v3Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v3Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v3Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v3Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v3Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v3Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v3Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v3Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v3Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v3Accessor) Schema() []byte { + return r.chrt.Schema +} + +func structToMap(obj interface{}) (map[string]interface{}, error) { + objValue := reflect.ValueOf(obj) + + // If the value is a pointer, dereference it + if objValue.Kind() == reflect.Ptr { + objValue = objValue.Elem() + } + + // Check if the input is a struct + if objValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("input must be a struct or a pointer to a struct") + } + + result := make(map[string]interface{}) + objType := objValue.Type() + + for i := 0; i < objValue.NumField(); i++ { + field := objType.Field(i) + value := objValue.Field(i) + + switch value.Kind() { + case reflect.Struct: + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + case reflect.Ptr: + // Recurse for pointers by dereferencing + if value.IsNil() { + result[field.Name] = nil + } else { + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + } + case reflect.Slice: + sliceOfMaps := make([]interface{}, value.Len()) + for j := 0; j < value.Len(); j++ { + sliceElement := value.Index(j) + if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Ptr { + nestedMap, err := structToMap(sliceElement.Interface()) + if err != nil { + return nil, err + } + sliceOfMaps[j] = nestedMap + } else { + sliceOfMaps[j] = sliceElement.Interface() + } + } + result[field.Name] = sliceOfMaps + default: + result[field.Name] = value.Interface() + } + } + return result, nil +} diff --git a/pkg/chart/v2/util/capabilities.go b/pkg/chart/common/capabilities.go similarity index 99% rename from pkg/chart/v2/util/capabilities.go rename to pkg/chart/common/capabilities.go index 19d62c5e3..355c3978a 100644 --- a/pkg/chart/v2/util/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "fmt" diff --git a/pkg/chart/v2/util/capabilities_test.go b/pkg/chart/common/capabilities_test.go similarity index 99% rename from pkg/chart/v2/util/capabilities_test.go rename to pkg/chart/common/capabilities_test.go index e5513b3fd..bf32b1f3f 100644 --- a/pkg/chart/v2/util/capabilities_test.go +++ b/pkg/chart/common/capabilities_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "testing" diff --git a/pkg/chart/v2/util/errors.go b/pkg/chart/common/errors.go similarity index 98% rename from pkg/chart/v2/util/errors.go rename to pkg/chart/common/errors.go index a175b9758..b0a2d650e 100644 --- a/pkg/chart/v2/util/errors.go +++ b/pkg/chart/common/errors.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "fmt" diff --git a/pkg/chart/v2/util/errors_test.go b/pkg/chart/common/errors_test.go similarity index 98% rename from pkg/chart/v2/util/errors_test.go rename to pkg/chart/common/errors_test.go index b8ae86384..06b3b054c 100644 --- a/pkg/chart/v2/util/errors_test.go +++ b/pkg/chart/common/errors_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "testing" diff --git a/internal/chart/v3/file.go b/pkg/chart/common/file.go similarity index 98% rename from internal/chart/v3/file.go rename to pkg/chart/common/file.go index ba04e106d..304643f1a 100644 --- a/internal/chart/v3/file.go +++ b/pkg/chart/common/file.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v3 +package common // File represents a file as a name/value pair. // diff --git a/pkg/chart/v2/util/testdata/coleridge.yaml b/pkg/chart/common/testdata/coleridge.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/coleridge.yaml rename to pkg/chart/common/testdata/coleridge.yaml diff --git a/pkg/chart/v2/util/coalesce.go b/pkg/chart/common/util/coalesce.go similarity index 81% rename from pkg/chart/v2/util/coalesce.go rename to pkg/chart/common/util/coalesce.go index a3e0f5ae8..5bfa1c608 100644 --- a/pkg/chart/v2/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -23,7 +23,8 @@ import ( "github.com/mitchellh/copystructure" - chart "helm.sh/helm/v4/pkg/chart/v2" + chart "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) func concatPrefix(a, b string) string { @@ -42,7 +43,7 @@ func concatPrefix(a, b string) string { // - Scalar values and arrays are replaced, maps are merged // - A chart has access to all of the variables for it, as well as all of // the values destined for its dependencies. -func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { +func CoalesceValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { valsCopy, err := copyValues(vals) if err != nil { return vals, err @@ -64,7 +65,7 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err // Retaining Nils is useful when processes early in a Helm action or business // logic need to retain them for when Coalescing will happen again later in the // business logic. -func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { +func MergeValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { valsCopy, err := copyValues(vals) if err != nil { return vals, err @@ -72,7 +73,7 @@ func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) return coalesce(log.Printf, chrt, valsCopy, "", true) } -func copyValues(vals map[string]interface{}) (Values, error) { +func copyValues(vals map[string]interface{}) (common.Values, error) { v, err := copystructure.Copy(vals) if err != nil { return vals, err @@ -96,28 +97,36 @@ type printFn func(format string, v ...interface{}) // Note, the merge argument specifies whether this is being used by MergeValues // or CoalesceValues. Coalescing removes null values and their keys in some // situations while merging keeps the null values. -func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { +func coalesce(printf printFn, ch chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { coalesceValues(printf, ch, dest, prefix, merge) return coalesceDeps(printf, ch, dest, prefix, merge) } // coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies() { - if c, ok := dest[subchart.Name()]; !ok { +func coalesceDeps(printf printFn, chrt chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return dest, err + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return dest, err + } + if c, ok := dest[sub.Name()]; !ok { // If dest doesn't already have the key, create it. - dest[subchart.Name()] = make(map[string]interface{}) + dest[sub.Name()] = make(map[string]interface{}) } else if !istable(c) { - return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c) + return dest, fmt.Errorf("type mismatch on %s: %t", sub.Name(), c) } - if dv, ok := dest[subchart.Name()]; ok { + if dv, ok := dest[sub.Name()]; ok { dvmap := dv.(map[string]interface{}) - subPrefix := concatPrefix(prefix, chrt.Metadata.Name) + subPrefix := concatPrefix(prefix, ch.Name()) // Get globals out of dest and merge them into dvmap. coalesceGlobals(printf, dvmap, dest, subPrefix, merge) // Now coalesce the rest of the values. var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) + dest[sub.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) if err != nil { return dest, err } @@ -132,17 +141,17 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{} func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { var dg, sg map[string]interface{} - if destglob, ok := dest[GlobalKey]; !ok { + if destglob, ok := dest[common.GlobalKey]; !ok { dg = make(map[string]interface{}) } else if dg, ok = destglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because destination %s is not a table.", GlobalKey) + printf("warning: skipping globals because destination %s is not a table.", common.GlobalKey) return } - if srcglob, ok := src[GlobalKey]; !ok { + if srcglob, ok := src[common.GlobalKey]; !ok { sg = make(map[string]interface{}) } else if sg, ok = srcglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because source %s is not a table.", GlobalKey) + printf("warning: skipping globals because source %s is not a table.", common.GlobalKey) return } @@ -178,7 +187,7 @@ func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix st dg[key] = val } } - dest[GlobalKey] = dg + dest[common.GlobalKey] = dg } func copyMap(src map[string]interface{}) map[string]interface{} { @@ -190,13 +199,18 @@ func copyMap(src map[string]interface{}) map[string]interface{} { // coalesceValues builds up a values map for a particular chart. // // Values in v will override the values in the chart. -func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { - subPrefix := concatPrefix(prefix, c.Metadata.Name) +func coalesceValues(printf printFn, c chart.Charter, v map[string]interface{}, prefix string, merge bool) { + ch, err := chart.NewAccessor(c) + if err != nil { + return + } + + subPrefix := concatPrefix(prefix, ch.Name()) // Using c.Values directly when coalescing a table can cause problems where // the original c.Values is altered. Creating a deep copy stops the problem. // This section is fault-tolerant as there is no ability to return an error. - valuesCopy, err := copystructure.Copy(c.Values) + valuesCopy, err := copystructure.Copy(ch.Values()) var vc map[string]interface{} var ok bool if err != nil { @@ -205,7 +219,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr // wrong with c.Values. In this case we will use c.Values and report // an error. printf("warning: unable to copy values, err: %s", err) - vc = c.Values + vc = ch.Values() } else { vc, ok = valuesCopy.(map[string]interface{}) if !ok { @@ -213,7 +227,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr // it cannot be treated as map[string]interface{} there is something // strangely wrong. Log it and use c.Values printf("warning: unable to convert values copy to values type") - vc = c.Values + vc = ch.Values() } } @@ -250,9 +264,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr } } -func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool { - for _, subchart := range chrt.Dependencies() { - if subchart.Name() == key { +func childChartMergeTrue(chrt chart.Charter, key string, merge bool) bool { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return merge + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return merge + } + if sub.Name() == key { return true } } @@ -306,3 +328,9 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref } return dst } + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} diff --git a/pkg/chart/v2/util/coalesce_test.go b/pkg/chart/common/util/coalesce_test.go similarity index 97% rename from pkg/chart/v2/util/coalesce_test.go rename to pkg/chart/common/util/coalesce_test.go index e2c45a435..871bfa8da 100644 --- a/pkg/chart/v2/util/coalesce_test.go +++ b/pkg/chart/common/util/coalesce_test.go @@ -17,13 +17,16 @@ limitations under the License. package util import ( + "bytes" "encoding/json" "fmt" "maps" "testing" + "text/template" "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -136,7 +139,7 @@ func TestCoalesceValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -144,7 +147,7 @@ func TestCoalesceValues(t *testing.T) { // taking a copy of the values before passing it // to CoalesceValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) + valsCopy := make(common.Values, len(vals)) maps.Copy(valsCopy, vals) v, err := CoalesceValues(c, vals) @@ -238,6 +241,13 @@ func TestCoalesceValues(t *testing.T) { is.Equal(valsCopy, vals) } +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + err := tt.Execute(&b, v) + return b.String(), err +} + func TestMergeValues(t *testing.T) { is := assert.New(t) @@ -294,7 +304,7 @@ func TestMergeValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -302,7 +312,7 @@ func TestMergeValues(t *testing.T) { // taking a copy of the values before passing it // to MergeValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) + valsCopy := make(common.Values, len(vals)) maps.Copy(valsCopy, vals) v, err := MergeValues(c, vals) diff --git a/pkg/chart/v2/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go similarity index 89% rename from pkg/chart/v2/util/jsonschema.go rename to pkg/chart/common/util/jsonschema.go index 72e133363..acd2ca100 100644 --- a/pkg/chart/v2/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -30,7 +30,8 @@ import ( "helm.sh/helm/v4/internal/version" - chart "helm.sh/helm/v4/pkg/chart/v2" + chart "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) // HTTPURLLoader implements a loader for HTTP/HTTPS URLs @@ -71,11 +72,15 @@ func newHTTPURLLoader() *HTTPURLLoader { } // ValidateAgainstSchema checks that values does not violate the structure laid out in schema -func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { +func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) error { + chrt, err := chart.NewAccessor(ch) + if err != nil { + return err + } var sb strings.Builder - if chrt.Schema != nil { + if chrt.Schema() != nil { slog.Debug("chart name", "chart-name", chrt.Name()) - err := ValidateAgainstSingleSchema(values, chrt.Schema) + err := ValidateAgainstSingleSchema(values, chrt.Schema()) if err != nil { sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) sb.WriteString(err.Error()) @@ -84,7 +89,11 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies())) // For each dependency, recursively call this function with the coalesced values for _, subchart := range chrt.Dependencies() { - subchartValues := values[subchart.Name()].(map[string]interface{}) + sub, err := chart.NewAccessor(subchart) + if err != nil { + return err + } + subchartValues := values[sub.Name()].(map[string]interface{}) if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { sb.WriteString(err.Error()) } @@ -98,7 +107,7 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err } // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { +func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { defer func() { if r := recover(); r != nil { reterr = fmt.Errorf("unable to validate schema: %s", r) diff --git a/pkg/chart/v2/util/jsonschema_test.go b/pkg/chart/common/util/jsonschema_test.go similarity index 96% rename from pkg/chart/v2/util/jsonschema_test.go rename to pkg/chart/common/util/jsonschema_test.go index cd95b7faf..b34f9d514 100644 --- a/pkg/chart/v2/util/jsonschema_test.go +++ b/pkg/chart/common/util/jsonschema_test.go @@ -23,11 +23,12 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestValidateAgainstSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") + values, err := common.ReadValuesFile("./testdata/test-values.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -42,7 +43,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) { } func TestValidateAgainstInvalidSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") + values, err := common.ReadValuesFile("./testdata/test-values.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -66,7 +67,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { } func TestValidateAgainstSingleSchemaNegative(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values-negative.yaml") + values, err := common.ReadValuesFile("./testdata/test-values-negative.yaml") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } diff --git a/pkg/chart/v2/util/testdata/test-values-invalid.schema.json b/pkg/chart/common/util/testdata/test-values-invalid.schema.json similarity index 100% rename from pkg/chart/v2/util/testdata/test-values-invalid.schema.json rename to pkg/chart/common/util/testdata/test-values-invalid.schema.json diff --git a/pkg/chart/v2/util/testdata/test-values-negative.yaml b/pkg/chart/common/util/testdata/test-values-negative.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/test-values-negative.yaml rename to pkg/chart/common/util/testdata/test-values-negative.yaml diff --git a/pkg/chart/v2/util/testdata/test-values.schema.json b/pkg/chart/common/util/testdata/test-values.schema.json similarity index 100% rename from pkg/chart/v2/util/testdata/test-values.schema.json rename to pkg/chart/common/util/testdata/test-values.schema.json diff --git a/pkg/chart/v2/util/testdata/test-values.yaml b/pkg/chart/common/util/testdata/test-values.yaml similarity index 100% rename from pkg/chart/v2/util/testdata/test-values.yaml rename to pkg/chart/common/util/testdata/test-values.yaml diff --git a/pkg/chart/common/util/values.go b/pkg/chart/common/util/values.go new file mode 100644 index 000000000..85cb29012 --- /dev/null +++ b/pkg/chart/common/util/values.go @@ -0,0 +1,70 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + + "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" +) + +// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValues(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities) (common.Values, error) { + return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) +} + +// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValuesWithSchemaValidation(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities, skipSchemaValidation bool) (common.Values, error) { + if caps == nil { + caps = common.DefaultCapabilities + } + accessor, err := chart.NewAccessor(chrt) + if err != nil { + return nil, err + } + top := map[string]interface{}{ + "Chart": accessor.MetadataAsMap(), + "Capabilities": caps, + "Release": map[string]interface{}{ + "Name": options.Name, + "Namespace": options.Namespace, + "IsUpgrade": options.IsUpgrade, + "IsInstall": options.IsInstall, + "Revision": options.Revision, + "Service": "Helm", + }, + } + + vals, err := CoalesceValues(chrt, chrtVals) + if err != nil { + return common.Values(top), err + } + + if !skipSchemaValidation { + if err := ValidateAgainstSchema(chrt, vals); err != nil { + return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) + } + } + + top["Values"] = vals + return top, nil +} diff --git a/pkg/chart/common/util/values_test.go b/pkg/chart/common/util/values_test.go new file mode 100644 index 000000000..5fc030567 --- /dev/null +++ b/pkg/chart/common/util/values_test.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "helm.sh/helm/v4/pkg/chart/common" + chart "helm.sh/helm/v4/pkg/chart/v2" +) + +func TestToRenderValues(t *testing.T) { + + chartValues := map[string]interface{}{ + "name": "al Rashid", + "where": map[string]interface{}{ + "city": "Basrah", + "title": "caliph", + }, + } + + overrideValues := map[string]interface{}{ + "name": "Haroun", + "where": map[string]interface{}{ + "city": "Baghdad", + "date": "809 CE", + }, + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + Templates: []*common.File{}, + Values: chartValues, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + }, + } + c.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "where"}, + }) + + o := common.ReleaseOptions{ + Name: "Seven Voyages", + Namespace: "default", + Revision: 1, + IsInstall: true, + } + + res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) + if err != nil { + t.Fatal(err) + } + + // Ensure that the top-level values are all set. + metamap := res["Chart"].(map[string]interface{}) + if name := metamap["Name"]; name.(string) != "test" { + t.Errorf("Expected chart name 'test', got %q", name) + } + relmap := res["Release"].(map[string]interface{}) + if name := relmap["Name"]; name.(string) != "Seven Voyages" { + t.Errorf("Expected release name 'Seven Voyages', got %q", name) + } + if namespace := relmap["Namespace"]; namespace.(string) != "default" { + t.Errorf("Expected namespace 'default', got %q", namespace) + } + if revision := relmap["Revision"]; revision.(int) != 1 { + t.Errorf("Expected revision '1', got %d", revision) + } + if relmap["IsUpgrade"].(bool) { + t.Error("Expected upgrade to be false.") + } + if !relmap["IsInstall"].(bool) { + t.Errorf("Expected install to be true.") + } + if !res["Capabilities"].(*common.Capabilities).APIVersions.Has("v1") { + t.Error("Expected Capabilities to have v1 as an API") + } + if res["Capabilities"].(*common.Capabilities).KubeVersion.Major != "1" { + t.Error("Expected Capabilities to have a Kube version") + } + + vals := res["Values"].(common.Values) + if vals["name"] != "Haroun" { + t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) + } + where := vals["where"].(map[string]interface{}) + expects := map[string]string{ + "city": "Baghdad", + "date": "809 CE", + "title": "caliph", + } + for field, expect := range expects { + if got := where[field]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, where) + } + } +} diff --git a/pkg/chart/v2/util/values.go b/pkg/chart/common/values.go similarity index 74% rename from pkg/chart/v2/util/values.go rename to pkg/chart/common/values.go index 6850e8b9b..94958a779 100644 --- a/pkg/chart/v2/util/values.go +++ b/pkg/chart/common/values.go @@ -14,18 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "errors" - "fmt" "io" "os" "strings" "sigs.k8s.io/yaml" - - chart "helm.sh/helm/v4/pkg/chart/v2" ) // GlobalKey is the name of the Values key that is used for storing global vars. @@ -131,48 +128,6 @@ type ReleaseOptions struct { IsInstall bool } -// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { - return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) -} - -// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) { - if caps == nil { - caps = DefaultCapabilities - } - top := map[string]interface{}{ - "Chart": chrt.Metadata, - "Capabilities": caps, - "Release": map[string]interface{}{ - "Name": options.Name, - "Namespace": options.Namespace, - "IsUpgrade": options.IsUpgrade, - "IsInstall": options.IsInstall, - "Revision": options.Revision, - "Service": "Helm", - }, - } - - vals, err := CoalesceValues(chrt, chrtVals) - if err != nil { - return top, err - } - - if !skipSchemaValidation { - if err := ValidateAgainstSchema(chrt, vals); err != nil { - return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) - } - } - - top["Values"] = vals - return top, nil -} - // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. func istable(v interface{}) bool { _, ok := v.(map[string]interface{}) diff --git a/pkg/chart/v2/util/values_test.go b/pkg/chart/common/values_test.go similarity index 66% rename from pkg/chart/v2/util/values_test.go rename to pkg/chart/common/values_test.go index 1a25fafb8..3cceeb2b5 100644 --- a/pkg/chart/v2/util/values_test.go +++ b/pkg/chart/common/values_test.go @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package common import ( "bytes" "fmt" "testing" "text/template" - - chart "helm.sh/helm/v4/pkg/chart/v2" ) func TestReadValues(t *testing.T) { @@ -66,92 +64,6 @@ water: } } -func TestToRenderValues(t *testing.T) { - - chartValues := map[string]interface{}{ - "name": "al Rashid", - "where": map[string]interface{}{ - "city": "Basrah", - "title": "caliph", - }, - } - - overrideValues := map[string]interface{}{ - "name": "Haroun", - "where": map[string]interface{}{ - "city": "Baghdad", - "date": "809 CE", - }, - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.File{}, - Values: chartValues, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - c.AddDependency(&chart.Chart{ - Metadata: &chart.Metadata{Name: "where"}, - }) - - o := ReleaseOptions{ - Name: "Seven Voyages", - Namespace: "default", - Revision: 1, - IsInstall: true, - } - - res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) - if err != nil { - t.Fatal(err) - } - - // Ensure that the top-level values are all set. - if name := res["Chart"].(*chart.Metadata).Name; name != "test" { - t.Errorf("Expected chart name 'test', got %q", name) - } - relmap := res["Release"].(map[string]interface{}) - if name := relmap["Name"]; name.(string) != "Seven Voyages" { - t.Errorf("Expected release name 'Seven Voyages', got %q", name) - } - if namespace := relmap["Namespace"]; namespace.(string) != "default" { - t.Errorf("Expected namespace 'default', got %q", namespace) - } - if revision := relmap["Revision"]; revision.(int) != 1 { - t.Errorf("Expected revision '1', got %d", revision) - } - if relmap["IsUpgrade"].(bool) { - t.Error("Expected upgrade to be false.") - } - if !relmap["IsInstall"].(bool) { - t.Errorf("Expected install to be true.") - } - if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { - t.Error("Expected Capabilities to have v1 as an API") - } - if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { - t.Error("Expected Capabilities to have a Kube version") - } - - vals := res["Values"].(Values) - if vals["name"] != "Haroun" { - t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) - } - where := vals["where"].(map[string]interface{}) - expects := map[string]string{ - "city": "Baghdad", - "date": "809 CE", - "title": "caliph", - } - for field, expect := range expects { - if got := where[field]; got != expect { - t.Errorf("Expected %q, got %q (%v)", expect, got, where) - } - } -} - func TestReadValuesFile(t *testing.T) { data, err := ReadValuesFile("./testdata/coleridge.yaml") if err != nil { diff --git a/pkg/chart/v2/file.go b/pkg/chart/interfaces.go similarity index 60% rename from pkg/chart/v2/file.go rename to pkg/chart/interfaces.go index a2eeb0fcd..e87dd2c08 100644 --- a/pkg/chart/v2/file.go +++ b/pkg/chart/interfaces.go @@ -13,15 +13,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package chart -// File represents a file as a name/value pair. -// -// By convention, name is a relative path within the scope of the chart's -// base directory. -type File struct { - // Name is the path-like name of the template. - Name string `json:"name"` - // Data is the template as byte data. - Data []byte `json:"data"` +import ( + common "helm.sh/helm/v4/pkg/chart/common" +) + +type Charter interface{} + +type Accessor interface { + Name() string + IsRoot() bool + MetadataAsMap() map[string]interface{} + Files() []*common.File + Templates() []*common.File + ChartFullPath() string + IsLibraryChart() bool + Dependencies() []Charter + Values() map[string]interface{} + Schema() []byte } diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index 66ddf98a5..f59bcd8b3 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -19,6 +19,8 @@ import ( "path/filepath" "regexp" "strings" + + "helm.sh/helm/v4/pkg/chart/common" ) // APIVersionV1 is the API version number for version 1. @@ -37,20 +39,20 @@ type Chart struct { // // This should not be used except in special cases like `helm show values`, // where we want to display the raw values, comments and all. - Raw []*File `json:"-"` + Raw []*common.File `json:"-"` // Metadata is the contents of the Chartfile. Metadata *Metadata `json:"metadata"` // Lock is the contents of Chart.lock. Lock *Lock `json:"lock"` // Templates for this chart. - Templates []*File `json:"templates"` + Templates []*common.File `json:"templates"` // Values are default config for this chart. Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. - Files []*File `json:"files"` + Files []*common.File `json:"files"` parent *Chart dependencies []*Chart @@ -62,7 +64,7 @@ type CRD struct { // Filename is the File obj Name including (sub-)chart.ChartFullPath Filename string // File is the File obj for the crd - File *File + File *common.File } // SetDependencies replaces the chart dependencies. @@ -137,8 +139,8 @@ func (ch *Chart) AppVersion() string { // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // Deprecated: use CRDObjects() -func (ch *Chart) CRDs() []*File { - files := []*File{} +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} // Find all resources in the crds/ directory for _, f := range ch.Files { if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index d6311085b..a96d8c0c0 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -20,11 +20,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/chart/common" ) func TestCRDs(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -57,7 +59,7 @@ func TestCRDs(t *testing.T) { func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ - Raw: []*File{ + Raw: []*common.File{ { Name: "fhqwhgads.yaml", Data: []byte("Everybody to the Limit"), @@ -76,7 +78,7 @@ func TestSaveChartNoRawData(t *testing.T) { t.Fatal(err) } - is.Equal([]*File(nil), res.Raw) + is.Equal([]*common.File(nil), res.Raw) } func TestMetadata(t *testing.T) { @@ -162,7 +164,7 @@ func TestChartFullPath(t *testing.T) { func TestCRDObjects(t *testing.T) { chrt := Chart{ - Files: []*File{ + Files: []*common.File{ { Name: "crds/foo.yaml", Data: []byte("hello"), @@ -190,7 +192,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo.yaml", Data: []byte("hello"), }, @@ -198,7 +200,7 @@ func TestCRDObjects(t *testing.T) { { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", - File: &File{ + File: &common.File{ Name: "crds/foo/bar/baz.yaml", Data: []byte("hello"), }, diff --git a/pkg/lint/lint.go b/pkg/chart/v2/lint/lint.go similarity index 83% rename from pkg/lint/lint.go rename to pkg/chart/v2/lint/lint.go index 64b2a6057..773c9bc5e 100644 --- a/pkg/lint/lint.go +++ b/pkg/chart/v2/lint/lint.go @@ -14,24 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -package lint // import "helm.sh/helm/v4/pkg/lint" +package lint // import "helm.sh/helm/v4/pkg/chart/v2/lint" import ( "path/filepath" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/rules" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint/rules" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) type linterOptions struct { - KubeVersion *chartutil.KubeVersion + KubeVersion *common.KubeVersion SkipSchemaValidation bool } type LinterOption func(lo *linterOptions) -func WithKubeVersion(kubeVersion *chartutil.KubeVersion) LinterOption { +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { return func(lo *linterOptions) { lo.KubeVersion = kubeVersion } diff --git a/pkg/lint/lint_test.go b/pkg/chart/v2/lint/lint_test.go similarity index 99% rename from pkg/lint/lint_test.go rename to pkg/chart/v2/lint/lint_test.go index 5b590c010..3c777e2bb 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/chart/v2/lint/lint_test.go @@ -23,8 +23,8 @@ import ( "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) var values map[string]interface{} diff --git a/pkg/lint/rules/chartfile.go b/pkg/chart/v2/lint/rules/chartfile.go similarity index 98% rename from pkg/lint/rules/chartfile.go rename to pkg/chart/v2/lint/rules/chartfile.go index 103c28374..185f524a4 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/chart/v2/lint/rules/chartfile.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import ( "errors" @@ -27,8 +27,8 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) // Chartfile runs a set of linter rules related to Chart.yaml file diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/chart/v2/lint/rules/chartfile_test.go similarity index 99% rename from pkg/lint/rules/chartfile_test.go rename to pkg/chart/v2/lint/rules/chartfile_test.go index 1719a2011..5a1ad2f24 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/chart/v2/lint/rules/chartfile_test.go @@ -24,8 +24,8 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) const ( diff --git a/pkg/lint/rules/crds.go b/pkg/chart/v2/lint/rules/crds.go similarity index 98% rename from pkg/lint/rules/crds.go rename to pkg/chart/v2/lint/rules/crds.go index 1b8a73139..49e30192a 100644 --- a/pkg/lint/rules/crds.go +++ b/pkg/chart/v2/lint/rules/crds.go @@ -28,8 +28,8 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" - "helm.sh/helm/v4/pkg/lint/support" ) // Crds lints the CRDs in the Linter. diff --git a/pkg/lint/rules/crds_test.go b/pkg/chart/v2/lint/rules/crds_test.go similarity index 95% rename from pkg/lint/rules/crds_test.go rename to pkg/chart/v2/lint/rules/crds_test.go index d497b29ba..e644f182f 100644 --- a/pkg/lint/rules/crds_test.go +++ b/pkg/chart/v2/lint/rules/crds_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) const invalidCrdsDir = "./testdata/invalidcrdsdir" diff --git a/pkg/lint/rules/dependencies.go b/pkg/chart/v2/lint/rules/dependencies.go similarity index 96% rename from pkg/lint/rules/dependencies.go rename to pkg/chart/v2/lint/rules/dependencies.go index 16c9d6435..d944a016d 100644 --- a/pkg/lint/rules/dependencies.go +++ b/pkg/chart/v2/lint/rules/dependencies.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import ( "fmt" "strings" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" - "helm.sh/helm/v4/pkg/lint/support" ) // Dependencies runs lints against a chart's dependencies diff --git a/pkg/lint/rules/dependencies_test.go b/pkg/chart/v2/lint/rules/dependencies_test.go similarity index 98% rename from pkg/lint/rules/dependencies_test.go rename to pkg/chart/v2/lint/rules/dependencies_test.go index 1369b2372..08a6646cd 100644 --- a/pkg/lint/rules/dependencies_test.go +++ b/pkg/chart/v2/lint/rules/dependencies_test.go @@ -20,8 +20,8 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) func chartWithBadDependencies() chart.Chart { diff --git a/pkg/chart/v2/lint/rules/deprecations.go b/pkg/chart/v2/lint/rules/deprecations.go new file mode 100644 index 000000000..6eba316bc --- /dev/null +++ b/pkg/chart/v2/lint/rules/deprecations.go @@ -0,0 +1,106 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" + +import ( + "fmt" + "strconv" + + "helm.sh/helm/v4/pkg/chart/common" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/deprecation" + kscheme "k8s.io/client-go/kubernetes/scheme" +) + +var ( + // This should be set in the Makefile based on the version of client-go being imported. + // These constants will be overwritten with LDFLAGS. The version components must be + // strings in order for LDFLAGS to set them. + k8sVersionMajor = "1" + k8sVersionMinor = "20" +) + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Message string +} + +func (e deprecatedAPIError) Error() string { + msg := e.Message + return msg +} + +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { + // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation + if resource.APIVersion == "" { + return nil + } + if resource.Kind == "" { + return nil + } + + majorVersion := k8sVersionMajor + minorVersion := k8sVersionMinor + + if kubeVersion != nil { + majorVersion = kubeVersion.Major + minorVersion = kubeVersion.Minor + } + + runtimeObject, err := resourceToRuntimeObject(resource) + if err != nil { + // do not error for non-kubernetes resources + if runtime.IsNotRegisteredError(err) { + return nil + } + return err + } + + major, err := strconv.Atoi(majorVersion) + if err != nil { + return err + } + minor, err := strconv.Atoi(minorVersion) + if err != nil { + return err + } + + if !deprecation.IsDeprecated(runtimeObject, major, minor) { + return nil + } + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + return deprecatedAPIError{ + Deprecated: gvk, + Message: deprecation.WarningMessage(runtimeObject), + } +} + +func resourceToRuntimeObject(resource *k8sYamlStruct) (runtime.Object, error) { + scheme := runtime.NewScheme() + kscheme.AddToScheme(scheme) + + gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) + out, err := scheme.New(gvk) + if err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(gvk) + return out, nil +} diff --git a/pkg/lint/rules/deprecations_test.go b/pkg/chart/v2/lint/rules/deprecations_test.go similarity index 94% rename from pkg/lint/rules/deprecations_test.go rename to pkg/chart/v2/lint/rules/deprecations_test.go index 6add843ce..e153f67e6 100644 --- a/pkg/lint/rules/deprecations_test.go +++ b/pkg/chart/v2/lint/rules/deprecations_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "helm.sh/helm/v4/pkg/lint/rules" +package rules // import "helm.sh/helm/v4/pkg/chart/v2/lint/rules" import "testing" diff --git a/pkg/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go similarity index 95% rename from pkg/lint/rules/template.go rename to pkg/chart/v2/lint/rules/template.go index b36153ec6..5c84d0f68 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -33,10 +33,12 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/engine" - "helm.sh/helm/v4/pkg/lint/support" ) // Templates lints the templates in the Linter. @@ -45,12 +47,12 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace } // TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. -func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) { +func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) } // TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. -func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, skipSchemaValidation bool) { +func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { fpath := "templates/" templatesPath := filepath.Join(linter.ChartDir, fpath) @@ -74,12 +76,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string return } - options := chartutil.ReleaseOptions{ + options := common.ReleaseOptions{ Name: "test-release", Namespace: namespace, } - caps := chartutil.DefaultCapabilities.Copy() + caps := common.DefaultCapabilities.Copy() if kubeVersion != nil { caps.KubeVersion = *kubeVersion } @@ -90,12 +92,12 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string return } - cvals, err := chartutil.CoalesceValues(chart, values) + cvals, err := util.CoalesceValues(chart, values) if err != nil { return } - valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) if err != nil { linter.RunLinterRule(support.ErrorSev, fpath, err) return diff --git a/pkg/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go similarity index 98% rename from pkg/lint/rules/template_test.go rename to pkg/chart/v2/lint/rules/template_test.go index 787bd6e4b..3e8e0b371 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -23,9 +23,10 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" ) const templateTestBasedir = "./testdata/albatross" @@ -189,7 +190,7 @@ func TestDeprecatedAPIFails(t *testing.T) { Version: "0.1.0", Icon: "satisfy-the-linting-gods.gif", }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/baddeployment.yaml", Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), @@ -249,7 +250,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) { "key1": "val1", }, }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/configmap.yaml", Data: []byte(manifest), @@ -378,7 +379,7 @@ func TestEmptyWithCommentsManifests(t *testing.T) { Version: "0.1.0", Icon: "satisfy-the-linting-gods.gif", }, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/empty-with-comments.yaml", Data: []byte("#@formatter:off\n"), diff --git a/pkg/lint/rules/testdata/albatross/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/albatross/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl b/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl new file mode 100644 index 000000000..24f76db73 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{define "name"}}{{default "nginx" .Values.nameOverride | trunc 63 | trimSuffix "-" }}{{end}} + +{{/* +Create a default fully qualified app name. + +We truncate at 63 chars because some Kubernetes name fields are limited to this +(by the DNS naming spec). +*/}} +{{define "fullname"}} +{{- $name := default "nginx" .Values.nameOverride -}} +{{printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{end}} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml new file mode 100644 index 000000000..a11e0e90e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml @@ -0,0 +1 @@ +{{ deliberateSyntaxError }} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml new file mode 100644 index 000000000..16bb27d55 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml @@ -0,0 +1,19 @@ +# This is a service gateway to the replica set created by the deployment. +# Take a look at the deployment.yaml for general notes about this chart. +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Values.name }}" + labels: + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + kubeVersion: {{ .Capabilities.KubeVersion.Major }} +spec: + ports: + - port: {{default 80 .Values.httpPort | quote}} + targetPort: 80 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{template "fullname" .}} diff --git a/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml b/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml new file mode 100644 index 000000000..74cc6a0dc --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml @@ -0,0 +1 @@ +name: "mariner" diff --git a/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml new file mode 100644 index 000000000..3564ede3e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml @@ -0,0 +1,11 @@ +description: A Helm chart for Kubernetes +version: 0.0.0.0 +home: "" +type: application +dependencies: +- name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml new file mode 100644 index 000000000..9f367033b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/lint/rules/testdata/badchartname/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badchartname/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml b/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml new file mode 100644 index 000000000..9f367033b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/lint/rules/testdata/badcrdfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badcrdfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml new file mode 100644 index 000000000..468916053 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml @@ -0,0 +1,2 @@ +apiVersion: bad.k8s.io/v1beta1 +kind: CustomResourceDefinition diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml new file mode 100644 index 000000000..523b97f85 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml @@ -0,0 +1,2 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: NotACustomResourceDefinition diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep b/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml new file mode 100644 index 000000000..2fffc7715 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/badvaluesfile/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml new file mode 100644 index 000000000..6c2ceb8db --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{.name | default "foo" | title}} diff --git a/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml new file mode 100644 index 000000000..b5a10271c --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml @@ -0,0 +1,2 @@ +# Invalid value for badvaluesfile for testing lint fails with invalid yaml format +name= "value" diff --git a/pkg/lint/rules/testdata/goodone/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/goodone/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml new file mode 100644 index 000000000..1d7350f1d --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml new file mode 100644 index 000000000..cd46f62c7 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.name | default "foo" | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml b/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml new file mode 100644 index 000000000..92c3d9bb9 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml @@ -0,0 +1 @@ +name: "goodone-here" diff --git a/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml new file mode 100644 index 000000000..0fd58d1d4 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml @@ -0,0 +1,6 @@ +name: some-chart +apiVersion: v2 +apiVersion: v1 +description: A Helm chart for Kubernetes +version: 1.3.0 +icon: http://example.com diff --git a/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml b/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml new file mode 100644 index 000000000..6b1611a64 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for invalidcrdsdir. diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore b/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/lint/rules/testdata/malformed-template/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/malformed-template/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml new file mode 100644 index 000000000..213198fda --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml @@ -0,0 +1 @@ +{ {- $relname := .Release.Name -}} diff --git a/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml b/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml new file mode 100644 index 000000000..1cc3182ea --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml @@ -0,0 +1,82 @@ +# Default values for test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/multi-template-fail/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml new file mode 100644 index 000000000..835be07be --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-config +data: + game.properties: cheat +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: -this:name-is-not_valid$ +data: + game.properties: empty diff --git a/pkg/lint/rules/testdata/v3-fail/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/v3-fail/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl new file mode 100644 index 000000000..0b89e723b --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "v3-fail.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "v3-fail.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "v3-fail.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "v3-fail.labels" -}} +helm.sh/chart: {{ include "v3-fail.chart" . }} +{{ include "v3-fail.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "v3-fail.selectorLabels" -}} +app.kubernetes.io/name: {{ include "v3-fail.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "v3-fail.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "v3-fail.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml new file mode 100644 index 000000000..6d651ab8e --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "v3-fail.fullname" . }} + labels: + nope: {{ .Release.Time }} + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "v3-fail.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "v3-fail.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "v3-fail.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml new file mode 100644 index 000000000..4790650d0 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml @@ -0,0 +1,62 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "v3-fail.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "v3-fail.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + "helm.sh/hook": crd-install + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml new file mode 100644 index 000000000..79a0f40b0 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "v3-fail.fullname" . }} + annotations: + helm.sh/hook: crd-install + labels: + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "v3-fail.selectorLabels" . | nindent 4 }} diff --git a/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml b/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml new file mode 100644 index 000000000..01d99b4e6 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml @@ -0,0 +1,66 @@ +# Default values for v3-fail. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/testdata/withsubchart/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml similarity index 100% rename from pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml rename to pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml new file mode 100644 index 000000000..422a359d5 --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml @@ -0,0 +1,2 @@ +subchart: + name: subchart \ No newline at end of file diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml b/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/values.go b/pkg/chart/v2/lint/rules/values.go similarity index 84% rename from pkg/lint/rules/values.go rename to pkg/chart/v2/lint/rules/values.go index 019e74fa7..5260bf8b3 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/chart/v2/lint/rules/values.go @@ -21,8 +21,9 @@ import ( "os" "path/filepath" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/lint/support" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" ) // ValuesWithOverrides tests the values.yaml file. @@ -52,7 +53,7 @@ func validateValuesFileExistence(valuesPath string) error { } func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { - values, err := chartutil.ReadValuesFile(valuesPath) + values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) } @@ -62,8 +63,8 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err // We could change that. For now, though, we retain that strategy, and thus can // coalesce tables (like reuse-values does) instead of doing the full chart // CoalesceValues - coalescedValues := chartutil.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) - coalescedValues = chartutil.CoalesceTables(coalescedValues, values) + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) ext := filepath.Ext(valuesPath) schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" @@ -74,5 +75,5 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err if err != nil { return err } - return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema) + return util.ValidateAgainstSingleSchema(coalescedValues, schema) } diff --git a/pkg/chart/v2/lint/rules/values_test.go b/pkg/chart/v2/lint/rules/values_test.go new file mode 100644 index 000000000..348695785 --- /dev/null +++ b/pkg/chart/v2/lint/rules/values_test.go @@ -0,0 +1,169 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/internal/test/ensure" +) + +var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") + +const testSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "helm values test schema", + "type": "object", + "additionalProperties": false, + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "Your username", + "type": "string" + }, + "password": { + "description": "Your password", + "type": "string" + } + } +} +` + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} + +func TestValidateValuesFileWellFormed(t *testing.T) { + badYaml := ` + not:well[]{}formed + ` + tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { + t.Fatal("expected values file to fail parsing") + } +} + +func TestValidateValuesFileSchema(t *testing.T) { + yaml := "username: admin\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFileSchemaFailure(t *testing.T) { + // 1234 is an int, not a string. This should fail. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}) + if err == nil { + t.Fatal("expected values file to fail parsing") + } + + assert.Contains(t, err.Error(), "- at '/username': got number, want string") +} + +func TestValidateValuesFileSchemaOverrides(t *testing.T) { + yaml := "username: admin" + overrides := map[string]interface{}{ + "password": "swordfish", + } + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, overrides); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFile(t *testing.T) { + tests := []struct { + name string + yaml string + overrides map[string]interface{} + errorMessage string + }{ + { + name: "value added", + yaml: "username: admin", + overrides: map[string]interface{}{"password": "swordfish"}, + }, + { + name: "value not overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser"}, + errorMessage: "- at '/password': got null, want string", + }, + { + name: "value overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, tt.overrides) + + switch { + case err != nil && tt.errorMessage == "": + t.Errorf("Failed validation with %s", err) + case err == nil && tt.errorMessage != "": + t.Error("expected values file to fail parsing") + case err != nil && tt.errorMessage != "": + assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") + } + }) + } +} + +func createTestingSchema(t *testing.T, dir string) string { + t.Helper() + schemafile := filepath.Join(dir, "values.schema.json") + if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { + t.Fatalf("Failed to write schema to tmpdir: %s", err) + } + return schemafile +} diff --git a/pkg/lint/support/doc.go b/pkg/chart/v2/lint/support/doc.go similarity index 91% rename from pkg/lint/support/doc.go rename to pkg/chart/v2/lint/support/doc.go index b007804dc..7e050b8c2 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/chart/v2/lint/support/doc.go @@ -20,4 +20,4 @@ Package support contains tools for linting charts. Linting is the process of testing charts for errors or warnings regarding formatting, compilation, or standards compliance. */ -package support // import "helm.sh/helm/v4/pkg/lint/support" +package support // import "helm.sh/helm/v4/pkg/chart/v2/lint/support" diff --git a/pkg/chart/v2/lint/support/message.go b/pkg/chart/v2/lint/support/message.go new file mode 100644 index 000000000..5efbc7a61 --- /dev/null +++ b/pkg/chart/v2/lint/support/message.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import "fmt" + +// Severity indicates the severity of a Message. +const ( + // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. + UnknownSev = iota + // InfoSev indicates information, for example missing values.yaml file + InfoSev + // WarningSev indicates that something does not meet code standards, but will likely function. + WarningSev + // ErrorSev indicates that something will not likely function. + ErrorSev +) + +// sev matches the *Sev states. +var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} + +// Linter encapsulates a linting run of a particular chart. +type Linter struct { + Messages []Message + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string +} + +// Message describes an error encountered while linting. +type Message struct { + // Severity is one of the *Sev constants + Severity int + Path string + Err error +} + +func (m Message) Error() string { + return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) +} + +// NewMessage creates a new Message struct +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} +} + +// RunLinterRule returns true if the validation passed +func (l *Linter) RunLinterRule(severity int, path string, err error) bool { + // severity is out of bound + if severity < 0 || severity >= len(sev) { + return false + } + + if err != nil { + l.Messages = append(l.Messages, NewMessage(severity, path, err)) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } + } + return err == nil +} diff --git a/pkg/chart/v2/lint/support/message_test.go b/pkg/chart/v2/lint/support/message_test.go new file mode 100644 index 000000000..ce5b5e42e --- /dev/null +++ b/pkg/chart/v2/lint/support/message_test.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import ( + "errors" + "testing" +) + +var errLint = errors.New("lint failed") + +func TestRunLinterRule(t *testing.T) { + var tests = []struct { + Severity int + LintError error + ExpectedMessages int + ExpectedReturn bool + ExpectedHighestSeverity int + }{ + {InfoSev, errLint, 1, false, InfoSev}, + {WarningSev, errLint, 2, false, WarningSev}, + {ErrorSev, errLint, 3, false, ErrorSev}, + // No error so it returns true + {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, errLint, 4, false, ErrorSev}, + // Invalid severity values + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, + } + + linter := Linter{} + for _, test := range tests { + isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) + if len(linter.Messages) != test.ExpectedMessages { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + } + + if linter.HighestSeverity != test.ExpectedHighestSeverity { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) + } + + if isValid != test.ExpectedReturn { + t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) + } + } +} + +func TestMessage(t *testing.T) { + m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} + if m.Error() != "[ERROR] Chart.yaml: Foo" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{WarningSev, "templates/", errors.New("Bar")} + if m.Error() != "[WARNING] templates/: Bar" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} + if m.Error() != "[INFO] templates/rc.yaml: FooBar" { + t.Errorf("Unexpected output: %s", m.Error()) + } +} diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 75c73e959..0c025e183 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -31,6 +31,7 @@ import ( utilyaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -80,7 +81,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { // do not rely on assumed ordering of files in the chart and crash // if Chart.yaml was not coming early enough to initialize metadata for _, f := range files { - c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -128,7 +129,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { return c, fmt.Errorf("cannot load requirements.yaml: %w", err) } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } // Deprecated: requirements.lock is deprecated use Chart.lock. case f.Name == "requirements.lock": @@ -143,14 +144,14 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.") } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) continue } @@ -158,7 +159,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { cname := strings.SplitN(fname, "/", 2)[0] subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) default: - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } } diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 41154421c..c4ae646f6 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -543,7 +544,7 @@ foo: } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesV2(t *testing.T) { nestedMap := map[string]interface{}{ "foo": "bar", "baz": map[string]string{ @@ -753,7 +754,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { } } -func verifyBomStripped(t *testing.T, files []*chart.File) { +func verifyBomStripped(t *testing.T, files []*common.File) { t.Helper() for _, file := range files { if bytes.HasPrefix(file.Data, utf8bom) { diff --git a/pkg/chart/v2/util/create.go b/pkg/chart/v2/util/create.go index a8ae3ab40..d7c1fe31c 100644 --- a/pkg/chart/v2/util/create.go +++ b/pkg/chart/v2/util/create.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -655,11 +656,11 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { schart.Metadata = chartfile - var updatedTemplates []*chart.File + var updatedTemplates []*common.File for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) } schart.Templates = updatedTemplates diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index 1a2aa1c95..a52f09f82 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -22,11 +22,13 @@ import ( "github.com/mitchellh/copystructure" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" ) // ProcessDependencies checks through this chart's dependencies, processing accordingly. -func ProcessDependencies(c *chart.Chart, v Values) error { +func ProcessDependencies(c *chart.Chart, v common.Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } @@ -34,7 +36,7 @@ func ProcessDependencies(c *chart.Chart, v Values) error { } // processDependencyConditions disables charts based on condition path value in values -func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { if reqs == nil { return } @@ -50,7 +52,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if _, ok := err.(ErrNoValue); !ok { + } else if _, ok := err.(common.ErrNoValue); !ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } @@ -60,7 +62,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s } // processDependencyTags disables charts based on tags in values -func processDependencyTags(reqs []*chart.Dependency, cvals Values) { +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { if reqs == nil { return } @@ -177,7 +179,7 @@ Loop: for _, lr := range c.Metadata.Dependencies { lr.Enabled = true } - cvals, err := CoalesceValues(c, v) + cvals, err := util.CoalesceValues(c, v) if err != nil { return err } @@ -232,6 +234,8 @@ func pathToMap(path string, data map[string]interface{}) map[string]interface{} return set(parsePath(path), data) } +func parsePath(key string) []string { return strings.Split(key, ".") } + func set(path []string, data map[string]interface{}) map[string]interface{} { if len(path) == 0 { return nil @@ -249,12 +253,12 @@ func processImportValues(c *chart.Chart, merge bool) error { return nil } // combine chart values and empty config to get Values - var cvals Values + var cvals common.Values var err error if merge { - cvals, err = MergeValues(c, nil) + cvals, err = util.MergeValues(c, nil) } else { - cvals, err = CoalesceValues(c, nil) + cvals, err = util.CoalesceValues(c, nil) } if err != nil { return err @@ -282,9 +286,9 @@ func processImportValues(c *chart.Chart, merge bool) error { } // create value map from child to be merged into parent if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) } case string: child := "exports." + iv @@ -298,9 +302,9 @@ func processImportValues(c *chart.Chart, merge bool) error { continue } if merge { - b = MergeTables(b, vm.AsMap()) + b = util.MergeTables(b, vm.AsMap()) } else { - b = CoalesceTables(b, vm.AsMap()) + b = util.CoalesceTables(b, vm.AsMap()) } } } @@ -315,14 +319,14 @@ func processImportValues(c *chart.Chart, merge bool) error { // deep copying the cvals as there are cases where pointers can end // up in the cvals when they are copied onto b in ways that break things. cvals = deepCopyMap(cvals) - c.Values = MergeTables(cvals, b) + c.Values = util.MergeTables(cvals, b) } else { // Trimming the nil values from cvals is needed for backwards compatibility. // Previously, the b value had been populated with cvals along with some // overrides. This caused the coalescing functionality to remove the // nil/null values. This trimming is for backwards compat. cvals = trimNilValues(cvals) - c.Values = CoalesceTables(cvals, b) + c.Values = util.CoalesceTables(cvals, b) } return nil @@ -355,6 +359,12 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} { return valsCopyMap } +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + // processDependencyImportValues imports specified chart values from child to parent. func processDependencyImportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { diff --git a/pkg/chart/v2/util/dependencies_test.go b/pkg/chart/v2/util/dependencies_test.go index d645d7bf5..c817b0b89 100644 --- a/pkg/chart/v2/util/dependencies_test.go +++ b/pkg/chart/v2/util/dependencies_test.go @@ -21,6 +21,7 @@ import ( "strconv" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -221,7 +222,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { @@ -251,7 +252,7 @@ func TestProcessDependencyImportValues(t *testing.T) { t.Error("expect nil value not found but found it") } switch xerr := err.(type) { - case ErrNoValue: + case common.ErrNoValue: // We found what we expected default: t.Errorf("expected an ErrNoValue but got %q instead", xerr) @@ -261,7 +262,7 @@ func TestProcessDependencyImportValues(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc = Values(c.Values) + cc = common.Values(c.Values) val, err := cc.PathValue("ensurenull") if err != nil { t.Error("expect value but ensurenull was not found") @@ -291,7 +292,7 @@ func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T e["foo.grandchild.defaults.defaultValue"] = "42" e["bar.grandchild.defaults.defaultValue"] = "42" - cValues := Values(c.Values) + cValues := common.Values(c.Values) for kk, vv := range e { pv, err := cValues.PathValue(kk) if err != nil { @@ -329,7 +330,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } - cc := Values(c.Values) + cc := common.Values(c.Values) for kk, vv := range e { pv, err := cc.PathValue(kk) if err != nil { diff --git a/pkg/chart/v2/util/save.go b/pkg/chart/v2/util/save.go index 624a5b562..69a98924c 100644 --- a/pkg/chart/v2/util/save.go +++ b/pkg/chart/v2/util/save.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -76,7 +77,7 @@ func SaveDir(c *chart.Chart, dest string) error { } // Save templates and files - for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, o := range [][]*common.File{c.Templates, c.Files} { for _, f := range o { n := filepath.Join(outdir, f.Name) if err := writeFile(n, f.Data); err != nil { @@ -258,7 +259,7 @@ func validateName(name string) error { nname := filepath.Base(name) if nname != name { - return ErrInvalidChartName{name} + return common.ErrInvalidChartName{Name: name} } return nil diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index ff96331b5..ef822a82a 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" ) @@ -47,7 +48,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -116,7 +117,7 @@ func TestSave(t *testing.T) { Lock: &chart.Lock{ Digest: "testdigest", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, } @@ -156,7 +157,7 @@ func TestSavePreservesTimestamps(t *testing.T) { "imageName": "testimage", "imageId": 42, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), @@ -222,10 +223,10 @@ func TestSaveDir(t *testing.T) { Name: "ahab", Version: "1.2.3", }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index 4dbc709f1..fe1afc5d2 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -294,7 +294,7 @@ func TestReadFileOriginal(t *testing.T) { } } -func TestMergeValues(t *testing.T) { +func TestMergeValuesCLI(t *testing.T) { tests := []struct { name string opts Options diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 40478c30e..55e3a842f 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -28,7 +28,7 @@ import ( "helm.sh/helm/v4/internal/test" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/cli" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -91,7 +91,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) actionConfig := &action.Configuration{ Releases: store, KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, - Capabilities: chartutil.DefaultCapabilities, + Capabilities: common.DefaultCapabilities, } root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) diff --git a/pkg/cmd/lint.go b/pkg/cmd/lint.go index 78083a7ea..71540f1be 100644 --- a/pkg/cmd/lint.go +++ b/pkg/cmd/lint.go @@ -27,10 +27,10 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/getter" - "helm.sh/helm/v4/pkg/lint/support" ) var longLintHelp = ` @@ -58,7 +58,7 @@ func newLintCmd(out io.Writer) *cobra.Command { } if kubeVersion != "" { - parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion) + parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index aa836f9f3..3d1309c3e 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -30,7 +30,7 @@ import ( coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common/util" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" @@ -197,7 +197,7 @@ func (s statusPrinter) WriteTable(out io.Writer) error { // Print an extra newline _, _ = fmt.Fprintln(out) - cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) + cfg, err := util.CoalesceValues(s.release.Chart, s.release.Config) if err != nil { return err } diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index aaf848c9e..81c112d51 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -35,7 +35,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" @@ -69,7 +69,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, RunE: func(_ *cobra.Command, args []string) error { if kubeVersion != "" { - parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion) + parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) } @@ -93,7 +93,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.ReleaseName = "release-name" client.Replace = true // Skip the name check client.ClientOnly = !validate - client.APIVersions = chartutil.VersionSet(extraAPIs) + client.APIVersions = common.VersionSet(extraAPIs) client.IncludeCRDs = includeCrds rel, err := runInstall(args, client, valueOpts, out) diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index d7375dcad..9b17f187d 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -24,6 +24,7 @@ import ( "strings" "testing" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -382,7 +383,7 @@ func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { @@ -490,7 +491,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 6e47a0e39..a0ca17f08 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -30,8 +30,8 @@ import ( "k8s.io/client-go/rest" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + ci "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/common" ) // taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 @@ -88,21 +88,21 @@ func New(config *rest.Config) Engine { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. -func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { +func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) return e.render(tmap) } // Render takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. -func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { +func Render(chrt ci.Charter, values common.Values) (map[string]string, error) { return new(Engine).Render(chrt, values) } // RenderWithClient takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. -func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) { +func RenderWithClient(chrt ci.Charter, values common.Values, config *rest.Config) (map[string]string, error) { var clientProvider ClientProvider = clientProviderFromConfig{config} return Engine{ clientProvider: &clientProvider, @@ -113,7 +113,7 @@ func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.C // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. // This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed. -func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) { +func RenderWithClientProvider(chrt ci.Charter, values common.Values, clientProvider ClientProvider) (map[string]string, error) { return Engine{ clientProvider: &clientProvider, }.Render(chrt, values) @@ -124,7 +124,7 @@ type renderable struct { // tpl is the current template. tpl string // vals are the values to be supplied to the template. - vals chartutil.Values + vals common.Values // namespace prefix to the templates of the current chart basePath string } @@ -312,7 +312,7 @@ func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, } // At render time, add information about the template that is being rendered. vals := tpls[filename].vals - vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} + vals["Template"] = common.Values{"Name": filename, "BasePath": tpls[filename].basePath} var buf strings.Builder if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { return map[string]string{}, reformatExecErrorMsg(filename, err) @@ -455,7 +455,7 @@ func (p byPathLen) Less(i, j int) bool { // allTemplates returns all templates for a chart and its dependencies. // // As it goes, it also prepares the values in a scope-sensitive manner. -func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { +func allTemplates(c ci.Charter, vals common.Values) map[string]renderable { templates := make(map[string]renderable) recAllTpls(c, templates, vals) return templates @@ -465,40 +465,46 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { // // As it recurses, it also sets the values to be appropriate for the template // scope. -func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { +func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Values) map[string]interface{} { + vals := values.AsMap() subCharts := make(map[string]interface{}) - chartMetaData := struct { - chart.Metadata - IsRoot bool - }{*c.Metadata, c.IsRoot()} + accessor, err := ci.NewAccessor(c) + if err != nil { + slog.Error("error accessing chart", "error", err) + } + chartMetaData := accessor.MetadataAsMap() + fmt.Printf("metadata: %v\n", chartMetaData) + chartMetaData["IsRoot"] = accessor.IsRoot() next := map[string]interface{}{ "Chart": chartMetaData, - "Files": newFiles(c.Files), + "Files": newFiles(accessor.Files()), "Release": vals["Release"], "Capabilities": vals["Capabilities"], - "Values": make(chartutil.Values), + "Values": make(common.Values), "Subcharts": subCharts, } // If there is a {{.Values.ThisChart}} in the parent metadata, // copy that into the {{.Values}} for this template. - if c.IsRoot() { + if accessor.IsRoot() { next["Values"] = vals["Values"] - } else if vs, err := vals.Table("Values." + c.Name()); err == nil { + } else if vs, err := values.Table("Values." + accessor.Name()); err == nil { next["Values"] = vs } - for _, child := range c.Dependencies() { - subCharts[child.Name()] = recAllTpls(child, templates, next) + for _, child := range accessor.Dependencies() { + // TODO: Handle error + sub, _ := ci.NewAccessor(child) + subCharts[sub.Name()] = recAllTpls(child, templates, next) } - newParentID := c.ChartFullPath() - for _, t := range c.Templates { + newParentID := accessor.ChartFullPath() + for _, t := range accessor.Templates() { if t == nil { continue } - if !isTemplateValid(c, t.Name) { + if !isTemplateValid(accessor, t.Name) { continue } templates[path.Join(newParentID, t.Name)] = renderable{ @@ -512,14 +518,9 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil. } // isTemplateValid returns true if the template is valid for the chart type -func isTemplateValid(ch *chart.Chart, templateName string) bool { - if isLibraryChart(ch) { +func isTemplateValid(accessor ci.Accessor, templateName string) bool { + if accessor.IsLibraryChart() { return strings.HasPrefix(filepath.Base(templateName), "_") } return true } - -// isLibraryChart returns true if the chart is a library chart -func isLibraryChart(c *chart.Chart) bool { - return strings.EqualFold(c.Metadata.Type, "library") -} diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index f4228fbd7..7ac892cec 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -32,8 +32,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/fake" + "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) func TestSortTemplates(t *testing.T) { @@ -94,7 +95,7 @@ func TestRender(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")}, @@ -114,7 +115,7 @@ func TestRender(t *testing.T) { }, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -144,7 +145,7 @@ func TestRenderRefsOrdering(t *testing.T) { Name: "parent", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, }, @@ -154,7 +155,7 @@ func TestRenderRefsOrdering(t *testing.T) { Name: "child", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, }, } @@ -165,7 +166,7 @@ func TestRenderRefsOrdering(t *testing.T) { } for i := 0; i < 100; i++ { - out, err := Render(parentChart, chartutil.Values{}) + out, err := Render(parentChart, common.Values{}) if err != nil { t.Fatalf("Failed to render templates: %s", err) } @@ -181,7 +182,7 @@ func TestRenderRefsOrdering(t *testing.T) { func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. - vals := chartutil.Values{"Name": "one", "Value": "two"} + vals := common.Values{"Name": "one", "Value": "two"} tpls := map[string]renderable{ "one": {tpl: `Hello {{title .Name}}`, vals: vals}, "two": {tpl: `Goodbye {{upper .Value}}`, vals: vals}, @@ -218,7 +219,7 @@ func TestRenderWithDNS(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{}, @@ -228,7 +229,7 @@ func TestRenderWithDNS(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -355,7 +356,7 @@ func TestRenderWithClientProvider(t *testing.T) { } for name, exp := range cases { - c.Templates = append(c.Templates, &chart.File{ + c.Templates = append(c.Templates, &common.File{ Name: path.Join("templates", name), Data: []byte(exp.template), }) @@ -365,7 +366,7 @@ func TestRenderWithClientProvider(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -391,7 +392,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, }, Values: map[string]interface{}{}, @@ -401,7 +402,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -448,7 +449,7 @@ func TestParallelRenderInternals(t *testing.T) { } func TestParseErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} tplsUndefinedFunction := map[string]renderable{ "undefined_function": {tpl: `{{foo}}`, vals: vals}, @@ -464,7 +465,7 @@ func TestParseErrors(t *testing.T) { } func TestExecErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} cases := []struct { name string tpls map[string]renderable @@ -528,7 +529,7 @@ linebreak`, } func TestFailErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} failtpl := `All your base are belong to us{{ fail "This is an error" }}` tplsFailed := map[string]renderable{ @@ -559,14 +560,14 @@ func TestFailErrors(t *testing.T) { func TestAllTemplates(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/foo", Data: []byte("foo")}, {Name: "templates/bar", Data: []byte("bar")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "laboratory mice"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/pinky", Data: []byte("pinky")}, {Name: "templates/brain", Data: []byte("brain")}, }, @@ -575,13 +576,13 @@ func TestAllTemplates(t *testing.T) { dep2 := &chart.Chart{ Metadata: &chart.Metadata{Name: "same thing we do every night"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/innermost", Data: []byte("innermost")}, }, } dep1.AddDependency(dep2) - tpls := allTemplates(ch1, chartutil.Values{}) + tpls := allTemplates(ch1, common.Values{}) if len(tpls) != 5 { t.Errorf("Expected 5 charts, got %d", len(tpls)) } @@ -590,19 +591,19 @@ func TestAllTemplates(t *testing.T) { func TestChartValuesContainsIsRoot(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "parent"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "child"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, }, } ch1.AddDependency(dep1) - out, err := Render(ch1, chartutil.Values{}) + out, err := Render(ch1, common.Values{}) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -622,13 +623,13 @@ func TestRenderDependency(t *testing.T) { toptpl := `Hello {{template "myblock"}}` ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/outer", Data: []byte(toptpl)}, }, } ch.AddDependency(&chart.Chart{ Metadata: &chart.Metadata{Name: "innerchart"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/inner", Data: []byte(deptpl)}, }, }) @@ -660,7 +661,7 @@ func TestRenderNestedValues(t *testing.T) { deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, @@ -669,7 +670,7 @@ func TestRenderNestedValues(t *testing.T) { inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, @@ -678,7 +679,7 @@ func TestRenderNestedValues(t *testing.T) { outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, @@ -706,15 +707,15 @@ func TestRenderNestedValues(t *testing.T) { }, } - tmp, err := chartutil.CoalesceValues(outer, injValues) + tmp, err := util.CoalesceValues(outer, injValues) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - inject := chartutil.Values{ + inject := common.Values{ "Values": tmp, "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "dyin", }, } @@ -754,30 +755,30 @@ func TestRenderNestedValues(t *testing.T) { func TestRenderBuiltinValues(t *testing.T) { inner := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Latium"}, - Templates: []*chart.File{ + Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "author", Data: []byte("Virgil")}, {Name: "book/title.txt", Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Troy"}, - Templates: []*chart.File{ + Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) - inject := chartutil.Values{ + inject := common.Values{ "Values": "", "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Aeneid", }, } @@ -806,7 +807,7 @@ func TestRenderBuiltinValues(t *testing.T) { func TestAlterFuncMap_include(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, }, @@ -815,16 +816,16 @@ func TestAlterFuncMap_include(t *testing.T) { // Check nested reference in include FuncMap d := &chart.Chart{ Metadata: &chart.Metadata{Name: "nested"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Mistah Kurtz", }, } @@ -849,19 +850,19 @@ func TestAlterFuncMap_include(t *testing.T) { func TestAlterFuncMap_require(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "who": "us", "bases": 2, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } @@ -882,12 +883,12 @@ func TestAlterFuncMap_require(t *testing.T) { // test required without passing in needed values with lint mode on // verifies lint replaces required with an empty string (should not fail) - lintValues := chartutil.Values{ - "Values": chartutil.Values{ + lintValues := common.Values{ + "Values": common.Values{ "who": "us", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } @@ -911,17 +912,17 @@ func TestAlterFuncMap_require(t *testing.T) { func TestAlterFuncMap_tpl(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -940,17 +941,17 @@ func TestAlterFuncMap_tpl(t *testing.T) { func TestAlterFuncMap_tplfunc(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -969,17 +970,17 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { func TestAlterFuncMap_tplinclude(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1000,15 +1001,15 @@ func TestRenderRecursionLimit(t *testing.T) { // endless recursion should produce an error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "bad"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1030,7 +1031,7 @@ func TestRenderRecursionLimit(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/quote", Data: []byte(repeatedIncl)}, {Name: "templates/_function", Data: []byte(printFunc)}, }, @@ -1054,23 +1055,23 @@ func TestRenderRecursionLimit(t *testing.T) { func TestRenderLoadTemplateForTplFromFile(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, }, - Files: []*chart.File{ + Files: []*common.File{ {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "filename": "test", "filename2": "test2", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1089,15 +1090,15 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) { func TestRenderTplEmpty(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplEmpty"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1123,7 +1124,7 @@ func TestRenderTplTemplateNames(t *testing.T) { // .Template.BasePath and .Name make it through c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplTemplateNames"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, @@ -1131,10 +1132,10 @@ func TestRenderTplTemplateNames(t *testing.T) { {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ - "dot": chartutil.Values{ - "Template": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ + "dot": common.Values{ + "Template": common.Values{ "BasePath": "path/to/template", "Name": "name-of-template", "Field": "extra-field", @@ -1142,7 +1143,7 @@ func TestRenderTplTemplateNames(t *testing.T) { }, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1170,7 +1171,7 @@ func TestRenderTplRedefines(t *testing.T) { // Redefining a template inside 'tpl' does not affect the outer definition c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplRedefines"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, {Name: "templates/partial", Data: []byte( `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, @@ -1192,8 +1193,8 @@ func TestRenderTplRedefines(t *testing.T) { )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`, "manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`, "manifestOnlyText": `tpl: {{include "manifest-only" .}}`, @@ -1205,7 +1206,7 @@ func TestRenderTplRedefines(t *testing.T) { "innerText": `{{define "nested"}}redefined-in-inner-tpl{{end}}inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1236,16 +1237,16 @@ func TestRenderTplMissingKey(t *testing.T) { // Rendering a missing key results in empty/zero output. c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKey"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/manifest", Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1269,16 +1270,16 @@ func TestRenderTplMissingKeyString(t *testing.T) { // Rendering a missing key results in error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/manifest", Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1301,7 +1302,7 @@ func TestRenderTplMissingKeyString(t *testing.T) { func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/svc.yaml", Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, @@ -1324,9 +1325,9 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 executing "common.names.get_name" at <.Values.nonexistant.key>: nil pointer evaluating interface {}.key` - v := chartutil.Values{} + v := common.Values{} - val, _ := chartutil.CoalesceValues(c, v) + val, _ := util.CoalesceValues(c, v) vals := map[string]interface{}{ "Values": val.AsMap(), } @@ -1339,7 +1340,7 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 func TestMultilineNoTemplateAssociatedError(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "multiline"}, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/svc.yaml", Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, @@ -1357,9 +1358,9 @@ func TestMultilineNoTemplateAssociatedError(t *testing.T) { error calling include: template: no template "nested_helper.name" associated with template "gotpl"` - v := chartutil.Values{} + v := common.Values{} - val, _ := chartutil.CoalesceValues(c, v) + val, _ := util.CoalesceValues(c, v) vals := map[string]interface{}{ "Values": val.AsMap(), } @@ -1373,7 +1374,7 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { // Create a chart with two templates that use custom functions c := &chart.Chart{ Metadata: &chart.Metadata{Name: "CustomFunc"}, - Templates: []*chart.File{ + Templates: []*common.File{ { Name: "templates/manifest", Data: []byte(`{{exclaim .Values.message}}`), @@ -1384,12 +1385,12 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { }, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "message": "hello", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } diff --git a/pkg/engine/files.go b/pkg/engine/files.go index 87166728c..f0a86988e 100644 --- a/pkg/engine/files.go +++ b/pkg/engine/files.go @@ -23,7 +23,7 @@ import ( "github.com/gobwas/glob" - chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/common" ) // files is a map of files in a chart that can be accessed from a template. @@ -31,7 +31,7 @@ type files map[string][]byte // NewFiles creates a new files from chart files. // Given an []*chart.File (the format for files in a chart.Chart), extract a map of files. -func newFiles(from []*chart.File) files { +func newFiles(from []*common.File) files { files := make(map[string][]byte) for _, f := range from { files[f.Name] = f.Data diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 605b43a48..18ed2b63b 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -35,7 +35,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // NewLookupFunction returns a function for looking up objects in the cluster. // // If the resource does not exist, no error is raised. -func NewLookupFunction(config *rest.Config) lookupFunc { +func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive return newLookupFunction(clientProviderFromConfig{config: config}) } diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 3d3b0c2e2..c3a6594cc 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -20,6 +20,7 @@ import ( "fmt" "math/rand" + "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/time" ) @@ -98,7 +99,7 @@ func Mock(opts *MockReleaseOptions) *Release { }, }, }, - Templates: []*chart.File{ + Templates: []*common.File{ {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, }, } diff --git a/pkg/release/v1/util/manifest_sorter.go b/pkg/release/v1/util/manifest_sorter.go index 21fdec7c6..6f7b4ea8b 100644 --- a/pkg/release/v1/util/manifest_sorter.go +++ b/pkg/release/v1/util/manifest_sorter.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/yaml" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/chart/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -74,7 +74,7 @@ var events = map[string]release.HookEvent{ // // Files that do not parse into the expected format are simply placed into a map and // returned. -func SortManifests(files map[string]string, _ chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { +func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { result := &result{} var sortedFilePaths []string