Merge pull request #118013 from neolit123/1.28-add-config-validate

kubeadm: add the "config validate" subcommand
This commit is contained in:
Kubernetes Prow Robot 2023-05-24 20:36:49 -07:00 committed by GitHub
commit 90ed8ba687
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 292 additions and 28 deletions

View File

@ -76,6 +76,7 @@ func newCmdConfig(out io.Writer) *cobra.Command {
kubeConfigFile = cmdutil.GetKubeConfigPath(kubeConfigFile)
cmd.AddCommand(newCmdConfigPrint(out))
cmd.AddCommand(newCmdConfigMigrate(out))
cmd.AddCommand(newCmdConfigValidate(out))
cmd.AddCommand(newCmdConfigImages(out))
return cmd
}
@ -272,6 +273,46 @@ func newCmdConfigMigrate(out io.Writer) *cobra.Command {
return cmd
}
// newCmdConfigValidate returns cobra.Command for the "kubeadm config validate" command
func newCmdConfigValidate(out io.Writer) *cobra.Command {
var cfgPath string
cmd := &cobra.Command{
Use: "validate",
Short: "Read a file containing the kubeadm configuration API and report any validation problems",
Long: fmt.Sprintf(dedent.Dedent(`
This command lets you validate a kubeadm configuration API file and report any warnings and errors.
If there are no errors the exit status will be zero, otherwise it will be non-zero.
Any unmarshaling problems such as unknown API fields will trigger errors. Unknown API versions and
fields with invalid values will also trigger errors. Any other errors or warnings may be reported
depending on contents of the input file.
In this version of kubeadm, the following API versions are supported:
- %s
`), kubeadmapiv1.SchemeGroupVersion),
RunE: func(cmd *cobra.Command, args []string) error {
if len(cfgPath) == 0 {
return errors.Errorf("the --%s flag is mandatory", options.CfgPath)
}
cfgBytes, err := os.ReadFile(cfgPath)
if err != nil {
return err
}
if err := configutil.ValidateConfig(cfgBytes); err != nil {
return err
}
fmt.Fprintln(out, "ok")
return nil
},
Args: cobra.NoArgs,
}
options.AddConfigFlag(cmd.Flags(), &cfgPath)
return cmd
}
// newCmdConfigImages returns the "kubeadm config images" command
func newCmdConfigImages(out io.Writer) *cobra.Command {
cmd := &cobra.Command{

View File

@ -52,6 +52,44 @@ var (
// kubeadm lookup dl.k8s.io to resolve what the latest stable release is
dummyKubernetesVersion = constants.MinimumControlPlaneVersion
dummyKubernetesVersionStr = dummyKubernetesVersion.String()
// predefined configuration contents for migration and validation
cfgInvalidSubdomain = []byte(dedent.Dedent(fmt.Sprintf(`
apiVersion: %s
kind: InitConfiguration
nodeRegistration:
criSocket: %s
name: foo bar # not a valid subdomain
`, kubeadmapiv1.SchemeGroupVersion.String(), constants.UnknownCRISocket)))
cfgUnknownAPI = []byte(dedent.Dedent(fmt.Sprintf(`
apiVersion: foo/bar # not a valid GroupVersion
kind: zzz # not a valid Kind
nodeRegistration:
criSocket: %s
`, constants.UnknownCRISocket)))
cfgLegacyAPI = []byte(dedent.Dedent(fmt.Sprintf(`
apiVersion: kubeadm.k8s.io/v1beta1 # legacy API
kind: InitConfiguration
nodeRegistration:
criSocket: %s
`, constants.UnknownCRISocket)))
cfgUnknownField = []byte(dedent.Dedent(fmt.Sprintf(`
apiVersion: %s
kind: InitConfiguration
foo: bar
nodeRegistration:
criSocket: %s
`, kubeadmapiv1.SchemeGroupVersion.String(), constants.UnknownCRISocket)))
cfgValid = []byte(dedent.Dedent(fmt.Sprintf(`
apiVersion: %s
kind: InitConfiguration
nodeRegistration:
criSocket: %s
`, kubeadmapiv1.SchemeGroupVersion.String(), constants.UnknownCRISocket)))
)
func TestNewCmdConfigImagesList(t *testing.T) {
@ -391,30 +429,132 @@ func TestImagesPull(t *testing.T) {
}
func TestMigrate(t *testing.T) {
cfg := []byte(dedent.Dedent(fmt.Sprintf(`
# This is intentionally testing an old API version. Sometimes this may be the latest version (if no old configs are supported).
apiVersion: %s
kind: InitConfiguration
nodeRegistration:
criSocket: %s
`, kubeadmapiv1.SchemeGroupVersion.String(), constants.UnknownCRISocket)))
configFile, cleanup := tempConfig(t, cfg)
cfgFileInvalidSubdomain, cleanup := tempConfig(t, cfgInvalidSubdomain)
defer cleanup()
cfgFileUnknownAPI, cleanup := tempConfig(t, cfgUnknownAPI)
defer cleanup()
cfgFileLegacyAPI, cleanup := tempConfig(t, cfgLegacyAPI)
defer cleanup()
cfgFileUnknownField, cleanup := tempConfig(t, cfgUnknownField)
defer cleanup()
cfgFileValid, cleanup := tempConfig(t, cfgValid)
defer cleanup()
var output bytes.Buffer
command := newCmdConfigMigrate(&output)
if err := command.Flags().Set("old-config", configFile); err != nil {
t.Fatalf("failed to set old-config flag")
testcases := []struct {
name string
cfg string
expectedError bool
}{
{
name: "invalid subdomain",
cfg: cfgFileInvalidSubdomain,
expectedError: true,
},
{
name: "unknown API GVK",
cfg: cfgFileUnknownAPI,
expectedError: true,
},
{
name: "legacy API GVK",
cfg: cfgFileLegacyAPI,
expectedError: true,
},
{
name: "unknown field",
cfg: cfgFileUnknownField,
expectedError: true,
},
{
name: "valid",
cfg: cfgFileValid,
expectedError: false,
},
}
newConfigPath := filepath.Join(filepath.Dir(configFile), "new-migrated-config")
if err := command.Flags().Set("new-config", newConfigPath); err != nil {
t.Fatalf("failed to set new-config flag")
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var output bytes.Buffer
command := newCmdConfigMigrate(&output)
if err := command.Flags().Set("old-config", tc.cfg); err != nil {
t.Fatalf("failed to set old-config flag")
}
newConfigPath := filepath.Join(filepath.Dir(tc.cfg), "new-migrated-config")
if err := command.Flags().Set("new-config", newConfigPath); err != nil {
t.Fatalf("failed to set new-config flag")
}
err := command.RunE(nil, nil)
if (err != nil) != tc.expectedError {
t.Fatalf("Expected error from validate command: %v, got: %v, error: %v",
tc.expectedError, err != nil, err)
}
if err != nil {
return
}
if _, err := configutil.LoadInitConfigurationFromFile(newConfigPath); err != nil {
t.Fatalf("Could not read output back into internal type: %v", err)
}
})
}
if err := command.RunE(nil, nil); err != nil {
t.Fatalf("Error from running the migrate command: %v", err)
}
func TestValidate(t *testing.T) {
cfgFileInvalidSubdomain, cleanup := tempConfig(t, cfgInvalidSubdomain)
defer cleanup()
cfgFileUnknownAPI, cleanup := tempConfig(t, cfgUnknownAPI)
defer cleanup()
cfgFileLegacyAPI, cleanup := tempConfig(t, cfgLegacyAPI)
defer cleanup()
cfgFileUnknownField, cleanup := tempConfig(t, cfgUnknownField)
defer cleanup()
cfgFileValid, cleanup := tempConfig(t, cfgValid)
defer cleanup()
testcases := []struct {
name string
cfg string
expectedError bool
}{
{
name: "invalid subdomain",
cfg: cfgFileInvalidSubdomain,
expectedError: true,
},
{
name: "unknown API GVK",
cfg: cfgFileUnknownAPI,
expectedError: true,
},
{
name: "legacy API GVK",
cfg: cfgFileLegacyAPI,
expectedError: true,
},
{
name: "unknown field",
cfg: cfgFileUnknownField,
expectedError: true,
},
{
name: "valid",
cfg: cfgFileValid,
expectedError: false,
},
}
if _, err := configutil.LoadInitConfigurationFromFile(newConfigPath); err != nil {
t.Fatalf("Could not read output back into internal type: %v", err)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var output bytes.Buffer
command := newCmdConfigValidate(&output)
if err := command.Flags().Set("config", tc.cfg); err != nil {
t.Fatalf("Failed to set config flag")
}
if err := command.RunE(nil, nil); (err != nil) != tc.expectedError {
t.Fatalf("Expected error from validate command: %v, got: %v, error: %v",
tc.expectedError, err != nil, err)
}
})
}
}

View File

@ -37,6 +37,7 @@ import (
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
"k8s.io/kubernetes/cmd/kubeadm/app/componentconfigs"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
)
@ -190,8 +191,44 @@ func ChooseAPIServerBindAddress(bindAddress net.IP) (net.IP, error) {
return ip, nil
}
// validateKnownGVKs takes a list of GVKs and verifies if they are known in kubeadm or component config schemes
func validateKnownGVKs(gvks []schema.GroupVersionKind) error {
var unknown []schema.GroupVersionKind
schemes := []*runtime.Scheme{
kubeadmscheme.Scheme,
componentconfigs.Scheme,
}
for _, gvk := range gvks {
var scheme *runtime.Scheme
// Skip legacy known GVs so that they don't return errors.
// This makes the function return errors only for GVs that where never known.
if err := validateSupportedVersion(gvk.GroupVersion(), true); err != nil {
continue
}
for _, s := range schemes {
if _, err := s.New(gvk); err == nil {
scheme = s
break
}
}
if scheme == nil {
unknown = append(unknown, gvk)
}
}
if len(unknown) > 0 {
return errors.Errorf("unknown configuration APIs: %#v", unknown)
}
return nil
}
// MigrateOldConfig migrates an old configuration from a byte slice into a new one (returned again as a byte slice).
// Only kubeadm kinds are migrated. Others are silently ignored.
// Only kubeadm kinds are migrated.
func MigrateOldConfig(oldConfig []byte) ([]byte, error) {
newConfig := [][]byte{}
@ -205,9 +242,13 @@ func MigrateOldConfig(oldConfig []byte) ([]byte, error) {
gvks = append(gvks, gvk)
}
if err := validateKnownGVKs(gvks); err != nil {
return []byte{}, err
}
// Migrate InitConfiguration and ClusterConfiguration if there are any in the config
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) || kubeadmutil.GroupVersionKindsHasClusterConfiguration(gvks...) {
o, err := documentMapToInitConfiguration(gvkmap, true)
o, err := documentMapToInitConfiguration(gvkmap, true, true)
if err != nil {
return []byte{}, err
}
@ -220,7 +261,7 @@ func MigrateOldConfig(oldConfig []byte) ([]byte, error) {
// Migrate JoinConfiguration if there is any
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
o, err := documentMapToJoinConfiguration(gvkmap, true)
o, err := documentMapToJoinConfiguration(gvkmap, true, true)
if err != nil {
return []byte{}, err
}
@ -234,6 +275,40 @@ func MigrateOldConfig(oldConfig []byte) ([]byte, error) {
return bytes.Join(newConfig, []byte(constants.YAMLDocumentSeparator)), nil
}
// ValidateConfig takes a byte slice containing a kubeadm configuration and performs conversion
// to internal types and validation.
func ValidateConfig(oldConfig []byte) error {
gvkmap, err := kubeadmutil.SplitYAMLDocuments(oldConfig)
if err != nil {
return err
}
gvks := []schema.GroupVersionKind{}
for gvk := range gvkmap {
gvks = append(gvks, gvk)
}
if err := validateKnownGVKs(gvks); err != nil {
return err
}
// Validate InitConfiguration and ClusterConfiguration if there are any in the config
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) || kubeadmutil.GroupVersionKindsHasClusterConfiguration(gvks...) {
if _, err := documentMapToInitConfiguration(gvkmap, true, true); err != nil {
return err
}
}
// Validate JoinConfiguration if there is any
if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
if _, err := documentMapToJoinConfiguration(gvkmap, true, true); err != nil {
return err
}
}
return nil
}
// isKubeadmPrereleaseVersion returns true if the kubeadm version is a pre-release version and
// the minimum control plane version is N+2 MINOR version of the given k8sVersion.
func isKubeadmPrereleaseVersion(versionInfo *apimachineryversion.Info, k8sVersion, mcpVersion *version.Version) bool {

View File

@ -287,11 +287,11 @@ func BytesToInitConfiguration(b []byte) (*kubeadmapi.InitConfiguration, error) {
return nil, err
}
return documentMapToInitConfiguration(gvkmap, false)
return documentMapToInitConfiguration(gvkmap, false, false)
}
// documentMapToInitConfiguration converts a map of GVKs and YAML documents to defaulted and validated configuration object.
func documentMapToInitConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecated bool) (*kubeadmapi.InitConfiguration, error) {
func documentMapToInitConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecated, strictErrors bool) (*kubeadmapi.InitConfiguration, error) {
var initcfg *kubeadmapi.InitConfiguration
var clustercfg *kubeadmapi.ClusterConfiguration
@ -303,7 +303,11 @@ func documentMapToInitConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecat
// verify the validity of the YAML
if err := strict.VerifyUnmarshalStrict([]*runtime.Scheme{kubeadmscheme.Scheme, componentconfigs.Scheme}, gvk, fileContent); err != nil {
klog.Warning(err.Error())
if !strictErrors {
klog.Warning(err.Error())
} else {
return nil, err
}
}
if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvk) {

View File

@ -85,12 +85,12 @@ func LoadJoinConfigurationFromFile(cfgPath string) (*kubeadmapi.JoinConfiguratio
return nil, err
}
return documentMapToJoinConfiguration(gvkmap, false)
return documentMapToJoinConfiguration(gvkmap, false, false)
}
// documentMapToJoinConfiguration takes a map between GVKs and YAML documents (as returned by SplitYAMLDocuments),
// finds a JoinConfiguration, decodes it, dynamically defaults it and then validates it prior to return.
func documentMapToJoinConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecated bool) (*kubeadmapi.JoinConfiguration, error) {
func documentMapToJoinConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecated, strictErrors bool) (*kubeadmapi.JoinConfiguration, error) {
joinBytes := []byte{}
for gvk, bytes := range gvkmap {
// not interested in anything other than JoinConfiguration
@ -105,7 +105,11 @@ func documentMapToJoinConfiguration(gvkmap kubeadmapi.DocumentMap, allowDeprecat
// verify the validity of the YAML
if err := strict.VerifyUnmarshalStrict([]*runtime.Scheme{kubeadmscheme.Scheme}, gvk, bytes); err != nil {
klog.Warning(err.Error())
if !strictErrors {
klog.Warning(err.Error())
} else {
return nil, err
}
}
joinBytes = bytes