Align chart name validation with create

Signed-off-by: Akash Kumar <meakash7902@gmail.com>
pull/32075/head
Akash Kumar 6 months ago
parent 011a56b9e3
commit eb8fb41837

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
@ -32,10 +31,6 @@ import (
chartutil "helm.sh/helm/v4/internal/chart/v3/util" chartutil "helm.sh/helm/v4/internal/chart/v3/util"
) )
const validChartNameMessage = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
var validChartName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
// Chartfile runs a set of linter rules related to Chart.yaml file // Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) { func Chartfile(linter *support.Linter) {
chartFileName := "Chart.yaml" chartFileName := "Chart.yaml"
@ -125,8 +120,8 @@ func validateChartName(cf *chart.Metadata) error {
if name != cf.Name { if name != cf.Name {
return fmt.Errorf("chart name %q is invalid", cf.Name) return fmt.Errorf("chart name %q is invalid", cf.Name)
} }
if !validChartName.MatchString(cf.Name) { if err := chartutil.ValidateChartName(cf.Name); err != nil {
return fmt.Errorf("chart name %q is invalid; %s", cf.Name, validChartNameMessage) return fmt.Errorf("chart name %q is invalid; %w", cf.Name, err)
} }
return nil return nil
} }

@ -64,6 +64,8 @@ func TestValidateChartYamlFormat(t *testing.T) {
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {
const chartNameRule = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
tests := []struct { tests := []struct {
name string name string
wantErr string wantErr string
@ -78,7 +80,7 @@ func TestValidateChartName(t *testing.T) {
}, },
{ {
name: "BadChart", name: "BadChart",
wantErr: `chart name "BadChart" is invalid`, wantErr: `chart name "BadChart" is invalid; ` + chartNameRule,
}, },
{ {
name: "bad.chart", name: "bad.chart",

@ -17,6 +17,7 @@ limitations under the License.
package util package util
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -32,10 +33,9 @@ import (
) )
// chartName is a regular expression for testing the supplied name of a chart. // chartName is a regular expression for testing the supplied name of a chart.
// This regular expression is probably stricter than it needs to be. We can relax it var chartName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be
// problematic. const validChartNameMessage = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
const ( const (
// ChartfileName is the default Chart file name. // ChartfileName is the default Chart file name.
@ -650,6 +650,10 @@ var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart. // CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error { func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
if err := ValidateChartName(chartfile.Name); err != nil {
return err
}
schart, err := loader.Load(src) schart, err := loader.Load(src)
if err != nil { if err != nil {
return fmt.Errorf("could not load %s: %w", src, err) return fmt.Errorf("could not load %s: %w", src, err)
@ -704,7 +708,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
func Create(name, dir string) (string, error) { func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems. // Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil { if err := ValidateChartName(name); err != nil {
return "", err return "", err
} }
@ -823,12 +827,13 @@ func writeFile(name string, content []byte) error {
return os.WriteFile(name, content, 0644) return os.WriteFile(name, content, 0644)
} }
func validateChartName(name string) error { // ValidateChartName validates a chart name used by chart creation and linting.
func ValidateChartName(name string) error {
if name == "" || len(name) > maxChartNameLength { if name == "" || len(name) > maxChartNameLength {
return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength)
} }
if !chartName.MatchString(name) { if !chartName.MatchString(name) {
return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) return errors.New(validChartNameMessage)
} }
return nil return nil
} }

@ -107,6 +107,13 @@ func TestCreateFrom(t *testing.T) {
t.Errorf("File %s contains <CHARTNAME>", f) t.Errorf("File %s contains <CHARTNAME>", f)
} }
} }
cf.Name = "Bad.Chart"
if err := CreateFrom(cf, t.TempDir(), srcdir); err == nil {
t.Fatal("CreateFrom with invalid chart name returned no error")
} else if err.Error() != validChartNameMessage {
t.Fatalf("CreateFrom returned %q, want %q", err, validChartNameMessage)
}
} }
// TestCreate_Overwrite is a regression test for making sure that files are overwritten. // TestCreate_Overwrite is a regression test for making sure that files are overwritten.
@ -145,16 +152,61 @@ func TestCreate_Overwrite(t *testing.T) {
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {
for name, shouldPass := range map[string]bool{ tests := []struct {
"": false, name string
"abcdefghijklmnopqrstuvwxyz-_.": true, wantErr string
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, }{
"$hello": false, {
"Hellô": false, name: "",
"he%%o": false, wantErr: "chart name must be between 1 and 250 characters",
"he\nllo": false, },
{
"abcdefghijklmnopqrstuvwxyz-_." + name: "abcdefghijklmnopqrstuvwxyz",
},
{
name: "bad-chart-1",
},
{
name: "bad--chart",
},
{
name: "BadChart",
wantErr: validChartNameMessage,
},
{
name: "bad.chart",
wantErr: validChartNameMessage,
},
{
name: "bad_chart",
wantErr: validChartNameMessage,
},
{
name: "-badchart",
wantErr: validChartNameMessage,
},
{
name: "badchart-",
wantErr: validChartNameMessage,
},
{
name: "$hello",
wantErr: validChartNameMessage,
},
{
name: "Hellô",
wantErr: validChartNameMessage,
},
{
name: "he%%o",
wantErr: validChartNameMessage,
},
{
name: "he\nllo",
wantErr: validChartNameMessage,
},
{
name: "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
@ -163,10 +215,26 @@ func TestValidateChartName(t *testing.T) {
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.",
} { wantErr: "chart name must be between 1 and 250 characters",
if err := validateChartName(name); (err != nil) == shouldPass { },
t.Errorf("test for %q failed", name) }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateChartName(tt.name)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("ValidateChartName(%q) returned unexpected error: %s", tt.name, err)
}
return
}
if err == nil {
t.Fatalf("ValidateChartName(%q) returned no error, want %q", tt.name, tt.wantErr)
}
if err.Error() != tt.wantErr {
t.Fatalf("ValidateChartName(%q) returned %q, want %q", tt.name, err, tt.wantErr)
} }
})
} }
} }

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
@ -32,10 +31,6 @@ import (
chartutil "helm.sh/helm/v4/pkg/chart/v2/util" chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
) )
const validChartNameMessage = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
var validChartName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
// Chartfile runs a set of linter rules related to Chart.yaml file // Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) { func Chartfile(linter *support.Linter) {
chartFileName := "Chart.yaml" chartFileName := "Chart.yaml"
@ -126,8 +121,8 @@ func validateChartName(cf *chart.Metadata) error {
if name != cf.Name { if name != cf.Name {
return fmt.Errorf("chart name %q is invalid", cf.Name) return fmt.Errorf("chart name %q is invalid", cf.Name)
} }
if !validChartName.MatchString(cf.Name) { if err := chartutil.ValidateChartName(cf.Name); err != nil {
return fmt.Errorf("chart name %q is invalid; %s", cf.Name, validChartNameMessage) return fmt.Errorf("chart name %q is invalid; %w", cf.Name, err)
} }
return nil return nil
} }

