...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/config/common.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util/config

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package config contains utilities for managing the kubeadm configuration API.
    18  package config
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"net"
    24  	"reflect"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	netutil "k8s.io/apimachinery/pkg/util/net"
    32  	"k8s.io/apimachinery/pkg/util/version"
    33  	apimachineryversion "k8s.io/apimachinery/pkg/version"
    34  	componentversion "k8s.io/component-base/version"
    35  	"k8s.io/klog/v2"
    36  	netutils "k8s.io/utils/net"
    37  
    38  	kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
    39  	kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
    40  	kubeadmapiv1old "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
    41  	kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta4"
    42  	"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
    43  	"k8s.io/kubernetes/cmd/kubeadm/app/componentconfigs"
    44  	"k8s.io/kubernetes/cmd/kubeadm/app/constants"
    45  	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
    46  )
    47  
    48  // LoadOrDefaultConfigurationOptions holds the common LoadOrDefaultConfiguration options.
    49  type LoadOrDefaultConfigurationOptions struct {
    50  	// AllowExperimental indicates whether the experimental / work in progress APIs can be used.
    51  	AllowExperimental bool
    52  	// SkipCRIDetect indicates whether to skip the CRI socket detection when no CRI socket is provided.
    53  	SkipCRIDetect bool
    54  }
    55  
    56  // MarshalKubeadmConfigObject marshals an Object registered in the kubeadm scheme. If the object is a InitConfiguration or ClusterConfiguration, some extra logic is run
    57  func MarshalKubeadmConfigObject(obj runtime.Object, gv schema.GroupVersion) ([]byte, error) {
    58  	switch internalcfg := obj.(type) {
    59  	case *kubeadmapi.InitConfiguration:
    60  		return MarshalInitConfigurationToBytes(internalcfg, gv)
    61  	default:
    62  		return kubeadmutil.MarshalToYamlForCodecs(obj, gv, kubeadmscheme.Codecs)
    63  	}
    64  }
    65  
    66  // validateSupportedVersion checks if the supplied GroupVersion is not on the lists of old unsupported or deprecated GVs.
    67  // If it is, an error is returned.
    68  func validateSupportedVersion(gv schema.GroupVersion, allowDeprecated, allowExperimental bool) error {
    69  	// The support matrix will look something like this now and in the future:
    70  	// v1.10 and earlier: v1alpha1
    71  	// v1.11: v1alpha1 read-only, writes only v1alpha2 config
    72  	// v1.12: v1alpha2 read-only, writes only v1alpha3 config. Errors if the user tries to use v1alpha1
    73  	// v1.13: v1alpha3 read-only, writes only v1beta1 config. Errors if the user tries to use v1alpha1 or v1alpha2
    74  	// v1.14: v1alpha3 convert only, writes only v1beta1 config. Errors if the user tries to use v1alpha1 or v1alpha2
    75  	// v1.15: v1beta1 read-only, writes only v1beta2 config. Errors if the user tries to use v1alpha1, v1alpha2 or v1alpha3
    76  	// v1.22: v1beta2 read-only, writes only v1beta3 config. Errors if the user tries to use v1beta1 and older
    77  	// v1.27: only v1beta3 config. Errors if the user tries to use v1beta2 and older
    78  	oldKnownAPIVersions := map[string]string{
    79  		"kubeadm.k8s.io/v1alpha1": "v1.11",
    80  		"kubeadm.k8s.io/v1alpha2": "v1.12",
    81  		"kubeadm.k8s.io/v1alpha3": "v1.14",
    82  		"kubeadm.k8s.io/v1beta1":  "v1.15",
    83  		"kubeadm.k8s.io/v1beta2":  "v1.22",
    84  	}
    85  
    86  	// v1.28: v1beta4 is released as experimental
    87  	experimentalAPIVersions := map[string]string{
    88  		// TODO: https://github.com/kubernetes/kubeadm/issues/2890
    89  		// remove this from experimental once v1beta4 is released
    90  		"kubeadm.k8s.io/v1beta4": "v1.28",
    91  	}
    92  
    93  	// Deprecated API versions are supported by us, but can only be used for migration.
    94  	deprecatedAPIVersions := map[string]struct{}{}
    95  
    96  	gvString := gv.String()
    97  
    98  	if useKubeadmVersion := oldKnownAPIVersions[gvString]; useKubeadmVersion != "" {
    99  		return errors.Errorf("your configuration file uses an old API spec: %q. Please use kubeadm %s instead and run 'kubeadm config migrate --old-config old.yaml --new-config new.yaml', which will write the new, similar spec using a newer API version.", gv.String(), useKubeadmVersion)
   100  	}
   101  
   102  	if _, present := deprecatedAPIVersions[gvString]; present && !allowDeprecated {
   103  		klog.Warningf("your configuration file uses a deprecated API spec: %q. Please use 'kubeadm config migrate --old-config old.yaml --new-config new.yaml', which will write the new, similar spec using a newer API version.", gv.String())
   104  	}
   105  
   106  	if _, present := experimentalAPIVersions[gvString]; present && !allowExperimental {
   107  		return errors.Errorf("experimental API spec: %q is not allowed. You can use the --%s flag if the command supports it.", gv, options.AllowExperimentalAPI)
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  // NormalizeKubernetesVersion resolves version labels, sets alternative
   114  // image registry if requested for CI builds, and validates minimal
   115  // version that kubeadm SetInitDynamicDefaultssupports.
   116  func NormalizeKubernetesVersion(cfg *kubeadmapi.ClusterConfiguration) error {
   117  	isCIVersion := kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion)
   118  
   119  	// Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images
   120  	if isCIVersion && cfg.ImageRepository == kubeadmapiv1.DefaultImageRepository {
   121  		cfg.CIImageRepository = constants.DefaultCIImageRepository
   122  	}
   123  
   124  	// Parse and validate the version argument and resolve possible CI version labels
   125  	ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	// Requested version is automatic CI build, thus mark CIKubernetesVersion as `ci/<resolved-version>`
   131  	if isCIVersion {
   132  		cfg.CIKubernetesVersion = fmt.Sprintf("%s%s", constants.CIKubernetesVersionPrefix, ver)
   133  	}
   134  
   135  	cfg.KubernetesVersion = ver
   136  
   137  	// Parse the given kubernetes version and make sure it's higher than the lowest supported
   138  	k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
   139  	if err != nil {
   140  		return errors.Wrapf(err, "couldn't parse Kubernetes version %q", cfg.KubernetesVersion)
   141  	}
   142  
   143  	// During the k8s release process, a kubeadm version in the main branch could be 1.23.0-pre,
   144  	// while the 1.22.0 version is not released yet. The MinimumControlPlaneVersion validation
   145  	// in such a case will not pass, since the value of MinimumControlPlaneVersion would be
   146  	// calculated as kubeadm version - 1 (1.22) and k8sVersion would still be at 1.21.x
   147  	// (fetched from the 'stable' marker). Handle this case by only showing a warning.
   148  	mcpVersion := constants.MinimumControlPlaneVersion
   149  	versionInfo := componentversion.Get()
   150  	if isKubeadmPrereleaseVersion(&versionInfo, k8sVersion, mcpVersion) {
   151  		klog.V(1).Infof("WARNING: tolerating control plane version %s as a pre-release version", cfg.KubernetesVersion)
   152  
   153  		return nil
   154  	}
   155  	// If not a pre-release version, handle the validation normally.
   156  	if k8sVersion.LessThan(mcpVersion) {
   157  		return errors.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s",
   158  			mcpVersion, cfg.KubernetesVersion)
   159  	}
   160  	return nil
   161  }
   162  
   163  // LowercaseSANs can be used to force all SANs to be lowercase so it passes IsDNS1123Subdomain
   164  func LowercaseSANs(sans []string) {
   165  	for i, san := range sans {
   166  		lowercase := strings.ToLower(san)
   167  		if lowercase != san {
   168  			klog.V(1).Infof("lowercasing SAN %q to %q", san, lowercase)
   169  			sans[i] = lowercase
   170  		}
   171  	}
   172  }
   173  
   174  // VerifyAPIServerBindAddress can be used to verify if a bind address for the API Server is 0.0.0.0,
   175  // in which case this address is not valid and should not be used.
   176  func VerifyAPIServerBindAddress(address string) error {
   177  	ip := netutils.ParseIPSloppy(address)
   178  	if ip == nil {
   179  		return errors.Errorf("cannot parse IP address: %s", address)
   180  	}
   181  	// There are users with network setups where default routes are present, but network interfaces
   182  	// use only link-local addresses (e.g. as described in RFC5549).
   183  	// In many cases that matching global unicast IP address can be found on loopback interface,
   184  	// so kubeadm allows users to specify address=Loopback for handling supporting the scenario above.
   185  	// Nb. SetAPIEndpointDynamicDefaults will try to translate loopback to a valid address afterwards
   186  	if ip.IsLoopback() {
   187  		return nil
   188  	}
   189  	if !ip.IsGlobalUnicast() {
   190  		return errors.Errorf("cannot use %q as the bind address for the API Server", address)
   191  	}
   192  	return nil
   193  }
   194  
   195  // ChooseAPIServerBindAddress is a wrapper for netutil.ResolveBindAddress that also handles
   196  // the case where no default routes were found and an IP for the API server could not be obtained.
   197  func ChooseAPIServerBindAddress(bindAddress net.IP) (net.IP, error) {
   198  	ip, err := netutil.ResolveBindAddress(bindAddress)
   199  	if err != nil {
   200  		if netutil.IsNoRoutesError(err) {
   201  			klog.Warningf("WARNING: could not obtain a bind address for the API Server: %v; using: %s", err, constants.DefaultAPIServerBindAddress)
   202  			defaultIP := netutils.ParseIPSloppy(constants.DefaultAPIServerBindAddress)
   203  			if defaultIP == nil {
   204  				return nil, errors.Errorf("cannot parse default IP address: %s", constants.DefaultAPIServerBindAddress)
   205  			}
   206  			return defaultIP, nil
   207  		}
   208  		return nil, err
   209  	}
   210  	if bindAddress != nil && !bindAddress.IsUnspecified() && !reflect.DeepEqual(ip, bindAddress) {
   211  		klog.Warningf("WARNING: overriding requested API server bind address: requested %q, actual %q", bindAddress, ip)
   212  	}
   213  	return ip, nil
   214  }
   215  
   216  // validateKnownGVKs takes a list of GVKs and verifies if they are known in kubeadm or component config schemes
   217  func validateKnownGVKs(gvks []schema.GroupVersionKind) error {
   218  	var unknown []schema.GroupVersionKind
   219  
   220  	schemes := []*runtime.Scheme{
   221  		kubeadmscheme.Scheme,
   222  		componentconfigs.Scheme,
   223  	}
   224  
   225  	for _, gvk := range gvks {
   226  		var scheme *runtime.Scheme
   227  
   228  		// Skip legacy known GVs so that they don't return errors.
   229  		// This makes the function return errors only for GVs that where never known.
   230  		if err := validateSupportedVersion(gvk.GroupVersion(), true, true); err != nil {
   231  			continue
   232  		}
   233  
   234  		for _, s := range schemes {
   235  			if _, err := s.New(gvk); err == nil {
   236  				scheme = s
   237  				break
   238  			}
   239  		}
   240  		if scheme == nil {
   241  			unknown = append(unknown, gvk)
   242  		}
   243  	}
   244  
   245  	if len(unknown) > 0 {
   246  		return errors.Errorf("unknown configuration APIs: %#v", unknown)
   247  	}
   248  
   249  	return nil
   250  }
   251  
   252  // MigrateOldConfig migrates an old configuration from a byte slice into a new one (returned again as a byte slice).
   253  // Only kubeadm kinds are migrated.
   254  func MigrateOldConfig(oldConfig []byte, allowExperimental bool, mutators migrateMutators) ([]byte, error) {
   255  	newConfig := [][]byte{}
   256  
   257  	if mutators == nil {
   258  		mutators = defaultMigrateMutators()
   259  	}
   260  
   261  	gvkmap, err := kubeadmutil.SplitYAMLDocuments(oldConfig)
   262  	if err != nil {
   263  		return []byte{}, err
   264  	}
   265  
   266  	gvks := []schema.GroupVersionKind{}
   267  	for gvk := range gvkmap {
   268  		gvks = append(gvks, gvk)
   269  	}
   270  
   271  	if err := validateKnownGVKs(gvks); err != nil {
   272  		return []byte{}, err
   273  	}
   274  
   275  	gv := kubeadmapiv1old.SchemeGroupVersion
   276  	if allowExperimental {
   277  		gv = kubeadmapiv1.SchemeGroupVersion
   278  	}
   279  	// Migrate InitConfiguration and ClusterConfiguration if there are any in the config
   280  	if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) || kubeadmutil.GroupVersionKindsHasClusterConfiguration(gvks...) {
   281  		o, err := documentMapToInitConfiguration(gvkmap, true, allowExperimental, true, false)
   282  		if err != nil {
   283  			return []byte{}, err
   284  		}
   285  		if err := mutators.mutate([]any{o}); err != nil {
   286  			return []byte{}, err
   287  		}
   288  		b, err := MarshalKubeadmConfigObject(o, gv)
   289  		if err != nil {
   290  			return []byte{}, err
   291  		}
   292  		newConfig = append(newConfig, b)
   293  	}
   294  
   295  	// Migrate JoinConfiguration if there is any
   296  	if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
   297  		o, err := documentMapToJoinConfiguration(gvkmap, true, allowExperimental, true, false)
   298  		if err != nil {
   299  			return []byte{}, err
   300  		}
   301  		if err := mutators.mutate([]any{o}); err != nil {
   302  			return []byte{}, err
   303  		}
   304  		b, err := MarshalKubeadmConfigObject(o, gv)
   305  		if err != nil {
   306  			return []byte{}, err
   307  		}
   308  		newConfig = append(newConfig, b)
   309  	}
   310  
   311  	// Migrate ResetConfiguration if there is any
   312  	if kubeadmutil.GroupVersionKindsHasResetConfiguration(gvks...) {
   313  		o, err := documentMapToResetConfiguration(gvkmap, true, allowExperimental, true, false)
   314  		if err != nil {
   315  			return []byte{}, err
   316  		}
   317  		if err := mutators.mutate([]any{o}); err != nil {
   318  			return []byte{}, err
   319  		}
   320  		b, err := MarshalKubeadmConfigObject(o, gv)
   321  		if err != nil {
   322  			return []byte{}, err
   323  		}
   324  		newConfig = append(newConfig, b)
   325  	}
   326  
   327  	return bytes.Join(newConfig, []byte(constants.YAMLDocumentSeparator)), nil
   328  }
   329  
   330  // ValidateConfig takes a byte slice containing a kubeadm configuration and performs conversion
   331  // to internal types and validation.
   332  func ValidateConfig(config []byte, allowExperimental bool) error {
   333  	gvkmap, err := kubeadmutil.SplitYAMLDocuments(config)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	gvks := []schema.GroupVersionKind{}
   339  	for gvk := range gvkmap {
   340  		gvks = append(gvks, gvk)
   341  	}
   342  
   343  	if err := validateKnownGVKs(gvks); err != nil {
   344  		return err
   345  	}
   346  
   347  	// Validate InitConfiguration and ClusterConfiguration if there are any in the config
   348  	if kubeadmutil.GroupVersionKindsHasInitConfiguration(gvks...) || kubeadmutil.GroupVersionKindsHasClusterConfiguration(gvks...) {
   349  		if _, err := documentMapToInitConfiguration(gvkmap, true, allowExperimental, true, true); err != nil {
   350  			return err
   351  		}
   352  	}
   353  
   354  	// Validate JoinConfiguration if there is any
   355  	if kubeadmutil.GroupVersionKindsHasJoinConfiguration(gvks...) {
   356  		if _, err := documentMapToJoinConfiguration(gvkmap, true, allowExperimental, true, true); err != nil {
   357  			return err
   358  		}
   359  	}
   360  
   361  	// Validate ResetConfiguration if there is any
   362  	if kubeadmutil.GroupVersionKindsHasResetConfiguration(gvks...) {
   363  		if _, err := documentMapToResetConfiguration(gvkmap, true, allowExperimental, true, true); err != nil {
   364  			return err
   365  		}
   366  	}
   367  
   368  	return nil
   369  }
   370  
   371  // isKubeadmPrereleaseVersion returns true if the kubeadm version is a pre-release version and
   372  // the minimum control plane version is N+2 MINOR version of the given k8sVersion.
   373  func isKubeadmPrereleaseVersion(versionInfo *apimachineryversion.Info, k8sVersion, mcpVersion *version.Version) bool {
   374  	if len(versionInfo.Major) != 0 { // Make sure the component version is populated
   375  		kubeadmVersion := version.MustParseSemantic(versionInfo.String())
   376  		if len(kubeadmVersion.PreRelease()) != 0 { // Only handle this if the kubeadm binary is a pre-release
   377  			// After incrementing the k8s MINOR version by one, if this version is equal or greater than the
   378  			// MCP version, return true.
   379  			v := k8sVersion.WithMinor(k8sVersion.Minor() + 1)
   380  			if comp, _ := v.Compare(mcpVersion.String()); comp != -1 {
   381  				return true
   382  			}
   383  		}
   384  	}
   385  	return false
   386  }
   387  
   388  // prepareStaticVariables takes a given config and stores values from it in variables
   389  // that can be used from multiple packages.
   390  func prepareStaticVariables(config any) {
   391  	switch c := config.(type) {
   392  	case *kubeadmapi.InitConfiguration:
   393  		kubeadmapi.SetActiveTimeouts(c.Timeouts)
   394  	case *kubeadmapi.JoinConfiguration:
   395  		kubeadmapi.SetActiveTimeouts(c.Timeouts)
   396  	case *kubeadmapi.ResetConfiguration:
   397  		kubeadmapi.SetActiveTimeouts(c.Timeouts)
   398  	case *kubeadmapi.UpgradeConfiguration:
   399  		kubeadmapi.SetActiveTimeouts(c.Timeouts)
   400  	}
   401  }
   402  
   403  // migrateMutator can be used to mutate a slice of configuration objects.
   404  // The mutation is applied in-place and no copies are made.
   405  type migrateMutator struct {
   406  	in         []any
   407  	mutateFunc func(in []any) error
   408  }
   409  
   410  // migrateMutators holds a list of registered mutators.
   411  type migrateMutators []migrateMutator
   412  
   413  // mutate can be called on a list of registered mutators to find a suitable one to perform
   414  // a configuration object mutation.
   415  func (mutators migrateMutators) mutate(in []any) error {
   416  	var mutator *migrateMutator
   417  	for idx, m := range mutators {
   418  		if len(m.in) != len(in) {
   419  			continue
   420  		}
   421  		inputMatch := true
   422  		for idx := range m.in {
   423  			if reflect.TypeOf(m.in[idx]) != reflect.TypeOf(in[idx]) {
   424  				inputMatch = false
   425  				break
   426  			}
   427  		}
   428  		if inputMatch {
   429  			mutator = &mutators[idx]
   430  			break
   431  		}
   432  	}
   433  	if mutator == nil {
   434  		return errors.Errorf("could not find a mutator for input: %#v", in)
   435  	}
   436  	return mutator.mutateFunc(in)
   437  }
   438  
   439  // addEmpty adds an empty migrate mutator for a given input.
   440  func (mutators *migrateMutators) addEmpty(in []any) {
   441  	mutator := migrateMutator{
   442  		in:         in,
   443  		mutateFunc: func(in []any) error { return nil },
   444  	}
   445  	*mutators = append(*mutators, mutator)
   446  }
   447  
   448  // defaultMutators returns the default list of mutators for known configuration objects.
   449  // TODO: make this function return defaultEmptyMutators() when v1beta3 is removed.
   450  func defaultMigrateMutators() migrateMutators {
   451  	var (
   452  		mutators migrateMutators
   453  		mutator  migrateMutator
   454  	)
   455  
   456  	// mutator for InitConfiguration, ClusterConfiguration.
   457  	mutator = migrateMutator{
   458  		in: []any{(*kubeadmapi.InitConfiguration)(nil)},
   459  		mutateFunc: func(in []any) error {
   460  			a := in[0].(*kubeadmapi.InitConfiguration)
   461  			a.Timeouts.ControlPlaneComponentHealthCheck.Duration = a.APIServer.TimeoutForControlPlane.Duration
   462  			a.APIServer.TimeoutForControlPlane = nil
   463  			return nil
   464  		},
   465  	}
   466  	mutators = append(mutators, mutator)
   467  
   468  	// mutator for JoinConfiguration.
   469  	mutator = migrateMutator{
   470  		in: []any{(*kubeadmapi.JoinConfiguration)(nil)},
   471  		mutateFunc: func(in []any) error {
   472  			a := in[0].(*kubeadmapi.JoinConfiguration)
   473  			a.Timeouts.Discovery.Duration = a.Discovery.Timeout.Duration
   474  			a.Discovery.Timeout = nil
   475  			return nil
   476  		},
   477  	}
   478  	mutators = append(mutators, mutator)
   479  
   480  	// empty mutator for ResetConfiguration.
   481  	mutators.addEmpty([]any{(*kubeadmapi.ResetConfiguration)(nil)})
   482  
   483  	return mutators
   484  }
   485  
   486  // defaultEmptyMigrateMutators returns a list of empty mutators for known types.
   487  func defaultEmptyMigrateMutators() migrateMutators {
   488  	mutators := &migrateMutators{}
   489  
   490  	mutators.addEmpty([]any{(*kubeadmapi.InitConfiguration)(nil)})
   491  	mutators.addEmpty([]any{(*kubeadmapi.JoinConfiguration)(nil)})
   492  	mutators.addEmpty([]any{(*kubeadmapi.ResetConfiguration)(nil)})
   493  
   494  	return *mutators
   495  }
   496  
   497  // isKubeadmConfigPresent checks if a kubeadm config type is found in the provided document map
   498  func isKubeadmConfigPresent(docmap kubeadmapi.DocumentMap) bool {
   499  	for gvk := range docmap {
   500  		if gvk.Group == kubeadmapi.GroupName {
   501  			return true
   502  		}
   503  	}
   504  	return false
   505  }
   506  

View as plain text