k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/config/common.go (about) 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 }