@ -64,6 +64,8 @@ func TestValidateChartYamlFormat(t *testing.T) {
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {
const chartNameRule = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
tests := []struct { tests := []struct {
name string name string
wantErr string wantErr string
@ -78,7 +80,7 @@ func TestValidateChartName(t *testing.T) {
}, },
{ {
name: "BadChart", name: "BadChart",
wantErr: `chart name "BadChart" is invalid`, wantErr: `chart name "BadChart" is invalid; ` + chartNameRule,
}, },
{ {
name: "bad.chart", name: "bad.chart",

@ -17,6 +17,7 @@ limitations under the License.
package util package util
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -32,10 +33,9 @@ import (
) )
// chartName is a regular expression for testing the supplied name of a chart. // chartName is a regular expression for testing the supplied name of a chart.
// This regular expression is probably stricter than it needs to be. We can relax it var chartName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be
// problematic. const validChartNameMessage = "chart names must contain lowercase letters, numbers, and dashes, and must start and end with a lowercase letter or number"
var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
const ( const (
// ChartfileName is the default Chart file name. // ChartfileName is the default Chart file name.
@ -649,6 +649,10 @@ var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart. // CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error { func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
if err := ValidateChartName(chartfile.Name); err != nil {
return err
}
schart, err := loader.Load(src) schart, err := loader.Load(src)
if err != nil { if err != nil {
return fmt.Errorf("could not load %s: %w", src, err) return fmt.Errorf("could not load %s: %w", src, err)
@ -703,7 +707,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
func Create(name, dir string) (string, error) { func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems. // Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil { if err := ValidateChartName(name); err != nil {
return "", err return "", err
} }
@ -822,12 +826,13 @@ func writeFile(name string, content []byte) error {
return os.WriteFile(name, content, 0644) return os.WriteFile(name, content, 0644)
} }
func validateChartName(name string) error { // ValidateChartName validates a chart name used by chart creation and linting.
func ValidateChartName(name string) error {
if name == "" || len(name) > maxChartNameLength { if name == "" || len(name) > maxChartNameLength {
return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength)
} }
if !chartName.MatchString(name) { if !chartName.MatchString(name) {
return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) return errors.New(validChartNameMessage)
} }
return nil return nil
} }

@ -107,6 +107,13 @@ func TestCreateFrom(t *testing.T) {
t.Errorf("File %s contains <CHARTNAME>", f) t.Errorf("File %s contains <CHARTNAME>", f)
} }
} }
cf.Name = "Bad.Chart"
if err := CreateFrom(cf, t.TempDir(), srcdir); err == nil {
t.Fatal("CreateFrom with invalid chart name returned no error")
} else if err.Error() != validChartNameMessage {
t.Fatalf("CreateFrom returned %q, want %q", err, validChartNameMessage)
}
} }
// TestCreate_Overwrite is a regression test for making sure that files are overwritten. // TestCreate_Overwrite is a regression test for making sure that files are overwritten.
@ -145,16 +152,61 @@ func TestCreate_Overwrite(t *testing.T) {
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {
for name, shouldPass := range map[string]bool{ tests := []struct {
"": false, name string
"abcdefghijklmnopqrstuvwxyz-_.": true, wantErr string
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, }{
"$hello": false, {
"Hellô": false, name: "",
"he%%o": false, wantErr: "chart name must be between 1 and 250 characters",
"he\nllo": false, },
{
"abcdefghijklmnopqrstuvwxyz-_." + name: "abcdefghijklmnopqrstuvwxyz",
},
{
name: "bad-chart-1",
},
{
name: "bad--chart",
},
{
name: "BadChart",
wantErr: validChartNameMessage,
},
{
name: "bad.chart",
wantErr: validChartNameMessage,
},
{
name: "bad_chart",
wantErr: validChartNameMessage,
},
{
name: "-badchart",
wantErr: validChartNameMessage,
},
{
name: "badchart-",
wantErr: validChartNameMessage,
},
{
name: "$hello",
wantErr: validChartNameMessage,
},
{
name: "Hellô",
wantErr: validChartNameMessage,
},
{
name: "he%%o",
wantErr: validChartNameMessage,
},
{
name: "he\nllo",
wantErr: validChartNameMessage,
},
{
name: "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
@ -163,10 +215,26 @@ func TestValidateChartName(t *testing.T) {
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." + "abcdefghijklmnopqrstuvwxyz-_." +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.",
} { wantErr: "chart name must be between 1 and 250 characters",
if err := validateChartName(name); (err != nil) == shouldPass { },
t.Errorf("test for %q failed", name) }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateChartName(tt.name)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("ValidateChartName(%q) returned unexpected error: %s", tt.name, err)
}
return
}
if err == nil {
t.Fatalf("ValidateChartName(%q) returned no error, want %q", tt.name, tt.wantErr)
}
if err.Error() != tt.wantErr {
t.Fatalf("ValidateChartName(%q) returned %q, want %q", tt.name, err, tt.wantErr)
} }
})
} }
} }

Loading…
Cancel
Save