diff --git a/go.mod b/go.mod index 1ecb9eb2437..6bba0c9b43c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module sigs.k8s.io/kubebuilder go 1.13 require ( + github.com/blang/semver v3.5.1+incompatible github.com/gobuffalo/flect v0.1.5 github.com/golang/protobuf v1.3.1 // indirect github.com/kr/pretty v0.1.0 // indirect diff --git a/go.sum b/go.sum index 1c89700c818..bc9a1010682 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index b466b88812e..7355d0844e3 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" internalconfig "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/internal/validation" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/plugin" ) @@ -146,6 +147,12 @@ func (c *cli) initialize() error { return fmt.Errorf("failed to read config: %v", err) } + // Validate after setting projectVersion but before buildRootCmd so we error + // out before an error resulting from an incorrect cli is returned downstream. + if err = c.validate(); err != nil { + return err + } + c.cmd = c.buildRootCmd() // Add extra commands injected by options. @@ -175,6 +182,37 @@ func (c *cli) initialize() error { return nil } +// validate validates fields in a cli. +func (c cli) validate() error { + // Validate project versions. + if err := validation.ValidateProjectVersion(c.defaultProjectVersion); err != nil { + return fmt.Errorf("failed to validate default project version %q: %v", c.defaultProjectVersion, err) + } + if err := validation.ValidateProjectVersion(c.projectVersion); err != nil { + return fmt.Errorf("failed to validate project version %q: %v", c.projectVersion, err) + } + + // Validate plugin versions and name. + for _, versionedPlugins := range c.plugins { + for _, versionedPlugin := range versionedPlugins { + pluginName := versionedPlugin.Name() + pluginVersion := versionedPlugin.Version() + if err := plugin.ValidateVersion(pluginVersion); err != nil { + return fmt.Errorf("failed to validate plugin %q version %q: %v", + pluginName, pluginVersion, err) + } + for _, projectVersion := range versionedPlugin.SupportedProjectVersions() { + if err := validation.ValidateProjectVersion(projectVersion); err != nil { + return fmt.Errorf("failed to validate plugin %q supported project version %q: %v", + pluginName, projectVersion, err) + } + } + } + } + + return nil +} + // buildRootCmd returns a root command with a subcommand tree reflecting the // current project's state. func (c cli) buildRootCmd() *cobra.Command { diff --git a/pkg/internal/validation/dns.go b/pkg/internal/validation/dns.go new file mode 100644 index 00000000000..bad99349e09 --- /dev/null +++ b/pkg/internal/validation/dns.go @@ -0,0 +1,114 @@ +/* +Copyright 2018 The Kubernetes 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 validation + +import ( + "fmt" + "regexp" +) + +// This file's code was modified from "k8s.io/apimachinery/pkg/util/validation" +// to avoid package dependencies. In case of additional functionality from +// "k8s.io/apimachinery" is needed, re-consider whether to add the dependency. + +const ( + dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" + dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" + dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?" +) + +type dnsValidationConfig struct { + format string + maxLen int + re *regexp.Regexp + errMsg string + examples []string +} + +var dns1123LabelConfig = dnsValidationConfig{ + format: dns1123LabelFmt, + maxLen: 56, // = 63 - len("-system") + re: regexp.MustCompile("^" + dns1123LabelFmt + "$"), + errMsg: "a DNS-1123 label must consist of lower case alphanumeric characters or '-'", + examples: []string{"example.com"}, +} + +var dns1123SubdomainConfig = dnsValidationConfig{ + format: dns1123SubdomainFmt, + maxLen: 253, // a subdomain's max length in DNS (RFC 1123). + re: regexp.MustCompile("^" + dns1123SubdomainFmt + "$"), + errMsg: "a DNS-1123 subdomain must consist of lower case alphanumeric characters, " + + "'-' or '.', and must start and end with an alphanumeric character", + examples: []string{"my-name", "abc-123"}, +} + +var dns1035LabelConfig = dnsValidationConfig{ + format: dns1035LabelFmt, + maxLen: 63, // a label's max length in DNS (RFC 1035). + re: regexp.MustCompile("^" + dns1035LabelFmt + "$"), + errMsg: "a DNS-1035 label must consist of lower case alphanumeric characters or '-', " + + "start with an alphabetic character, and end with an alphanumeric character", + examples: []string{"my-name", "123-abc"}, +} + +func (c dnsValidationConfig) check(value string) (errs []string) { + if len(value) > c.maxLen { + errs = append(errs, maxLenError(c.maxLen)) + } + if !c.re.MatchString(value) { + errs = append(errs, regexError(c.errMsg, c.format, c.examples...)) + } + return errs +} + +// IsDNS1123Subdomain tests for a string that conforms to the definition of a +// subdomain in DNS (RFC 1123). +func IsDNS1123Subdomain(value string) []string { + return dns1123SubdomainConfig.check(value) +} + +// IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). +func IsDNS1123Label(value string) []string { + return dns1123LabelConfig.check(value) +} + +// IsDNS1035Label tests for a string that conforms to the definition of a label in DNS (RFC 1035). +func IsDNS1035Label(value string) []string { + return dns1035LabelConfig.check(value) +} + +// maxLenError returns a string explanation of a "string too long" validation +// failure. +func maxLenError(length int) string { + return fmt.Sprintf("must be no more than %d characters", length) +} + +// regexError returns a string explanation of a regex validation failure. +func regexError(msg string, fmt string, examples ...string) string { + if len(examples) == 0 { + return msg + " (regex used for validation is '" + fmt + "')" + } + msg += " (e.g. " + for i := range examples { + if i > 0 { + msg += " or " + } + msg += "'" + examples[i] + "', " + } + msg += "regex used for validation is '" + fmt + "')" + return msg +} diff --git a/pkg/internal/validation/project.go b/pkg/internal/validation/project.go new file mode 100644 index 00000000000..3afe3e98f21 --- /dev/null +++ b/pkg/internal/validation/project.go @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Kubernetes 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 validation + +import ( + "errors" + "regexp" +) + +// projectVersionFmt defines the project version format from a project config. +const projectVersionFmt string = "[1-9][0-9]*(-(alpha|beta))?" + +var projectVersionRe = regexp.MustCompile("^" + projectVersionFmt + "$") + +// ValidateProjectVersion ensures version adheres to the project version format. +func ValidateProjectVersion(version string) error { + if version == "" { + return errors.New("project version is empty") + } + if !projectVersionRe.MatchString(version) { + return errors.New(regexError("invalid value for project version", projectVersionFmt)) + } + return nil +} diff --git a/pkg/model/resource/options.go b/pkg/model/resource/options.go index 34ca15ab338..44e7f606c28 100644 --- a/pkg/model/resource/options.go +++ b/pkg/model/resource/options.go @@ -26,6 +26,7 @@ import ( "github.com/gobuffalo/flect" + "sigs.k8s.io/kubebuilder/pkg/internal/validation" "sigs.k8s.io/kubebuilder/pkg/model/config" ) @@ -116,7 +117,7 @@ func (opts *Options) Validate() error { } // Check if the Group has a valid DNS1123 subdomain value - if err := IsDNS1123Subdomain(opts.Group); err != nil { + if err := validation.IsDNS1123Subdomain(opts.Group); err != nil { return fmt.Errorf("group name is invalid: (%v)", err) } @@ -133,7 +134,7 @@ func (opts *Options) Validate() error { validationErrors = append(validationErrors, "Kind must start with an uppercase character") } - validationErrors = append(validationErrors, isDNS1035Label(strings.ToLower(opts.Kind))...) + validationErrors = append(validationErrors, validation.IsDNS1035Label(strings.ToLower(opts.Kind))...) if len(validationErrors) != 0 { return fmt.Errorf("Invalid Kind: %#v", validationErrors) diff --git a/pkg/model/resource/validation.go b/pkg/model/resource/validation.go deleted file mode 100644 index 1f0c01d6c76..00000000000 --- a/pkg/model/resource/validation.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2018 The Kubernetes 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 resource - -import ( - "fmt" - "regexp" -) - -// This file's code was copied from "k8s.io/apimachinery/pkg/util/validation" to -// avoid package dependencies. In case of additional functionality from -// "k8s.io/apimachinery" is needed, re-consider whether to add the dependency. - -const ( - dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" - dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" - dns1123SubdomainErrorMsg string = "a DNS-1123 subdomain must consist of lower case alphanumeric characters, " + - "'-' or '.', and must start and end with an alphanumeric character" - - // dns1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123) - dns1123SubdomainMaxLength int = 253 - - dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?" - dns1035LabelErrMsg string = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', " + - "start with an alphabetic character, and end with an alphanumeric character" - - // DNS1035LabelMaxLength is a label's max length in DNS (RFC 1035) - dns1035LabelMaxLength int = 63 -) - -var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$") -var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFmt + "$") - -// IsDNS1123Subdomain tests for a string that conforms to the definition of a -// subdomain in DNS (RFC 1123). -func IsDNS1123Subdomain(value string) []string { - var errs []string - if len(value) > dns1123SubdomainMaxLength { - errs = append(errs, maxLenError(dns1123SubdomainMaxLength)) - } - if !dns1123SubdomainRegexp.MatchString(value) { - errs = append(errs, regexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com")) - } - return errs -} - -func isDNS1035Label(value string) []string { - var errs []string - if len(value) > dns1035LabelMaxLength { - errs = append(errs, maxLenError(dns1035LabelMaxLength)) - } - if !dns1035LabelRegexp.MatchString(value) { - errs = append(errs, regexError(dns1035LabelErrMsg, dns1035LabelFmt, "my-name", "abc-123")) - } - return errs -} - -// MaxLenError returns a string explanation of a "string too long" validation -// failure. -func maxLenError(length int) string { - return fmt.Sprintf("must be no more than %d characters", length) -} - -// RegexError returns a string explanation of a regex validation failure. -func regexError(msg string, fmt string, examples ...string) string { - if len(examples) == 0 { - return msg + " (regex used for validation is '" + fmt + "')" - } - msg += " (e.g. " - for i := range examples { - if i > 0 { - msg += " or " - } - msg += "'" + examples[i] + "', " - } - msg += "regex used for validation is '" + fmt + "')" - return msg -} diff --git a/pkg/plugin/internal/validations.go b/pkg/plugin/internal/validations.go deleted file mode 100644 index 05d613f0eff..00000000000 --- a/pkg/plugin/internal/validations.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2020 The Kubernetes 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 internal - -import ( - "fmt" - "regexp" -) - -// The following code came from "k8s.io/apimachinery/pkg/util/validation/validation.go" -// If be required the usage of more funcs from this then please replace it for the import -// --------------------------------------- - -const ( - dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" - dns1123LabelMaxLength int = 56 // = 63 - len("-system") -) - -var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") - -//IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). -func IsDNS1123Label(value string) []string { - var errs []string - if len(value) > dns1123LabelMaxLength { - errs = append(errs, maxLenError(dns1123LabelMaxLength)) - } - if !dns1123LabelRegexp.MatchString(value) { - errs = append(errs, regexError("invalid value for project name", dns1123LabelFmt)) - } - return errs -} - -// regexError returns a string explanation of a regex validation failure. -func regexError(msg string, fmt string, examples ...string) string { - if len(examples) == 0 { - return msg + " (regex used for validation is '" + fmt + "')" - } - msg += " (e.g. " - for i := range examples { - if i > 0 { - msg += " or " - } - msg += "'" + examples[i] + "', " - } - msg += "regex used for validation is '" + fmt + "')" - return msg -} - -// maxLenError returns a string explanation of a "string too long" validation -// failure. -func maxLenError(length int) string { - return fmt.Sprintf("must be no more than %d characters", length) -} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 0683e9cae74..2a96379a1fb 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -32,8 +32,6 @@ type Base interface { // Version returns the plugin's semantic version, ex. "v1.2.3". // // Note: this version is different from config version. - // - // TODO: version format enforcement. Version() string // SupportedProjectVersions lists all project configuration versions this // plugin supports, ex. []string{"2", "3"}. The returned slice cannot be empty. diff --git a/pkg/plugin/v1/init.go b/pkg/plugin/v1/init.go index 84aca5a2f01..babf55b704a 100644 --- a/pkg/plugin/v1/init.go +++ b/pkg/plugin/v1/init.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/kubebuilder/internal/cmdutil" "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/internal/validation" "sigs.k8s.io/kubebuilder/pkg/plugin" "sigs.k8s.io/kubebuilder/pkg/plugin/internal" "sigs.k8s.io/kubebuilder/pkg/scaffold" @@ -141,7 +142,7 @@ func (p *initPlugin) Validate(c *config.Config) error { return fmt.Errorf("error to get the current path: %v", err) } projectName := filepath.Base(dir) - if err := internal.IsDNS1123Label(strings.ToLower(projectName)); err != nil { + if err := validation.IsDNS1123Label(strings.ToLower(projectName)); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) } diff --git a/pkg/plugin/v2/init.go b/pkg/plugin/v2/init.go index 94f9a3b159f..e5adc063742 100644 --- a/pkg/plugin/v2/init.go +++ b/pkg/plugin/v2/init.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/kubebuilder/internal/cmdutil" "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/internal/validation" "sigs.k8s.io/kubebuilder/pkg/plugin" "sigs.k8s.io/kubebuilder/pkg/plugin/internal" "sigs.k8s.io/kubebuilder/pkg/scaffold" @@ -140,7 +141,7 @@ func (p *initPlugin) Validate(c *config.Config) error { return fmt.Errorf("error to get the current path: %v", err) } projectName := filepath.Base(dir) - if err := internal.IsDNS1123Label(strings.ToLower(projectName)); err != nil { + if err := validation.IsDNS1123Label(strings.ToLower(projectName)); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) } diff --git a/pkg/plugin/validation.go b/pkg/plugin/validation.go new file mode 100644 index 00000000000..3f1a5948cd1 --- /dev/null +++ b/pkg/plugin/validation.go @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Kubernetes 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 plugin + +import ( + "errors" + "fmt" + + "github.com/blang/semver" +) + +// ValidateVersion ensures version adheres to the plugin version format, +// which is tolerant semver. +func ValidateVersion(version string) error { + if version == "" { + return errors.New("plugin version is empty") + } + // ParseTolerant allows versions with a "v" prefix or shortened versions, + // ex. "3" or "v3.0". + if _, err := semver.ParseTolerant(version); err != nil { + return fmt.Errorf("failed to validate plugin version %q: %v", version, err) + } + return nil +}