mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-08 03:33:56 +00:00
Merge pull request #118013 from neolit123/1.28-add-config-validate
kubeadm: add the "config validate" subcommand
This commit is contained in:
commit
90ed8ba687
@ -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{
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user