mirror of
https://github.com/helm/helm.git
synced 2026-02-19 02:39:04 -05:00
feat(create): add --chart-api-version flag (when HELM_EXPERIMENTAL_CHART_V3 env var is set) (#31592)
* feat(create): add hidden --chart-api-version flag Add --chart-api-version flag to helm create command to allow selecting chart API version (v2 or v3) when creating a new chart. - Default is v2 (existing behavior unchanged) - v3 uses internal/chart/v3 scaffold generator - Invalid versions return clear error message - Works with --starter flag Signed-off-by: Evans Mungai <mbuevans@gmail.com> * Add HELM_EXPERIMENTAL_CHART_V3 feature gate to create command Signed-off-by: Evans Mungai <mbuevans@gmail.com> * make chartv3 private and use loader to load the chart Signed-off-by: Evans Mungai <mbuevans@gmail.com> * Hide chart-api-version flag until chart v3 is officially released Signed-off-by: Evans Mungai <mbuevans@gmail.com> * Conditionally hide the --chart-api-version flag if chart v3 is not enabled Signed-off-by: Evans Mungai <mbuevans@gmail.com> * Add internal gates package for internal feature gates Signed-off-by: Evans Mungai <mbuevans@gmail.com> * Add doc for internal/gates package Signed-off-by: Evans Mungai <mbuevans@gmail.com> --------- Signed-off-by: Evans Mungai <mbuevans@gmail.com>
This commit is contained in:
parent
fbb8de54be
commit
5aac32077f
4 changed files with 330 additions and 126 deletions
19
internal/gates/doc.go
Normal file
19
internal/gates/doc.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
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 gates contains internal feature gates that can be used to enable or disable experimental features.
|
||||
// This is a separate internal package instead of using the pkg/gates package to avoid circular dependencies.
|
||||
package gates
|
||||
21
internal/gates/gates.go
Normal file
21
internal/gates/gates.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
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 gates
|
||||
|
||||
import "helm.sh/helm/v4/pkg/gates"
|
||||
|
||||
// ChartV3 is the feature gate for chart API version v3.
|
||||
const ChartV3 gates.Gate = "HELM_EXPERIMENTAL_CHART_V3"
|
||||
|
|
@ -23,6 +23,9 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
chartv3 "helm.sh/helm/v4/internal/chart/v3"
|
||||
chartutilv3 "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/internal/gates"
|
||||
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
|
||||
"helm.sh/helm/v4/pkg/cmd/require"
|
||||
|
|
@ -51,9 +54,10 @@ will be overwritten, but other files will be left alone.
|
|||
`
|
||||
|
||||
type createOptions struct {
|
||||
starter string // --starter
|
||||
name string
|
||||
starterDir string
|
||||
starter string // --starter
|
||||
name string
|
||||
starterDir string
|
||||
chartAPIVersion string // --chart-api-version
|
||||
}
|
||||
|
||||
func newCreateCmd(out io.Writer) *cobra.Command {
|
||||
|
|
@ -81,12 +85,32 @@ func newCreateCmd(out io.Writer) *cobra.Command {
|
|||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.starter, "starter", "p", "", "the name or absolute path to Helm starter scaffold")
|
||||
cmd.Flags().StringVar(&o.chartAPIVersion, "chart-api-version", chart.APIVersionV2, "chart API version to use (v2 or v3)")
|
||||
|
||||
if !gates.ChartV3.IsEnabled() {
|
||||
cmd.Flags().MarkHidden("chart-api-version")
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *createOptions) run(out io.Writer) error {
|
||||
fmt.Fprintf(out, "Creating %s\n", o.name)
|
||||
|
||||
switch o.chartAPIVersion {
|
||||
case chart.APIVersionV2, "":
|
||||
return o.createV2Chart(out)
|
||||
case chartv3.APIVersionV3:
|
||||
if !gates.ChartV3.IsEnabled() {
|
||||
return gates.ChartV3.Error()
|
||||
}
|
||||
return o.createV3Chart(out)
|
||||
default:
|
||||
return fmt.Errorf("unsupported chart API version: %s (supported: v2, v3)", o.chartAPIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *createOptions) createV2Chart(out io.Writer) error {
|
||||
chartname := filepath.Base(o.name)
|
||||
cfile := &chart.Metadata{
|
||||
Name: chartname,
|
||||
|
|
@ -111,3 +135,29 @@ func (o *createOptions) run(out io.Writer) error {
|
|||
_, err := chartutil.Create(chartname, filepath.Dir(o.name))
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *createOptions) createV3Chart(out io.Writer) error {
|
||||
chartname := filepath.Base(o.name)
|
||||
cfile := &chartv3.Metadata{
|
||||
Name: chartname,
|
||||
Description: "A Helm chart for Kubernetes",
|
||||
Type: "application",
|
||||
Version: "0.1.0",
|
||||
AppVersion: "0.1.0",
|
||||
APIVersion: chartv3.APIVersionV3,
|
||||
}
|
||||
|
||||
if o.starter != "" {
|
||||
// Create from the starter
|
||||
lstarter := filepath.Join(o.starterDir, o.starter)
|
||||
// If path is absolute, we don't want to prefix it with helm starters folder
|
||||
if filepath.IsAbs(o.starter) {
|
||||
lstarter = o.starter
|
||||
}
|
||||
return chartutilv3.CreateFrom(cfile, filepath.Dir(o.name), lstarter)
|
||||
}
|
||||
|
||||
chartutilv3.Stderr = out
|
||||
_, err := chartutilv3.Create(chartname, filepath.Dir(o.name))
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
chartv3 "helm.sh/helm/v4/internal/chart/v3"
|
||||
chartutilv3 "helm.sh/helm/v4/internal/chart/v3/util"
|
||||
"helm.sh/helm/v4/internal/gates"
|
||||
"helm.sh/helm/v4/internal/test/ensure"
|
||||
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||
"helm.sh/helm/v4/pkg/chart/v2/loader"
|
||||
chart "helm.sh/helm/v4/pkg/chart"
|
||||
chartloader "helm.sh/helm/v4/pkg/chart/loader"
|
||||
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
|
||||
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
|
||||
"helm.sh/helm/v4/pkg/helmpath"
|
||||
)
|
||||
|
|
@ -46,143 +50,155 @@ func TestCreateCmd(t *testing.T) {
|
|||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
c, err := loader.LoadDir(cname)
|
||||
c, err := chartloader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, c.Name())
|
||||
acc, err := chart.NewAccessor(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV2 {
|
||||
t.Errorf("Wrong API version: %q", c.Metadata.APIVersion)
|
||||
|
||||
if acc.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, acc.Name())
|
||||
}
|
||||
metadata := acc.MetadataAsMap()
|
||||
apiVersion, ok := metadata["APIVersion"].(string)
|
||||
if !ok {
|
||||
t.Fatal("APIVersion not found in metadata")
|
||||
}
|
||||
if apiVersion != chartv2.APIVersionV2 {
|
||||
t.Errorf("Wrong API version: %q", apiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStarterCmd(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
ensure.HelmHome(t)
|
||||
cname := "testchart"
|
||||
defer resetEnv()()
|
||||
// Create a starter.
|
||||
starterchart := helmpath.DataPath("starters")
|
||||
os.MkdirAll(starterchart, 0o755)
|
||||
if dest, err := chartutil.Create("starterchart", starterchart); err != nil {
|
||||
t.Fatalf("Could not create chart: %s", err)
|
||||
} else {
|
||||
t.Logf("Created %s", dest)
|
||||
}
|
||||
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
|
||||
if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("Could not write template: %s", err)
|
||||
tests := []struct {
|
||||
name string
|
||||
chartAPIVersion string
|
||||
useAbsolutePath bool
|
||||
expectedVersion string
|
||||
}{
|
||||
{
|
||||
name: "v2 with relative starter path",
|
||||
chartAPIVersion: "",
|
||||
useAbsolutePath: false,
|
||||
expectedVersion: chartv2.APIVersionV2,
|
||||
},
|
||||
{
|
||||
name: "v2 with absolute starter path",
|
||||
chartAPIVersion: "",
|
||||
useAbsolutePath: true,
|
||||
expectedVersion: chartv2.APIVersionV2,
|
||||
},
|
||||
{
|
||||
name: "v3 with relative starter path",
|
||||
chartAPIVersion: "v3",
|
||||
useAbsolutePath: false,
|
||||
expectedVersion: chartv3.APIVersionV3,
|
||||
},
|
||||
}
|
||||
|
||||
// Run a create
|
||||
if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=starterchart %s", cname)); err != nil {
|
||||
t.Errorf("Failed to run create: %s", err)
|
||||
return
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
ensure.HelmHome(t)
|
||||
defer resetEnv()()
|
||||
|
||||
// Test that the chart is there
|
||||
if fi, err := os.Stat(cname); err != nil {
|
||||
t.Fatalf("no chart directory: %s", err)
|
||||
} else if !fi.IsDir() {
|
||||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
c, err := loader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, c.Name())
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV2 {
|
||||
t.Errorf("Wrong API version: %q", c.Metadata.APIVersion)
|
||||
}
|
||||
|
||||
expectedNumberOfTemplates := 10
|
||||
if l := len(c.Templates); l != expectedNumberOfTemplates {
|
||||
t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, tpl := range c.Templates {
|
||||
if tpl.Name == "templates/foo.tpl" {
|
||||
found = true
|
||||
if data := string(tpl.Data); data != "test" {
|
||||
t.Errorf("Expected template 'test', got %q", data)
|
||||
// Enable feature gate for v3 charts
|
||||
if tt.chartAPIVersion == "v3" {
|
||||
t.Setenv(string(gates.ChartV3), "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Did not find foo.tpl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateStarterAbsoluteCmd(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
defer resetEnv()()
|
||||
ensure.HelmHome(t)
|
||||
cname := "testchart"
|
||||
cname := "testchart"
|
||||
|
||||
// Create a starter.
|
||||
starterchart := helmpath.DataPath("starters")
|
||||
os.MkdirAll(starterchart, 0o755)
|
||||
if dest, err := chartutil.Create("starterchart", starterchart); err != nil {
|
||||
t.Fatalf("Could not create chart: %s", err)
|
||||
} else {
|
||||
t.Logf("Created %s", dest)
|
||||
}
|
||||
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
|
||||
if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("Could not write template: %s", err)
|
||||
}
|
||||
|
||||
starterChartPath := filepath.Join(starterchart, "starterchart")
|
||||
|
||||
// Run a create
|
||||
if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=%s %s", starterChartPath, cname)); err != nil {
|
||||
t.Errorf("Failed to run create: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test that the chart is there
|
||||
if fi, err := os.Stat(cname); err != nil {
|
||||
t.Fatalf("no chart directory: %s", err)
|
||||
} else if !fi.IsDir() {
|
||||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
c, err := loader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, c.Name())
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV2 {
|
||||
t.Errorf("Wrong API version: %q", c.Metadata.APIVersion)
|
||||
}
|
||||
|
||||
expectedNumberOfTemplates := 10
|
||||
if l := len(c.Templates); l != expectedNumberOfTemplates {
|
||||
t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, tpl := range c.Templates {
|
||||
if tpl.Name == "templates/foo.tpl" {
|
||||
found = true
|
||||
if data := string(tpl.Data); data != "test" {
|
||||
t.Errorf("Expected template 'test', got %q", data)
|
||||
// Create a starter using the appropriate chartutil
|
||||
starterchart := helmpath.DataPath("starters")
|
||||
os.MkdirAll(starterchart, 0o755)
|
||||
var err error
|
||||
var dest string
|
||||
if tt.chartAPIVersion == "v3" {
|
||||
dest, err = chartutilv3.Create("starterchart", starterchart)
|
||||
} else {
|
||||
dest, err = chartutil.Create("starterchart", starterchart)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Did not find foo.tpl")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create chart: %s", err)
|
||||
}
|
||||
t.Logf("Created %s", dest)
|
||||
|
||||
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
|
||||
if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("Could not write template: %s", err)
|
||||
}
|
||||
|
||||
// Build the command
|
||||
starterArg := "starterchart"
|
||||
if tt.useAbsolutePath {
|
||||
starterArg = filepath.Join(starterchart, "starterchart")
|
||||
}
|
||||
cmd := fmt.Sprintf("create --starter=%s", starterArg)
|
||||
if tt.chartAPIVersion == "v3" {
|
||||
cmd += fmt.Sprintf(" --chart-api-version=%s", chartv3.APIVersionV3)
|
||||
} else {
|
||||
cmd += fmt.Sprintf(" --chart-api-version=%s", chartv2.APIVersionV2)
|
||||
}
|
||||
cmd += " " + cname
|
||||
|
||||
// Run create
|
||||
if _, _, err := executeActionCommand(cmd); err != nil {
|
||||
t.Fatalf("Failed to run create: %s", err)
|
||||
}
|
||||
|
||||
// Test that the chart is there
|
||||
if fi, err := os.Stat(cname); err != nil {
|
||||
t.Fatalf("no chart directory: %s", err)
|
||||
} else if !fi.IsDir() {
|
||||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
// Load and verify the chart
|
||||
c, err := chartloader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
acc, err := chart.NewAccessor(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chartName := acc.Name()
|
||||
metadata := acc.MetadataAsMap()
|
||||
apiVersion, ok := metadata["APIVersion"].(string)
|
||||
if !ok {
|
||||
t.Fatal("APIVersion not found in metadata")
|
||||
}
|
||||
var templates []string
|
||||
for _, tpl := range acc.Templates() {
|
||||
templates = append(templates, tpl.Name)
|
||||
}
|
||||
|
||||
if chartName != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, chartName)
|
||||
}
|
||||
if apiVersion != tt.expectedVersion {
|
||||
t.Errorf("Wrong API version: expected %q, got %q", tt.expectedVersion, apiVersion)
|
||||
}
|
||||
|
||||
// Verify custom template exists
|
||||
found := false
|
||||
for _, name := range templates {
|
||||
if name == "templates/foo.tpl" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Did not find foo.tpl")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,3 +206,101 @@ func TestCreateFileCompletion(t *testing.T) {
|
|||
checkFileCompletion(t, "create", true)
|
||||
checkFileCompletion(t, "create myname", false)
|
||||
}
|
||||
|
||||
func TestCreateCmdChartAPIVersionV2(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
ensure.HelmHome(t)
|
||||
cname := "testchart"
|
||||
|
||||
// Run a create with explicit v2
|
||||
if _, _, err := executeActionCommand("create --chart-api-version=v2 " + cname); err != nil {
|
||||
t.Fatalf("Failed to run create: %s", err)
|
||||
}
|
||||
|
||||
// Test that the chart is there
|
||||
if fi, err := os.Stat(cname); err != nil {
|
||||
t.Fatalf("no chart directory: %s", err)
|
||||
} else if !fi.IsDir() {
|
||||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
c, err := chartloader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
acc, err := chart.NewAccessor(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if acc.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, acc.Name())
|
||||
}
|
||||
metadata := acc.MetadataAsMap()
|
||||
apiVersion, ok := metadata["APIVersion"].(string)
|
||||
if !ok {
|
||||
t.Fatal("APIVersion not found in metadata")
|
||||
}
|
||||
if apiVersion != chartv2.APIVersionV2 {
|
||||
t.Errorf("Wrong API version: expected %q, got %q", chartv2.APIVersionV2, apiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCmdChartAPIVersionV3(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
ensure.HelmHome(t)
|
||||
t.Setenv(string(gates.ChartV3), "1")
|
||||
cname := "testchart"
|
||||
|
||||
// Run a create with v3
|
||||
if _, _, err := executeActionCommand("create --chart-api-version=v3 " + cname); err != nil {
|
||||
t.Fatalf("Failed to run create: %s", err)
|
||||
}
|
||||
|
||||
// Test that the chart is there
|
||||
if fi, err := os.Stat(cname); err != nil {
|
||||
t.Fatalf("no chart directory: %s", err)
|
||||
} else if !fi.IsDir() {
|
||||
t.Fatalf("chart is not directory")
|
||||
}
|
||||
|
||||
c, err := chartloader.LoadDir(cname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
acc, err := chart.NewAccessor(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if acc.Name() != cname {
|
||||
t.Errorf("Expected %q name, got %q", cname, acc.Name())
|
||||
}
|
||||
metadata := acc.MetadataAsMap()
|
||||
apiVersion, ok := metadata["APIVersion"].(string)
|
||||
if !ok {
|
||||
t.Fatal("APIVersion not found in metadata")
|
||||
}
|
||||
if apiVersion != chartv3.APIVersionV3 {
|
||||
t.Errorf("Wrong API version: expected %q, got %q", chartv3.APIVersionV3, apiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCmdInvalidChartAPIVersion(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
ensure.HelmHome(t)
|
||||
cname := "testchart"
|
||||
|
||||
// Run a create with invalid version
|
||||
_, _, err := executeActionCommand("create --chart-api-version=v1 " + cname)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid API version, got nil")
|
||||
}
|
||||
|
||||
expectedErr := "unsupported chart API version: v1 (supported: v2, v3)"
|
||||
if err.Error() != expectedErr {
|
||||
t.Errorf("Expected error %q, got %q", expectedErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue