mirror of https://github.com/k3s-io/k3s
Add kubeadm preflight check framework.
Includes checks for verifying services exist and are enabled, ports are open, directories do not exist or are empty, and required binaries are in the path.pull/6/head
parent
06c1f2ba2c
commit
b673e2d0a0
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// PreFlightCheck validates the state of the system to ensure kubeadm will be
|
||||
// successful as often as possilble.
|
||||
type PreFlightCheck interface {
|
||||
Check() (warnings []string, errors []string)
|
||||
}
|
||||
|
||||
// ServiceCheck verifies that the given service is enabled and active. If we do not
|
||||
// detect a supported init system however, all checks are skipped and a warning is
|
||||
// returned.
|
||||
type ServiceCheck struct {
|
||||
service string
|
||||
}
|
||||
|
||||
func (sc ServiceCheck) Check() (warnings []string, errors []string) {
|
||||
|
||||
initSystem := getInitSystem()
|
||||
if initSystem == nil {
|
||||
return []string{"no supported init system detected, skipping service checks"}, nil
|
||||
}
|
||||
|
||||
warnings = []string{}
|
||||
|
||||
if !initSystem.ServiceExists(sc.service) {
|
||||
warnings = append(warnings, fmt.Sprintf("%s service does not exist", sc.service))
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
if !initSystem.ServiceIsEnabled(sc.service) {
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("%s service is not enabled, please run 'systemctl enable %s.service'",
|
||||
sc.service, sc.service))
|
||||
}
|
||||
|
||||
if !initSystem.ServiceIsActive(sc.service) {
|
||||
errors = append(errors,
|
||||
fmt.Sprintf("%s service is not active, please run 'systemctl start %s.service'",
|
||||
sc.service, sc.service))
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
// PortOpenCheck ensures the given port is available for use.
|
||||
type PortOpenCheck struct {
|
||||
port int
|
||||
}
|
||||
|
||||
func (poc PortOpenCheck) Check() (warnings []string, errors []string) {
|
||||
errors = []string{}
|
||||
// TODO: Get IP from KubeadmConfig
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port))
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("Port %d is in use.", poc.port))
|
||||
}
|
||||
if ln != nil {
|
||||
ln.Close()
|
||||
}
|
||||
|
||||
return nil, errors
|
||||
}
|
||||
|
||||
// DirAvailableCheck checks if the given directory either does not exist, or
|
||||
// is empty.
|
||||
type DirAvailableCheck struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (dac DirAvailableCheck) Check() (warnings []string, errors []string) {
|
||||
errors = []string{}
|
||||
// If it doesn't exist we are good:
|
||||
if _, err := os.Stat(dac.path); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(dac.path)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("Unable to check if %s is empty: %s", dac.path, err))
|
||||
return nil, errors
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Readdirnames(1)
|
||||
if err != io.EOF {
|
||||
errors = append(errors, fmt.Sprintf("%s is not empty", dac.path))
|
||||
}
|
||||
|
||||
return nil, errors
|
||||
}
|
||||
|
||||
// InPathChecks checks if the given executable is present in the path.
|
||||
type InPathCheck struct {
|
||||
executable string
|
||||
mandatory bool
|
||||
}
|
||||
|
||||
func (ipc InPathCheck) Check() (warnings []string, errors []string) {
|
||||
_, err := exec.LookPath(ipc.executable)
|
||||
if err != nil {
|
||||
if ipc.mandatory {
|
||||
// Return as an error:
|
||||
return nil, []string{fmt.Sprintf("%s not found in system path.", ipc.executable)}
|
||||
}
|
||||
// Return as a warning:
|
||||
return []string{fmt.Sprintf("%s not found in system path.", ipc.executable)}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func RunInitMasterChecks() {
|
||||
// TODO: Some of these ports should come from kubeadm config eventually:
|
||||
checks := []PreFlightCheck{
|
||||
ServiceCheck{service: "kubelet"},
|
||||
ServiceCheck{service: "docker"},
|
||||
PortOpenCheck{port: 443},
|
||||
PortOpenCheck{port: 2379},
|
||||
PortOpenCheck{port: 8080},
|
||||
PortOpenCheck{port: 10250},
|
||||
PortOpenCheck{port: 10251},
|
||||
PortOpenCheck{port: 10252},
|
||||
DirAvailableCheck{path: "/etc/kubernetes"},
|
||||
DirAvailableCheck{path: "/var/lib/etcd"},
|
||||
DirAvailableCheck{path: "/var/lib/kubelet"},
|
||||
InPathCheck{executable: "socat", mandatory: true},
|
||||
InPathCheck{executable: "ethtool", mandatory: true},
|
||||
}
|
||||
|
||||
runChecks(checks)
|
||||
}
|
||||
|
||||
func RunJoinNodeChecks() {
|
||||
// TODO: Some of these ports should come from kubeadm config eventually:
|
||||
checks := []PreFlightCheck{
|
||||
ServiceCheck{service: "kubelet"},
|
||||
ServiceCheck{service: "docker"},
|
||||
PortOpenCheck{port: 8080},
|
||||
PortOpenCheck{port: 10250},
|
||||
PortOpenCheck{port: 10251},
|
||||
PortOpenCheck{port: 10252},
|
||||
DirAvailableCheck{path: "/etc/kubernetes"},
|
||||
DirAvailableCheck{path: "/var/lib/kubelet"},
|
||||
InPathCheck{executable: "socat", mandatory: true},
|
||||
InPathCheck{executable: "ethtool", mandatory: true},
|
||||
}
|
||||
|
||||
runChecks(checks)
|
||||
}
|
||||
|
||||
// runChecks runs each check, displays it's warnings/errors, and once all
|
||||
// are processed will exit if any errors occurred.
|
||||
func runChecks(checks []PreFlightCheck) {
|
||||
foundErrors := false
|
||||
for _, check := range checks {
|
||||
warnings, errors := check.Check()
|
||||
for _, warnMsg := range warnings {
|
||||
fmt.Printf("WARNING: %s\n", warnMsg)
|
||||
}
|
||||
for _, errMsg := range errors {
|
||||
foundErrors = true
|
||||
fmt.Printf("ERROR: %s\n", errMsg)
|
||||
}
|
||||
}
|
||||
if foundErrors {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package checks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InitSystem interface {
|
||||
|
||||
// ServiceExists ensures the service is defined for this init system.
|
||||
ServiceExists(service string) bool
|
||||
|
||||
// ServiceIsEnabled ensures the service is enabled to start on each boot.
|
||||
ServiceIsEnabled(service string) bool
|
||||
|
||||
// ServiceIsActive ensures the service is running, or attempting to run. (crash looping in the case of kubelet)
|
||||
ServiceIsActive(service string) bool
|
||||
}
|
||||
|
||||
type SystemdInitSystem struct{}
|
||||
|
||||
func (sysd SystemdInitSystem) ServiceExists(service string) bool {
|
||||
args := []string{"status", service}
|
||||
outBytes, _ := exec.Command("systemctl", args...).Output()
|
||||
output := string(outBytes)
|
||||
if strings.Contains(output, "Loaded: not-found") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (sysd SystemdInitSystem) ServiceIsEnabled(service string) bool {
|
||||
args := []string{"is-enabled", service}
|
||||
_, err := exec.Command("systemctl", args...).Output()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ServiceIsActive will check is the service is "active". In the case of
|
||||
// crash looping services (kubelet in our case) status will return as
|
||||
// "activating", so we will consider this active as well.
|
||||
func (sysd SystemdInitSystem) ServiceIsActive(service string) bool {
|
||||
args := []string{"is-active", service}
|
||||
// Ignoring error here, command returns non-0 if in "activating" status:
|
||||
outBytes, _ := exec.Command("systemctl", args...).Output()
|
||||
output := strings.TrimSpace(string(outBytes))
|
||||
if output == "active" || output == "activating" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getInitSystem returns an InitSystem for the current system, or nil
|
||||
// if we cannot detect a supported init system for pre-flight checks.
|
||||
// This indicates we will skip init system checks, not an error.
|
||||
func getInitSystem() InitSystem {
|
||||
// Assume existence of systemctl in path implies this is a systemd system:
|
||||
_, err := exec.LookPath("systemctl")
|
||||
if err == nil {
|
||||
return &SystemdInitSystem{}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubechecks "k8s.io/kubernetes/cmd/kubeadm/app/checks"
|
||||
kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master"
|
||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
|
@ -49,6 +50,7 @@ var (
|
|||
func NewCmdInit(out io.Writer) *cobra.Command {
|
||||
cfg := &kubeadmapi.MasterConfiguration{}
|
||||
var cfgPath string
|
||||
var skipChecks bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Run this in order to set up the Kubernetes master.",
|
||||
|
@ -58,7 +60,7 @@ func NewCmdInit(out io.Writer) *cobra.Command {
|
|||
cmdutil.CheckErr(fmt.Errorf("<cmd/init> %v", err))
|
||||
}
|
||||
}
|
||||
i, err := NewInit(cfgPath, cfg)
|
||||
i, err := NewInit(cfgPath, cfg, skipChecks)
|
||||
check(err)
|
||||
check(i.Run(out))
|
||||
},
|
||||
|
@ -124,6 +126,10 @@ func NewCmdInit(out io.Writer) *cobra.Command {
|
|||
"etcd client key file. Note: The path must be in /etc/ssl/certs",
|
||||
)
|
||||
cmd.PersistentFlags().MarkDeprecated("external-etcd-keyfile", "this flag will be removed when componentconfig exists")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&skipChecks, "skip-checks", false,
|
||||
"skip checks normally run before modifying the system",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -132,7 +138,7 @@ type Init struct {
|
|||
cfg *kubeadmapi.MasterConfiguration
|
||||
}
|
||||
|
||||
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration) (*Init, error) {
|
||||
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipChecks bool) (*Init, error) {
|
||||
if cfgPath != "" {
|
||||
b, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
|
@ -142,6 +148,14 @@ func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration) (*Init, error)
|
|||
return nil, fmt.Errorf("unable to decode config from %q [%v]", cfgPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !skipChecks {
|
||||
fmt.Println("Running pre-flight checks")
|
||||
kubechecks.RunInitMasterChecks()
|
||||
} else {
|
||||
fmt.Println("Skipping pre-flight checks")
|
||||
}
|
||||
|
||||
// Auto-detect the IP
|
||||
if len(cfg.API.AdvertiseAddresses) == 0 {
|
||||
// TODO(phase1+) perhaps we could actually grab eth0 and eth1
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubechecks "k8s.io/kubernetes/cmd/kubeadm/app/checks"
|
||||
kubenode "k8s.io/kubernetes/cmd/kubeadm/app/node"
|
||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
|
@ -43,11 +44,12 @@ var (
|
|||
// NewCmdJoin returns "kubeadm join" command.
|
||||
func NewCmdJoin(out io.Writer) *cobra.Command {
|
||||
cfg := &kubeadmapi.NodeConfiguration{}
|
||||
var skipChecks bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "join",
|
||||
Short: "Run this on any machine you wish to join an existing cluster.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := RunJoin(out, cmd, args, cfg)
|
||||
err := RunJoin(out, cmd, args, cfg, skipChecks)
|
||||
cmdutil.CheckErr(err)
|
||||
},
|
||||
}
|
||||
|
@ -57,11 +59,24 @@ func NewCmdJoin(out io.Writer) *cobra.Command {
|
|||
"(required) Shared secret used to secure bootstrap. Must match the output of 'kubeadm init'",
|
||||
)
|
||||
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&skipChecks, "skip-checks", false,
|
||||
"skip checks normally run before modifying the system",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunJoin executes worked node provisioning and tries to join an existing cluster.
|
||||
func RunJoin(out io.Writer, cmd *cobra.Command, args []string, s *kubeadmapi.NodeConfiguration) error {
|
||||
func RunJoin(out io.Writer, cmd *cobra.Command, args []string, s *kubeadmapi.NodeConfiguration, skipChecks bool) error {
|
||||
|
||||
if !skipChecks {
|
||||
fmt.Println("Running pre-flight checks")
|
||||
kubechecks.RunJoinNodeChecks()
|
||||
} else {
|
||||
fmt.Println("Skipping pre-flight checks")
|
||||
}
|
||||
|
||||
// TODO(phase1+) this we are missing args from the help text, there should be a way to tell cobra about it
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("<cmd/join> must specify master IP address (see --help)")
|
||||
|
|
Loading…
Reference in New Issue