k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/preflight/checks.go (about) 1 /* 2 Copyright 2016 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 preflight 18 19 import ( 20 "bufio" 21 "bytes" 22 "crypto/tls" 23 "crypto/x509" 24 "encoding/json" 25 "fmt" 26 "io" 27 "net" 28 "net/http" 29 "net/url" 30 "os" 31 "path/filepath" 32 "runtime" 33 "strings" 34 "time" 35 36 "github.com/pkg/errors" 37 38 v1 "k8s.io/api/core/v1" 39 netutil "k8s.io/apimachinery/pkg/util/net" 40 "k8s.io/apimachinery/pkg/util/sets" 41 "k8s.io/apimachinery/pkg/util/validation" 42 versionutil "k8s.io/apimachinery/pkg/util/version" 43 kubeadmversion "k8s.io/component-base/version" 44 "k8s.io/klog/v2" 45 system "k8s.io/system-validators/validators" 46 utilsexec "k8s.io/utils/exec" 47 netutils "k8s.io/utils/net" 48 49 kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" 50 kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" 51 "k8s.io/kubernetes/cmd/kubeadm/app/images" 52 "k8s.io/kubernetes/cmd/kubeadm/app/util/initsystem" 53 utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime" 54 ) 55 56 const ( 57 ipv4Forward = "/proc/sys/net/ipv4/ip_forward" 58 ipv6DefaultForwarding = "/proc/sys/net/ipv6/conf/default/forwarding" 59 externalEtcdRequestTimeout = 10 * time.Second 60 externalEtcdRequestRetries = 3 61 externalEtcdRequestInterval = 5 * time.Second 62 ) 63 64 var ( 65 minExternalEtcdVersion = versionutil.MustParseSemantic(kubeadmconstants.MinExternalEtcdVersion) 66 ) 67 68 // Error defines struct for communicating error messages generated by preflight checks 69 type Error struct { 70 Msg string 71 } 72 73 // Error implements the standard error interface 74 func (e *Error) Error() string { 75 return fmt.Sprintf("[preflight] Some fatal errors occurred:\n%s%s", e.Msg, "[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`") 76 } 77 78 // Preflight identifies this error as a preflight error 79 func (e *Error) Preflight() bool { 80 return true 81 } 82 83 // Checker validates the state of the system to ensure kubeadm will be 84 // successful as often as possible. 85 type Checker interface { 86 Check() (warnings, errorList []error) 87 Name() string 88 } 89 90 // ContainerRuntimeCheck verifies the container runtime. 91 type ContainerRuntimeCheck struct { 92 runtime utilruntime.ContainerRuntime 93 } 94 95 // Name returns label for RuntimeCheck. 96 func (ContainerRuntimeCheck) Name() string { 97 return "CRI" 98 } 99 100 // Check validates the container runtime 101 func (crc ContainerRuntimeCheck) Check() (warnings, errorList []error) { 102 klog.V(1).Infoln("validating the container runtime") 103 if err := crc.runtime.IsRunning(); err != nil { 104 errorList = append(errorList, err) 105 } 106 return warnings, errorList 107 } 108 109 // ServiceCheck verifies that the given service is enabled and active. If we do not 110 // detect a supported init system however, all checks are skipped and a warning is 111 // returned. 112 type ServiceCheck struct { 113 Service string 114 CheckIfActive bool 115 Label string 116 } 117 118 // Name returns label for ServiceCheck. If not provided, will return based on the service parameter 119 func (sc ServiceCheck) Name() string { 120 if sc.Label != "" { 121 return sc.Label 122 } 123 return fmt.Sprintf("Service-%s", strings.Title(sc.Service)) 124 } 125 126 // Check validates if the service is enabled and active. 127 func (sc ServiceCheck) Check() (warnings, errorList []error) { 128 klog.V(1).Infof("validating if the %q service is enabled and active", sc.Service) 129 initSystem, err := initsystem.GetInitSystem() 130 if err != nil { 131 return []error{err}, nil 132 } 133 134 if !initSystem.ServiceExists(sc.Service) { 135 return []error{errors.Errorf("%s service does not exist", sc.Service)}, nil 136 } 137 138 if !initSystem.ServiceIsEnabled(sc.Service) { 139 warnings = append(warnings, 140 errors.Errorf("%s service is not enabled, please run '%s'", 141 sc.Service, initSystem.EnableCommand(sc.Service))) 142 } 143 144 if sc.CheckIfActive && !initSystem.ServiceIsActive(sc.Service) { 145 errorList = append(errorList, 146 errors.Errorf("%s service is not active, please run 'systemctl start %s.service'", 147 sc.Service, sc.Service)) 148 } 149 150 return warnings, errorList 151 } 152 153 // FirewalldCheck checks if firewalld is enabled or active. If it is, warn the user that there may be problems 154 // if no actions are taken. 155 type FirewalldCheck struct { 156 ports []int 157 } 158 159 // Name returns label for FirewalldCheck. 160 func (FirewalldCheck) Name() string { 161 return "Firewalld" 162 } 163 164 // Check validates if the firewall is enabled and active. 165 func (fc FirewalldCheck) Check() (warnings, errorList []error) { 166 klog.V(1).Infoln("validating if the firewall is enabled and active") 167 initSystem, err := initsystem.GetInitSystem() 168 if err != nil { 169 return []error{err}, nil 170 } 171 172 if !initSystem.ServiceExists("firewalld") { 173 return nil, nil 174 } 175 176 if initSystem.ServiceIsActive("firewalld") { 177 err := errors.Errorf("firewalld is active, please ensure ports %v are open or your cluster may not function correctly", 178 fc.ports) 179 return []error{err}, nil 180 } 181 182 return nil, nil 183 } 184 185 // PortOpenCheck ensures the given port is available for use. 186 type PortOpenCheck struct { 187 port int 188 label string 189 } 190 191 // Name returns name for PortOpenCheck. If not known, will return "PortXXXX" based on port number 192 func (poc PortOpenCheck) Name() string { 193 if poc.label != "" { 194 return poc.label 195 } 196 return fmt.Sprintf("Port-%d", poc.port) 197 } 198 199 // Check validates if the particular port is available. 200 func (poc PortOpenCheck) Check() (warnings, errorList []error) { 201 klog.V(1).Infof("validating availability of port %d", poc.port) 202 203 ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port)) 204 if err != nil { 205 errorList = []error{errors.Errorf("Port %d is in use", poc.port)} 206 } 207 if ln != nil { 208 if err = ln.Close(); err != nil { 209 warnings = append(warnings, 210 errors.Errorf("when closing port %d, encountered %v", poc.port, err)) 211 } 212 } 213 214 return warnings, errorList 215 } 216 217 // IsPrivilegedUserCheck verifies user is privileged (linux - root, windows - Administrator) 218 type IsPrivilegedUserCheck struct{} 219 220 // Name returns name for IsPrivilegedUserCheck 221 func (IsPrivilegedUserCheck) Name() string { 222 return "IsPrivilegedUser" 223 } 224 225 // DirAvailableCheck checks if the given directory either does not exist, or is empty. 226 type DirAvailableCheck struct { 227 Path string 228 Label string 229 } 230 231 // Name returns label for individual DirAvailableChecks. If not known, will return based on path. 232 func (dac DirAvailableCheck) Name() string { 233 if dac.Label != "" { 234 return dac.Label 235 } 236 return fmt.Sprintf("DirAvailable-%s", strings.Replace(dac.Path, "/", "-", -1)) 237 } 238 239 // Check validates if a directory does not exist or empty. 240 func (dac DirAvailableCheck) Check() (warnings, errorList []error) { 241 klog.V(1).Infof("validating the existence and emptiness of directory %s", dac.Path) 242 243 // If it doesn't exist we are good: 244 if _, err := os.Stat(dac.Path); os.IsNotExist(err) { 245 return nil, nil 246 } 247 248 f, err := os.Open(dac.Path) 249 if err != nil { 250 return nil, []error{errors.Wrapf(err, "unable to check if %s is empty", dac.Path)} 251 } 252 defer f.Close() 253 254 _, err = f.Readdirnames(1) 255 if err != io.EOF { 256 return nil, []error{errors.Errorf("%s is not empty", dac.Path)} 257 } 258 259 return nil, nil 260 } 261 262 // FileAvailableCheck checks that the given file does not already exist. 263 type FileAvailableCheck struct { 264 Path string 265 Label string 266 } 267 268 // Name returns label for individual FileAvailableChecks. If not known, will return based on path. 269 func (fac FileAvailableCheck) Name() string { 270 if fac.Label != "" { 271 return fac.Label 272 } 273 return fmt.Sprintf("FileAvailable-%s", strings.Replace(fac.Path, "/", "-", -1)) 274 } 275 276 // Check validates if the given file does not already exist. 277 func (fac FileAvailableCheck) Check() (warnings, errorList []error) { 278 klog.V(1).Infof("validating the existence of file %s", fac.Path) 279 280 if _, err := os.Stat(fac.Path); err == nil { 281 return nil, []error{errors.Errorf("%s already exists", fac.Path)} 282 } 283 return nil, nil 284 } 285 286 // FileExistingCheck checks that the given file does not already exist. 287 type FileExistingCheck struct { 288 Path string 289 Label string 290 } 291 292 // Name returns label for individual FileExistingChecks. If not known, will return based on path. 293 func (fac FileExistingCheck) Name() string { 294 if fac.Label != "" { 295 return fac.Label 296 } 297 return fmt.Sprintf("FileExisting-%s", strings.Replace(fac.Path, "/", "-", -1)) 298 } 299 300 // Check validates if the given file already exists. 301 func (fac FileExistingCheck) Check() (warnings, errorList []error) { 302 klog.V(1).Infof("validating the existence of file %s", fac.Path) 303 304 if _, err := os.Stat(fac.Path); err != nil { 305 return nil, []error{errors.Errorf("%s doesn't exist", fac.Path)} 306 } 307 return nil, nil 308 } 309 310 // FileContentCheck checks that the given file contains the string Content. 311 type FileContentCheck struct { 312 Path string 313 Content []byte 314 Label string 315 } 316 317 // Name returns label for individual FileContentChecks. If not known, will return based on path. 318 func (fcc FileContentCheck) Name() string { 319 if fcc.Label != "" { 320 return fcc.Label 321 } 322 return fmt.Sprintf("FileContent-%s", strings.Replace(fcc.Path, "/", "-", -1)) 323 } 324 325 // Check validates if the given file contains the given content. 326 func (fcc FileContentCheck) Check() (warnings, errorList []error) { 327 klog.V(1).Infof("validating the contents of file %s", fcc.Path) 328 f, err := os.Open(fcc.Path) 329 if err != nil { 330 return nil, []error{errors.Errorf("%s does not exist", fcc.Path)} 331 } 332 333 lr := io.LimitReader(f, int64(len(fcc.Content))) 334 defer f.Close() 335 336 buf := &bytes.Buffer{} 337 _, err = io.Copy(buf, lr) 338 if err != nil { 339 return nil, []error{errors.Errorf("%s could not be read", fcc.Path)} 340 } 341 342 if !bytes.Equal(buf.Bytes(), fcc.Content) { 343 return nil, []error{errors.Errorf("%s contents are not set to %s", fcc.Path, fcc.Content)} 344 } 345 return nil, []error{} 346 347 } 348 349 // InPathCheck checks if the given executable is present in $PATH 350 type InPathCheck struct { 351 executable string 352 mandatory bool 353 exec utilsexec.Interface 354 label string 355 suggestion string 356 } 357 358 // Name returns label for individual InPathCheck. If not known, will return based on path. 359 func (ipc InPathCheck) Name() string { 360 if ipc.label != "" { 361 return ipc.label 362 } 363 return fmt.Sprintf("FileExisting-%s", strings.Replace(ipc.executable, "/", "-", -1)) 364 } 365 366 // Check validates if the given executable is present in the path. 367 func (ipc InPathCheck) Check() (warnings, errs []error) { 368 klog.V(1).Infof("validating the presence of executable %s", ipc.executable) 369 _, err := ipc.exec.LookPath(ipc.executable) 370 if err != nil { 371 if ipc.mandatory { 372 // Return as an error: 373 return nil, []error{errors.Errorf("%s not found in system path", ipc.executable)} 374 } 375 // Return as a warning: 376 warningMessage := fmt.Sprintf("%s not found in system path", ipc.executable) 377 if ipc.suggestion != "" { 378 warningMessage += fmt.Sprintf("\nSuggestion: %s", ipc.suggestion) 379 } 380 return []error{errors.New(warningMessage)}, nil 381 } 382 return nil, nil 383 } 384 385 // HostnameCheck checks if hostname match dns subdomain regex. 386 // If hostname doesn't match this regex, kubelet will not launch static pods like kube-apiserver/kube-controller-manager and so on. 387 type HostnameCheck struct { 388 nodeName string 389 } 390 391 // Name will return Hostname as name for HostnameCheck 392 func (HostnameCheck) Name() string { 393 return "Hostname" 394 } 395 396 // Check validates if hostname match dns subdomain regex. 397 // Check hostname length and format 398 func (hc HostnameCheck) Check() (warnings, errorList []error) { 399 klog.V(1).Infoln("checking whether the given node name is valid and reachable using net.LookupHost") 400 for _, msg := range validation.IsQualifiedName(hc.nodeName) { 401 warnings = append(warnings, errors.Errorf("invalid node name format %q: %s", hc.nodeName, msg)) 402 } 403 404 addr, err := net.LookupHost(hc.nodeName) 405 if addr == nil { 406 warnings = append(warnings, errors.Errorf("hostname \"%s\" could not be reached", hc.nodeName)) 407 } 408 if err != nil { 409 warnings = append(warnings, errors.Wrapf(err, "hostname \"%s\"", hc.nodeName)) 410 } 411 return warnings, errorList 412 } 413 414 // HTTPProxyCheck checks if https connection to specific host is going 415 // to be done directly or over proxy. If proxy detected, it will return warning. 416 type HTTPProxyCheck struct { 417 Proto string 418 Host string 419 } 420 421 // Name returns HTTPProxy as name for HTTPProxyCheck 422 func (hst HTTPProxyCheck) Name() string { 423 return "HTTPProxy" 424 } 425 426 // Check validates http connectivity type, direct or via proxy. 427 func (hst HTTPProxyCheck) Check() (warnings, errorList []error) { 428 klog.V(1).Infoln("validating if the connectivity type is via proxy or direct") 429 u := &url.URL{Scheme: hst.Proto, Host: hst.Host} 430 if netutils.IsIPv6String(hst.Host) { 431 u.Host = net.JoinHostPort(hst.Host, "1234") 432 } 433 434 req, err := http.NewRequest("GET", u.String(), nil) 435 if err != nil { 436 return nil, []error{err} 437 } 438 439 proxy, err := netutil.SetOldTransportDefaults(&http.Transport{}).Proxy(req) 440 if err != nil { 441 return nil, []error{err} 442 } 443 if proxy != nil { 444 return []error{errors.Errorf("Connection to %q uses proxy %q. If that is not intended, adjust your proxy settings", u, proxy)}, nil 445 } 446 return nil, nil 447 } 448 449 // HTTPProxyCIDRCheck checks if https connection to specific subnet is going 450 // to be done directly or over proxy. If proxy detected, it will return warning. 451 // Similar to HTTPProxyCheck above, but operates with subnets and uses API 452 // machinery transport defaults to simulate kube-apiserver accessing cluster 453 // services and pods. 454 type HTTPProxyCIDRCheck struct { 455 Proto string 456 CIDR string 457 } 458 459 // Name will return HTTPProxyCIDR as name for HTTPProxyCIDRCheck 460 func (HTTPProxyCIDRCheck) Name() string { 461 return "HTTPProxyCIDR" 462 } 463 464 // Check validates http connectivity to first IP address in the CIDR. 465 // If it is not directly connected and goes via proxy it will produce warning. 466 func (subnet HTTPProxyCIDRCheck) Check() (warnings, errorList []error) { 467 klog.V(1).Infoln("validating http connectivity to first IP address in the CIDR") 468 if len(subnet.CIDR) == 0 { 469 return nil, nil 470 } 471 472 _, cidr, err := netutils.ParseCIDRSloppy(subnet.CIDR) 473 if err != nil { 474 return nil, []error{errors.Wrapf(err, "error parsing CIDR %q", subnet.CIDR)} 475 } 476 477 testIP, err := netutils.GetIndexedIP(cidr, 1) 478 if err != nil { 479 return nil, []error{errors.Wrapf(err, "unable to get first IP address from the given CIDR (%s)", cidr.String())} 480 } 481 482 testIPstring := testIP.String() 483 if len(testIP) == net.IPv6len { 484 testIPstring = fmt.Sprintf("[%s]:1234", testIP) 485 } 486 url := fmt.Sprintf("%s://%s/", subnet.Proto, testIPstring) 487 488 req, err := http.NewRequest("GET", url, nil) 489 if err != nil { 490 return nil, []error{err} 491 } 492 493 // Utilize same transport defaults as it will be used by API server 494 proxy, err := netutil.SetOldTransportDefaults(&http.Transport{}).Proxy(req) 495 if err != nil { 496 return nil, []error{err} 497 } 498 if proxy != nil { 499 return []error{errors.Errorf("connection to %q uses proxy %q. This may lead to malfunctional cluster setup. Make sure that Pod and Services IP ranges specified correctly as exceptions in proxy configuration", subnet.CIDR, proxy)}, nil 500 } 501 return nil, nil 502 } 503 504 // SystemVerificationCheck defines struct used for running the system verification node check in test/e2e_node/system 505 type SystemVerificationCheck struct{} 506 507 // Name will return SystemVerification as name for SystemVerificationCheck 508 func (SystemVerificationCheck) Name() string { 509 return "SystemVerification" 510 } 511 512 // Check runs all individual checks 513 func (sysver SystemVerificationCheck) Check() (warnings, errorList []error) { 514 klog.V(1).Infoln("running all checks") 515 // Create a buffered writer and choose a quite large value (1M) and suppose the output from the system verification test won't exceed the limit 516 // Run the system verification check, but write to out buffered writer instead of stdout 517 bufw := bufio.NewWriterSize(os.Stdout, 1*1024*1024) 518 reporter := &system.StreamReporter{WriteStream: bufw} 519 520 var errs []error 521 var warns []error 522 // All the common validators we'd like to run: 523 var validators = []system.Validator{ 524 &system.KernelValidator{Reporter: reporter}} 525 526 validators = addOSValidator(validators, reporter) 527 528 // Run all validators 529 for _, v := range validators { 530 warn, err := v.Validate(system.DefaultSysSpec) 531 if err != nil { 532 errs = append(errs, err...) 533 } 534 if warn != nil { 535 warns = append(warns, warn...) 536 } 537 } 538 539 if len(errs) != 0 { 540 // Only print the output from the system verification check if the check failed 541 fmt.Println("[preflight] The system verification failed. Printing the output from the verification:") 542 bufw.Flush() 543 return warns, errs 544 } 545 return warns, nil 546 } 547 548 // KubernetesVersionCheck validates Kubernetes and kubeadm versions 549 type KubernetesVersionCheck struct { 550 KubeadmVersion string 551 KubernetesVersion string 552 } 553 554 // Name will return KubernetesVersion as name for KubernetesVersionCheck 555 func (KubernetesVersionCheck) Name() string { 556 return "KubernetesVersion" 557 } 558 559 // Check validates Kubernetes and kubeadm versions 560 func (kubever KubernetesVersionCheck) Check() (warnings, errorList []error) { 561 klog.V(1).Infoln("validating Kubernetes and kubeadm version") 562 // Skip this check for "super-custom builds", where apimachinery/the overall codebase version is not set. 563 if strings.HasPrefix(kubever.KubeadmVersion, "v0.0.0") { 564 return nil, nil 565 } 566 567 kadmVersion, err := versionutil.ParseSemantic(kubever.KubeadmVersion) 568 if err != nil { 569 return nil, []error{errors.Wrapf(err, "couldn't parse kubeadm version %q", kubever.KubeadmVersion)} 570 } 571 572 k8sVersion, err := versionutil.ParseSemantic(kubever.KubernetesVersion) 573 if err != nil { 574 return nil, []error{errors.Wrapf(err, "couldn't parse Kubernetes version %q", kubever.KubernetesVersion)} 575 } 576 577 // Checks if k8sVersion greater or equal than the first unsupported versions by current version of kubeadm, 578 // that is major.minor+1 (all patch and pre-releases versions included) 579 // NB. in semver patches number is a numeric, while pre-release is a string where numeric identifiers always have lower precedence than non-numeric identifiers. 580 // thus setting the value to x.y.0-0 we are defining the very first patch - pre-releases within x.y minor release. 581 firstUnsupportedVersion := versionutil.MustParseSemantic(fmt.Sprintf("%d.%d.%s", kadmVersion.Major(), kadmVersion.Minor()+1, "0-0")) 582 if k8sVersion.AtLeast(firstUnsupportedVersion) { 583 return []error{errors.Errorf("Kubernetes version is greater than kubeadm version. Please consider to upgrade kubeadm. Kubernetes version: %s. Kubeadm version: %d.%d.x", k8sVersion, kadmVersion.Components()[0], kadmVersion.Components()[1])}, nil 584 } 585 586 return nil, nil 587 } 588 589 // KubeletVersionCheck validates installed kubelet version 590 type KubeletVersionCheck struct { 591 KubernetesVersion string 592 minKubeletVersion *versionutil.Version 593 exec utilsexec.Interface 594 } 595 596 // Name will return KubeletVersion as name for KubeletVersionCheck 597 func (KubeletVersionCheck) Name() string { 598 return "KubeletVersion" 599 } 600 601 // Check validates kubelet version. It should be not less than minimal supported version 602 func (kubever KubeletVersionCheck) Check() (warnings, errorList []error) { 603 klog.V(1).Infoln("validating kubelet version") 604 kubeletVersion, err := GetKubeletVersion(kubever.exec) 605 if err != nil { 606 return nil, []error{errors.Wrap(err, "couldn't get kubelet version")} 607 } 608 if kubever.minKubeletVersion == nil { 609 kubever.minKubeletVersion = kubeadmconstants.MinimumKubeletVersion 610 } 611 if kubeletVersion.LessThan(kubever.minKubeletVersion) { 612 return nil, []error{errors.Errorf("Kubelet version %q is lower than kubeadm can support. Please upgrade kubelet", kubeletVersion)} 613 } 614 615 if kubever.KubernetesVersion != "" { 616 k8sVersion, err := versionutil.ParseSemantic(kubever.KubernetesVersion) 617 if err != nil { 618 return nil, []error{errors.Wrapf(err, "couldn't parse Kubernetes version %q", kubever.KubernetesVersion)} 619 } 620 if kubeletVersion.Major() > k8sVersion.Major() || kubeletVersion.Minor() > k8sVersion.Minor() { 621 return nil, []error{errors.Errorf("the kubelet version is higher than the control plane version. This is not a supported version skew and may lead to a malfunctional cluster. Kubelet version: %q Control plane version: %q", kubeletVersion, k8sVersion)} 622 } 623 } 624 return nil, nil 625 } 626 627 // SwapCheck warns if swap is enabled 628 type SwapCheck struct{} 629 630 // Name will return Swap as name for SwapCheck 631 func (SwapCheck) Name() string { 632 return "Swap" 633 } 634 635 // Check validates whether swap is enabled or not 636 func (swc SwapCheck) Check() (warnings, errorList []error) { 637 klog.V(1).Infoln("validating whether swap is enabled or not") 638 f, err := os.Open("/proc/swaps") 639 if err != nil { 640 // /proc/swaps not available, thus no reasons to warn 641 return nil, nil 642 } 643 defer f.Close() 644 var buf []string 645 scanner := bufio.NewScanner(f) 646 for scanner.Scan() { 647 buf = append(buf, scanner.Text()) 648 } 649 if err := scanner.Err(); err != nil { 650 return []error{errors.Wrap(err, "error parsing /proc/swaps")}, nil 651 } 652 653 if len(buf) > 1 { 654 return []error{errors.New("swap is supported for cgroup v2 only; the NodeSwap feature gate of the kubelet is beta but disabled by default")}, nil 655 } 656 657 return nil, nil 658 } 659 660 type etcdVersionResponse struct { 661 Etcdserver string `json:"etcdserver"` 662 Etcdcluster string `json:"etcdcluster"` 663 } 664 665 // ExternalEtcdVersionCheck checks if version of external etcd meets the demand of kubeadm 666 type ExternalEtcdVersionCheck struct { 667 Etcd kubeadmapi.Etcd 668 } 669 670 // Name will return ExternalEtcdVersion as name for ExternalEtcdVersionCheck 671 func (ExternalEtcdVersionCheck) Name() string { 672 return "ExternalEtcdVersion" 673 } 674 675 // Check validates external etcd version 676 // TODO: Use the official etcd Golang client for this instead? 677 func (evc ExternalEtcdVersionCheck) Check() (warnings, errorList []error) { 678 klog.V(1).Infoln("validating the external etcd version") 679 680 // Return quickly if the user isn't using external etcd 681 if evc.Etcd.External.Endpoints == nil { 682 return nil, nil 683 } 684 685 var config *tls.Config 686 var err error 687 if config, err = evc.configRootCAs(config); err != nil { 688 errorList = append(errorList, err) 689 return nil, errorList 690 } 691 if config, err = evc.configCertAndKey(config); err != nil { 692 errorList = append(errorList, err) 693 return nil, errorList 694 } 695 696 client := evc.getHTTPClient(config) 697 for _, endpoint := range evc.Etcd.External.Endpoints { 698 if _, err := url.Parse(endpoint); err != nil { 699 errorList = append(errorList, errors.Wrapf(err, "failed to parse external etcd endpoint %s", endpoint)) 700 continue 701 } 702 resp := etcdVersionResponse{} 703 var err error 704 versionURL := fmt.Sprintf("%s/%s", endpoint, "version") 705 if tmpVersionURL, err := normalizeURLString(versionURL); err != nil { 706 errorList = append(errorList, errors.Wrapf(err, "failed to normalize external etcd version url %s", versionURL)) 707 continue 708 } else { 709 versionURL = tmpVersionURL 710 } 711 if err = getEtcdVersionResponse(client, versionURL, &resp); err != nil { 712 errorList = append(errorList, err) 713 continue 714 } 715 716 etcdVersion, err := versionutil.ParseSemantic(resp.Etcdserver) 717 if err != nil { 718 errorList = append(errorList, errors.Wrapf(err, "couldn't parse external etcd version %q", resp.Etcdserver)) 719 continue 720 } 721 if etcdVersion.LessThan(minExternalEtcdVersion) { 722 errorList = append(errorList, errors.Errorf("this version of kubeadm only supports external etcd version >= %s. Current version: %s", kubeadmconstants.MinExternalEtcdVersion, resp.Etcdserver)) 723 continue 724 } 725 } 726 727 return nil, errorList 728 } 729 730 // configRootCAs configures and returns a reference to tls.Config instance if CAFile is provided 731 func (evc ExternalEtcdVersionCheck) configRootCAs(config *tls.Config) (*tls.Config, error) { 732 var CACertPool *x509.CertPool 733 if evc.Etcd.External.CAFile != "" { 734 CACert, err := os.ReadFile(evc.Etcd.External.CAFile) 735 if err != nil { 736 return nil, errors.Wrapf(err, "couldn't load external etcd's server certificate %s", evc.Etcd.External.CAFile) 737 } 738 CACertPool = x509.NewCertPool() 739 CACertPool.AppendCertsFromPEM(CACert) 740 } 741 if CACertPool != nil { 742 if config == nil { 743 config = &tls.Config{} 744 } 745 config.RootCAs = CACertPool 746 } 747 return config, nil 748 } 749 750 // configCertAndKey configures and returns a reference to tls.Config instance if CertFile and KeyFile pair is provided 751 func (evc ExternalEtcdVersionCheck) configCertAndKey(config *tls.Config) (*tls.Config, error) { 752 var cert tls.Certificate 753 if evc.Etcd.External.CertFile != "" && evc.Etcd.External.KeyFile != "" { 754 var err error 755 cert, err = tls.LoadX509KeyPair(evc.Etcd.External.CertFile, evc.Etcd.External.KeyFile) 756 if err != nil { 757 return nil, errors.Wrapf(err, "couldn't load external etcd's certificate and key pair %s, %s", evc.Etcd.External.CertFile, evc.Etcd.External.KeyFile) 758 } 759 if config == nil { 760 config = &tls.Config{} 761 } 762 config.Certificates = []tls.Certificate{cert} 763 } 764 return config, nil 765 } 766 767 func (evc ExternalEtcdVersionCheck) getHTTPClient(config *tls.Config) *http.Client { 768 if config != nil { 769 transport := netutil.SetOldTransportDefaults(&http.Transport{ 770 TLSClientConfig: config, 771 }) 772 return &http.Client{ 773 Transport: transport, 774 Timeout: externalEtcdRequestTimeout, 775 } 776 } 777 return &http.Client{Timeout: externalEtcdRequestTimeout, Transport: netutil.SetOldTransportDefaults(&http.Transport{})} 778 } 779 780 func getEtcdVersionResponse(client *http.Client, url string, target interface{}) error { 781 loopCount := externalEtcdRequestRetries + 1 782 var err error 783 var stopRetry bool 784 for loopCount > 0 { 785 if loopCount <= externalEtcdRequestRetries { 786 time.Sleep(externalEtcdRequestInterval) 787 } 788 stopRetry, err = func() (stopRetry bool, err error) { 789 r, err := client.Get(url) 790 if err != nil { 791 loopCount-- 792 return false, err 793 } 794 defer r.Body.Close() 795 796 if r.StatusCode >= 500 && r.StatusCode <= 599 { 797 loopCount-- 798 return false, errors.Errorf("server responded with non-successful status: %s", r.Status) 799 } 800 return true, json.NewDecoder(r.Body).Decode(target) 801 802 }() 803 if stopRetry { 804 break 805 } 806 } 807 return err 808 } 809 810 // ImagePullCheck will pull container images used by kubeadm 811 type ImagePullCheck struct { 812 runtime utilruntime.ContainerRuntime 813 imageList []string 814 sandboxImage string 815 imagePullPolicy v1.PullPolicy 816 imagePullSerial bool 817 } 818 819 // Name returns the label for ImagePullCheck 820 func (ImagePullCheck) Name() string { 821 return "ImagePull" 822 } 823 824 // Check pulls images required by kubeadm. This is a mutating check 825 func (ipc ImagePullCheck) Check() (warnings, errorList []error) { 826 // Handle unsupported image pull policy and policy Never. 827 policy := ipc.imagePullPolicy 828 switch policy { 829 case v1.PullAlways, v1.PullIfNotPresent: 830 klog.V(1).Infof("using image pull policy: %s", policy) 831 case v1.PullNever: 832 klog.V(1).Infof("skipping the pull of all images due to policy: %s", policy) 833 return warnings, errorList 834 default: 835 errorList = append(errorList, errors.Errorf("unsupported pull policy %q", policy)) 836 return warnings, errorList 837 } 838 839 // Handle CRI sandbox image warnings. 840 criSandboxImage, err := ipc.runtime.SandboxImage() 841 if err != nil { 842 klog.V(4).Infof("failed to detect the sandbox image for local container runtime, %v", err) 843 } else if criSandboxImage != ipc.sandboxImage { 844 klog.Warningf("detected that the sandbox image %q of the container runtime is inconsistent with that used by kubeadm."+ 845 "It is recommended to use %q as the CRI sandbox image.", criSandboxImage, ipc.sandboxImage) 846 } 847 848 // Perform parallel pulls. 849 if !ipc.imagePullSerial { 850 if err := ipc.runtime.PullImagesInParallel(ipc.imageList, policy == v1.PullIfNotPresent); err != nil { 851 errorList = append(errorList, err) 852 } 853 return warnings, errorList 854 } 855 856 // Perform serial pulls. 857 for _, image := range ipc.imageList { 858 switch policy { 859 case v1.PullIfNotPresent: 860 ret, err := ipc.runtime.ImageExists(image) 861 if ret && err == nil { 862 klog.V(1).Infof("image exists: %s", image) 863 continue 864 } 865 if err != nil { 866 errorList = append(errorList, errors.Wrapf(err, "failed to check if image %s exists", image)) 867 } 868 fallthrough // Proceed with pulling the image if it does not exist 869 case v1.PullAlways: 870 klog.V(1).Infof("pulling: %s", image) 871 if err := ipc.runtime.PullImage(image); err != nil { 872 errorList = append(errorList, errors.WithMessagef(err, "failed to pull image %s", image)) 873 } 874 } 875 } 876 877 return warnings, errorList 878 } 879 880 // NumCPUCheck checks if current number of CPUs is not less than required 881 type NumCPUCheck struct { 882 NumCPU int 883 } 884 885 // Name returns the label for NumCPUCheck 886 func (NumCPUCheck) Name() string { 887 return "NumCPU" 888 } 889 890 // Check number of CPUs required by kubeadm 891 func (ncc NumCPUCheck) Check() (warnings, errorList []error) { 892 numCPU := runtime.NumCPU() 893 if numCPU < ncc.NumCPU { 894 errorList = append(errorList, errors.Errorf("the number of available CPUs %d is less than the required %d", numCPU, ncc.NumCPU)) 895 } 896 return warnings, errorList 897 } 898 899 // MemCheck checks if the number of megabytes of memory is not less than required 900 type MemCheck struct { 901 Mem uint64 902 } 903 904 // Name returns the label for memory 905 func (MemCheck) Name() string { 906 return "Mem" 907 } 908 909 // InitNodeChecks returns checks specific to "kubeadm init" 910 func InitNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string], isSecondaryControlPlane bool, downloadCerts bool) ([]Checker, error) { 911 if !isSecondaryControlPlane { 912 // First, check if we're root separately from the other preflight checks and fail fast 913 if err := RunRootCheckOnly(ignorePreflightErrors); err != nil { 914 return nil, err 915 } 916 } 917 918 manifestsDir := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ManifestsSubDirName) 919 checks := []Checker{ 920 NumCPUCheck{NumCPU: kubeadmconstants.ControlPlaneNumCPU}, 921 // Linux only 922 // TODO: support other OS, if control-plane is supported on it. 923 MemCheck{Mem: kubeadmconstants.ControlPlaneMem}, 924 KubernetesVersionCheck{KubernetesVersion: cfg.KubernetesVersion, KubeadmVersion: kubeadmversion.Get().GitVersion}, 925 FirewalldCheck{ports: []int{int(cfg.LocalAPIEndpoint.BindPort), kubeadmconstants.KubeletPort}}, 926 PortOpenCheck{port: int(cfg.LocalAPIEndpoint.BindPort)}, 927 PortOpenCheck{port: kubeadmconstants.KubeSchedulerPort}, 928 PortOpenCheck{port: kubeadmconstants.KubeControllerManagerPort}, 929 FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeAPIServer, manifestsDir)}, 930 FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeControllerManager, manifestsDir)}, 931 FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeScheduler, manifestsDir)}, 932 FileAvailableCheck{Path: kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.Etcd, manifestsDir)}, 933 HTTPProxyCheck{Proto: "https", Host: cfg.LocalAPIEndpoint.AdvertiseAddress}, 934 } 935 936 // File content check for IPV4 and IPV6 are needed if it is: 937 // (dual stack) `--service-cidr` or `--pod-network-cidr` is set with an IPV4 and IPV6 CIDR, `--apiserver-advertise-address` is optional as it can be auto-detected. 938 // (single stack) which is decided by the `--apiserver-advertise-address`. 939 // Note that for the case of dual stack, user might only give IPV6 CIDR for `--service-cidr` and leave the `--apiserver-advertise-address` a default value which will be 940 // auto-detected and properly bound to an IPV4 address, this will make the cluster non-functional eventually. The case like this should be avoided by the validation instead, 941 // i.e. We don't care whether the input values for those parameters are set correctly here but if it's an IPV4 scoped CIDR or address we will add the file content check for IPV4, 942 // as does the IPV6. 943 IPV4Check := false 944 IPV6Check := false 945 cidrs := strings.Split(cfg.Networking.ServiceSubnet, ",") 946 for _, cidr := range cidrs { 947 checks = append(checks, HTTPProxyCIDRCheck{Proto: "https", CIDR: cidr}) 948 if !IPV4Check && netutils.IsIPv4CIDRString(cidr) { 949 IPV4Check = true 950 } 951 if !IPV6Check && netutils.IsIPv6CIDRString(cidr) { 952 IPV6Check = true 953 } 954 955 } 956 cidrs = strings.Split(cfg.Networking.PodSubnet, ",") 957 for _, cidr := range cidrs { 958 checks = append(checks, HTTPProxyCIDRCheck{Proto: "https", CIDR: cidr}) 959 if !IPV4Check && netutils.IsIPv4CIDRString(cidr) { 960 IPV4Check = true 961 } 962 if !IPV6Check && netutils.IsIPv6CIDRString(cidr) { 963 IPV6Check = true 964 } 965 } 966 967 if !isSecondaryControlPlane { 968 checks = addCommonChecks(execer, cfg.KubernetesVersion, &cfg.NodeRegistration, checks) 969 970 // Check if Bridge-netfilter and IPv6 relevant flags are set 971 if ip := netutils.ParseIPSloppy(cfg.LocalAPIEndpoint.AdvertiseAddress); ip != nil { 972 if !IPV4Check && netutils.IsIPv4(ip) { 973 IPV4Check = true 974 } 975 if !IPV6Check && netutils.IsIPv6(ip) { 976 IPV6Check = true 977 } 978 } 979 980 if IPV4Check { 981 checks = addIPv4Checks(checks) 982 } 983 if IPV6Check { 984 checks = addIPv6Checks(checks) 985 } 986 987 // if using an external etcd 988 if cfg.Etcd.External != nil { 989 // Check external etcd version before creating the cluster 990 checks = append(checks, ExternalEtcdVersionCheck{Etcd: cfg.Etcd}) 991 } 992 } 993 994 if cfg.Etcd.Local != nil { 995 // Only do etcd related checks when required to install a local etcd 996 checks = append(checks, 997 PortOpenCheck{port: kubeadmconstants.EtcdListenClientPort}, 998 PortOpenCheck{port: kubeadmconstants.EtcdListenPeerPort}, 999 DirAvailableCheck{Path: cfg.Etcd.Local.DataDir}, 1000 ) 1001 } 1002 1003 if cfg.Etcd.External != nil && !(isSecondaryControlPlane && downloadCerts) { 1004 // Only check etcd certificates when using an external etcd and not joining with automatic download of certs 1005 if cfg.Etcd.External.CAFile != "" { 1006 checks = append(checks, FileExistingCheck{Path: cfg.Etcd.External.CAFile, Label: "ExternalEtcdClientCertificates"}) 1007 } 1008 if cfg.Etcd.External.CertFile != "" { 1009 checks = append(checks, FileExistingCheck{Path: cfg.Etcd.External.CertFile, Label: "ExternalEtcdClientCertificates"}) 1010 } 1011 if cfg.Etcd.External.KeyFile != "" { 1012 checks = append(checks, FileExistingCheck{Path: cfg.Etcd.External.KeyFile, Label: "ExternalEtcdClientCertificates"}) 1013 } 1014 } 1015 return checks, nil 1016 } 1017 1018 // RunInitNodeChecks executes all individual, applicable to control-plane node checks. 1019 // The boolean flag 'isSecondaryControlPlane' controls whether we are running checks in a --join-control-plane scenario. 1020 // The boolean flag 'downloadCerts' controls whether we should skip checks on certificates because we are downloading them. 1021 // If the flag is set to true we should skip checks already executed by RunJoinNodeChecks. 1022 func RunInitNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string], isSecondaryControlPlane bool, downloadCerts bool) error { 1023 checks, err := InitNodeChecks(execer, cfg, ignorePreflightErrors, isSecondaryControlPlane, downloadCerts) 1024 if err != nil { 1025 return err 1026 } 1027 return RunChecks(checks, os.Stderr, ignorePreflightErrors) 1028 } 1029 1030 // JoinNodeChecks returns checks specific to "kubeadm join" 1031 func JoinNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.JoinConfiguration, ignorePreflightErrors sets.Set[string]) ([]Checker, error) { 1032 // First, check if we're root separately from the other preflight checks and fail fast 1033 if err := RunRootCheckOnly(ignorePreflightErrors); err != nil { 1034 return nil, err 1035 } 1036 1037 checks := []Checker{ 1038 FileAvailableCheck{Path: filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.KubeletKubeConfigFileName)}, 1039 FileAvailableCheck{Path: filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.KubeletBootstrapKubeConfigFileName)}, 1040 } 1041 checks = addCommonChecks(execer, "", &cfg.NodeRegistration, checks) 1042 if cfg.ControlPlane == nil { 1043 checks = append(checks, FileAvailableCheck{Path: cfg.CACertPath}) 1044 } 1045 1046 if cfg.Discovery.BootstrapToken != nil { 1047 ipstr, _, err := net.SplitHostPort(cfg.Discovery.BootstrapToken.APIServerEndpoint) 1048 if err == nil { 1049 checks = append(checks, 1050 HTTPProxyCheck{Proto: "https", Host: ipstr}, 1051 ) 1052 if ip := netutils.ParseIPSloppy(ipstr); ip != nil { 1053 if netutils.IsIPv4(ip) { 1054 checks = addIPv4Checks(checks) 1055 } 1056 if netutils.IsIPv6(ip) { 1057 checks = addIPv6Checks(checks) 1058 } 1059 } 1060 } 1061 } 1062 return checks, nil 1063 } 1064 1065 // RunJoinNodeChecks executes all individual, applicable to node checks. 1066 func RunJoinNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.JoinConfiguration, ignorePreflightErrors sets.Set[string]) error { 1067 checks, err := JoinNodeChecks(execer, cfg, ignorePreflightErrors) 1068 if err != nil { 1069 return err 1070 } 1071 return RunChecks(checks, os.Stderr, ignorePreflightErrors) 1072 } 1073 1074 // addCommonChecks is a helper function to duplicate checks that are common between both the 1075 // kubeadm init and join commands 1076 func addCommonChecks(execer utilsexec.Interface, k8sVersion string, nodeReg *kubeadmapi.NodeRegistrationOptions, checks []Checker) []Checker { 1077 containerRuntime, err := utilruntime.NewContainerRuntime(execer, nodeReg.CRISocket) 1078 if err != nil { 1079 klog.Warningf("[preflight] WARNING: Couldn't create the interface used for talking to the container runtime: %v\n", err) 1080 } else { 1081 checks = append(checks, ContainerRuntimeCheck{runtime: containerRuntime}) 1082 } 1083 1084 // non-windows checks 1085 checks = addSwapCheck(checks) 1086 checks = addExecChecks(checks, execer) 1087 checks = append(checks, 1088 SystemVerificationCheck{}, 1089 HostnameCheck{nodeName: nodeReg.Name}, 1090 KubeletVersionCheck{KubernetesVersion: k8sVersion, exec: execer}, 1091 ServiceCheck{Service: "kubelet", CheckIfActive: false}, 1092 PortOpenCheck{port: kubeadmconstants.KubeletPort}) 1093 return checks 1094 } 1095 1096 // RunRootCheckOnly initializes checks slice of structs and call RunChecks 1097 func RunRootCheckOnly(ignorePreflightErrors sets.Set[string]) error { 1098 checks := []Checker{ 1099 IsPrivilegedUserCheck{}, 1100 } 1101 1102 return RunChecks(checks, os.Stderr, ignorePreflightErrors) 1103 } 1104 1105 // RunPullImagesCheck will pull images kubeadm needs if they are not found on the system 1106 func RunPullImagesCheck(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string]) error { 1107 containerRuntime, err := utilruntime.NewContainerRuntime(utilsexec.New(), cfg.NodeRegistration.CRISocket) 1108 if err != nil { 1109 return &Error{Msg: err.Error()} 1110 } 1111 1112 serialPull := true 1113 if cfg.NodeRegistration.ImagePullSerial != nil { 1114 serialPull = *cfg.NodeRegistration.ImagePullSerial 1115 } 1116 1117 checks := []Checker{ 1118 ImagePullCheck{ 1119 runtime: containerRuntime, 1120 imageList: images.GetControlPlaneImages(&cfg.ClusterConfiguration), 1121 sandboxImage: images.GetPauseImage(&cfg.ClusterConfiguration), 1122 imagePullPolicy: cfg.NodeRegistration.ImagePullPolicy, 1123 imagePullSerial: serialPull, 1124 }, 1125 } 1126 return RunChecks(checks, os.Stderr, ignorePreflightErrors) 1127 } 1128 1129 // RunChecks runs each check, displays its warnings/errors, and once all 1130 // are processed will exit if any errors occurred. 1131 func RunChecks(checks []Checker, ww io.Writer, ignorePreflightErrors sets.Set[string]) error { 1132 var errsBuffer bytes.Buffer 1133 1134 for _, c := range checks { 1135 name := c.Name() 1136 warnings, errs := c.Check() 1137 1138 if setHasItemOrAll(ignorePreflightErrors, name) { 1139 // Decrease severity of errors to warnings for this check 1140 warnings = append(warnings, errs...) 1141 errs = []error{} 1142 } 1143 1144 for _, w := range warnings { 1145 io.WriteString(ww, fmt.Sprintf("\t[WARNING %s]: %v\n", name, w)) 1146 } 1147 for _, i := range errs { 1148 errsBuffer.WriteString(fmt.Sprintf("\t[ERROR %s]: %v\n", name, i.Error())) 1149 } 1150 } 1151 if errsBuffer.Len() > 0 { 1152 return &Error{Msg: errsBuffer.String()} 1153 } 1154 return nil 1155 } 1156 1157 // setHasItemOrAll is helper function that return true if item is present in the set (case-insensitive) or special key 'all' is present 1158 func setHasItemOrAll(s sets.Set[string], item string) bool { 1159 if s.Has("all") || s.Has(strings.ToLower(item)) { 1160 return true 1161 } 1162 return false 1163 } 1164 1165 // normalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object. 1166 // It takes a URL string as input. 1167 func normalizeURLString(s string) (string, error) { 1168 u, err := url.Parse(s) 1169 if err != nil { 1170 return "", err 1171 } 1172 if len(u.Path) > 0 { 1173 u.Path = strings.ReplaceAll(u.Path, "//", "/") 1174 } 1175 return u.String(), nil 1176 }