kubeadm implement preflight checks

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.

Checks that user running kubeamd init and join is root and will only execute
command if user is root. Moved away from using kubectl error handling to
having kubeadm handle its own errors. This should allow kubeadm to have
more meaningful errors, exit codes, and logging for specific kubeadm use
cases.
This commit is contained in:
Derek McQuay 2016-10-04 13:45:24 -07:00 committed by Devan Goodwin
parent b673e2d0a0
commit 16b159c12b
7 changed files with 205 additions and 68 deletions

View File

@ -25,8 +25,8 @@ 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"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/cloudprovider"
@ -50,7 +50,7 @@ var (
func NewCmdInit(out io.Writer) *cobra.Command {
cfg := &kubeadmapi.MasterConfiguration{}
var cfgPath string
var skipChecks bool
var skipPreFlight bool
cmd := &cobra.Command{
Use: "init",
Short: "Run this in order to set up the Kubernetes master.",
@ -60,7 +60,7 @@ func NewCmdInit(out io.Writer) *cobra.Command {
cmdutil.CheckErr(fmt.Errorf("<cmd/init> %v", err))
}
}
i, err := NewInit(cfgPath, cfg, skipChecks)
i, err := NewInit(cfgPath, cfg, skipPreFlight)
check(err)
check(i.Run(out))
},
@ -127,8 +127,8 @@ func NewCmdInit(out io.Writer) *cobra.Command {
)
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",
&skipPreFlight, "skip-preflight-checks", false,
"skip preflight checks normally run before modifying the system",
)
return cmd
@ -138,7 +138,7 @@ type Init struct {
cfg *kubeadmapi.MasterConfiguration
}
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipChecks bool) (*Init, error) {
func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipPreFlight bool) (*Init, error) {
if cfgPath != "" {
b, err := ioutil.ReadFile(cfgPath)
if err != nil {
@ -149,9 +149,12 @@ func NewInit(cfgPath string, cfg *kubeadmapi.MasterConfiguration, skipChecks boo
}
}
if !skipChecks {
if !skipPreFlight {
fmt.Println("Running pre-flight checks")
kubechecks.RunInitMasterChecks()
err := preflight.RunInitMasterChecks()
if err != nil {
return nil, err
}
} else {
fmt.Println("Skipping pre-flight checks")
}

View File

@ -24,10 +24,9 @@ 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"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
var (
@ -44,13 +43,13 @@ var (
// NewCmdJoin returns "kubeadm join" command.
func NewCmdJoin(out io.Writer) *cobra.Command {
cfg := &kubeadmapi.NodeConfiguration{}
var skipChecks bool
var skipPreFlight 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, skipChecks)
cmdutil.CheckErr(err)
err := RunJoin(out, cmd, args, cfg, skipPreFlight)
kubeadmutil.CheckErr(err)
},
}
@ -60,24 +59,26 @@ func NewCmdJoin(out io.Writer) *cobra.Command {
)
cmd.PersistentFlags().BoolVar(
&skipChecks, "skip-checks", false,
"skip checks normally run before modifying the system",
&skipPreFlight, "skip-preflight-checks", false,
"skip preflight 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, skipChecks bool) error {
if !skipChecks {
func RunJoin(out io.Writer, cmd *cobra.Command, args []string, s *kubeadmapi.NodeConfiguration, skipPreFlight bool) error {
// TODO(phase1+) this we are missing args from the help text, there should be a way to tell cobra about it
if !skipPreFlight {
fmt.Println("Running pre-flight checks")
kubechecks.RunJoinNodeChecks()
err := preflight.RunJoinNodeChecks()
if err != nil {
return err
}
} 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)")
}

View File

@ -19,7 +19,6 @@ package app
import (
"os"
"github.com/renstrom/dedent"
"github.com/spf13/pflag"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd"
@ -27,12 +26,6 @@ import (
"k8s.io/kubernetes/pkg/util/logs"
)
var AlphaWarningOnExit = dedent.Dedent(`
kubeadm: I am an alpha version, my authors welcome your feedback and bug reports
kubeadm: please create an issue using https://github.com/kubernetes/kubernetes/issues/new
kubeadm: and make sure to mention @kubernetes/sig-cluster-lifecycle. Thank you!
`)
func Run() error {
logs.InitLogs()
defer logs.FlushLogs()

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package checks
package preflight
import (
"fmt"
@ -22,12 +22,23 @@ import (
"net"
"os"
"os/exec"
"k8s.io/kubernetes/pkg/util/initsystem"
)
type PreFlightError struct {
Msg string
Count int
}
func (e *PreFlightError) Error() string {
return fmt.Sprintf("preflight check error\n count: %d \n msg: %s", e.Count, e.Msg)
}
// 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)
Check() (warnings, errors []error)
}
// ServiceCheck verifies that the given service is enabled and active. If we do not
@ -37,29 +48,29 @@ type ServiceCheck struct {
service string
}
func (sc ServiceCheck) Check() (warnings []string, errors []string) {
func (sc ServiceCheck) Check() (warnings, errors []error) {
initSystem := getInitSystem()
initSystem := initsystem.GetInitSystem()
if initSystem == nil {
return []string{"no supported init system detected, skipping service checks"}, nil
return []error{fmt.Errorf("no supported init system detected, skipping service checks for %s", sc.service)}, nil
}
warnings = []string{}
warnings = []error{}
if !initSystem.ServiceExists(sc.service) {
warnings = append(warnings, fmt.Sprintf("%s service does not exist", sc.service))
warnings = append(warnings, fmt.Errorf("%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'",
fmt.Errorf("%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'",
fmt.Errorf("%s service is not active, please run 'systemctl start %s.service'",
sc.service, sc.service))
}
@ -71,12 +82,12 @@ type PortOpenCheck struct {
port int
}
func (poc PortOpenCheck) Check() (warnings []string, errors []string) {
errors = []string{}
func (poc PortOpenCheck) Check() (warnings, errors []error) {
errors = []error{}
// 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))
errors = append(errors, fmt.Errorf("Port %d is in use", poc.port))
}
if ln != nil {
ln.Close()
@ -85,14 +96,28 @@ func (poc PortOpenCheck) Check() (warnings []string, errors []string) {
return nil, errors
}
// IsRootCheck verifies user is root
type IsRootCheck struct {
root bool
}
func (irc IsRootCheck) Check() (warnings, errors []error) {
errors = []error{}
if os.Getuid() != 0 {
errors = append(errors, fmt.Errorf("user is not running as root"))
}
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{}
func (dac DirAvailableCheck) Check() (warnings, errors []error) {
errors = []error{}
// If it doesn't exist we are good:
if _, err := os.Stat(dac.path); os.IsNotExist(err) {
return nil, nil
@ -100,14 +125,14 @@ func (dac DirAvailableCheck) Check() (warnings []string, errors []string) {
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))
errors = append(errors, fmt.Errorf("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))
errors = append(errors, fmt.Errorf("%s is not empty", dac.path))
}
return nil, errors
@ -119,22 +144,23 @@ type InPathCheck struct {
mandatory bool
}
func (ipc InPathCheck) Check() (warnings []string, errors []string) {
func (ipc InPathCheck) Check() (warnings, errors []error) {
_, 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 nil, []error{fmt.Errorf("%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 []error{fmt.Errorf("%s not found in system path", ipc.executable)}, nil
}
return nil, nil
}
func RunInitMasterChecks() {
func RunInitMasterChecks() error {
// TODO: Some of these ports should come from kubeadm config eventually:
checks := []PreFlightCheck{
IsRootCheck{root: true},
ServiceCheck{service: "kubelet"},
ServiceCheck{service: "docker"},
PortOpenCheck{port: 443},
@ -146,46 +172,68 @@ func RunInitMasterChecks() {
DirAvailableCheck{path: "/etc/kubernetes"},
DirAvailableCheck{path: "/var/lib/etcd"},
DirAvailableCheck{path: "/var/lib/kubelet"},
InPathCheck{executable: "socat", mandatory: true},
InPathCheck{executable: "ebtables", mandatory: true},
InPathCheck{executable: "ethtool", mandatory: true},
InPathCheck{executable: "ip", mandatory: true},
InPathCheck{executable: "iptables", mandatory: true},
InPathCheck{executable: "mount", mandatory: true},
InPathCheck{executable: "nsenter", mandatory: true},
InPathCheck{executable: "socat", mandatory: true},
InPathCheck{executable: "tc", mandatory: false},
InPathCheck{executable: "touch", mandatory: false},
}
runChecks(checks)
return runChecks(checks)
}
func RunJoinNodeChecks() {
func RunJoinNodeChecks() error {
// TODO: Some of these ports should come from kubeadm config eventually:
checks := []PreFlightCheck{
ServiceCheck{service: "kubelet"},
IsRootCheck{root: true},
ServiceCheck{service: "docker"},
ServiceCheck{service: "kubelet"},
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: "ebtables", mandatory: true},
InPathCheck{executable: "ethtool", mandatory: true},
InPathCheck{executable: "ip", mandatory: true},
InPathCheck{executable: "iptables", mandatory: true},
InPathCheck{executable: "mount", mandatory: true},
InPathCheck{executable: "nsenter", mandatory: true},
InPathCheck{executable: "socat", mandatory: true},
InPathCheck{executable: "tc", mandatory: false},
InPathCheck{executable: "touch", mandatory: false},
}
runChecks(checks)
return 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)
func runChecks(checks []PreFlightCheck) error {
found := []error{}
for _, c := range checks {
warnings, errors := c.Check()
for _, w := range warnings {
fmt.Printf("WARNING: %s\n", w)
}
for _, errMsg := range errors {
foundErrors = true
fmt.Printf("ERROR: %s\n", errMsg)
for _, e := range errors {
found = append(found, e)
}
}
if foundErrors {
os.Exit(1)
if len(found) > 0 {
errors := "\n"
for _, i := range found {
errors += "\t" + i.Error() + "\n"
}
return &PreFlightError{
Msg: errors,
Count: len(found),
}
}
return nil
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2014 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 util
import (
"fmt"
"os"
"strings"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight"
"github.com/golang/glog"
"github.com/renstrom/dedent"
)
const (
DefaultErrorExitCode = 1
PreFlight = 2
)
var AlphaWarningOnExit = dedent.Dedent(`
kubeadm: I am an alpha version, my authors welcome your feedback and bug reports
kubeadm: please create issue an using https://github.com/kubernetes/kubernetes/issues/new
kubeadm: and make sure to mention @kubernetes/sig-cluster-lifecycle. Thank you!
`)
type debugError interface {
DebugError() (msg string, args []interface{})
}
var fatalErrHandler = fatal
// BehaviorOnFatal allows you to override the default behavior when a fatal
// error occurs, which is to call os.Exit(code). You can pass 'panic' as a function
// here if you prefer the panic() over os.Exit(1).
func BehaviorOnFatal(f func(string, int)) {
fatalErrHandler = f
}
// fatal prints the message if set and then exits. If V(2) or greater, glog.Fatal
// is invoked for extended information.
func fatal(msg string, code int) {
if len(msg) > 0 {
// add newline if needed
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
if glog.V(2) {
glog.FatalDepth(2, msg)
}
fmt.Fprint(os.Stderr, msg)
}
os.Exit(code)
}
// CheckErr prints a user friendly error to STDERR and exits with a non-zero
// exit code. Unrecognized errors will be printed with an "error: " prefix.
//
// This method is generic to the command in use and may be used by non-Kubectl
// commands.
func CheckErr(err error) {
checkErr("", err, fatalErrHandler)
}
// checkErr formats a given error as a string and calls the passed handleErr
// func with that string and an kubectl exit code.
func checkErr(prefix string, err error, handleErr func(string, int)) {
switch err.(type) {
case nil:
return
case *preflight.PreFlightError:
handleErr(err.Error(), PreFlight)
default:
fmt.Printf(AlphaWarningOnExit)
handleErr(err.Error(), DefaultErrorExitCode)
}
}

View File

@ -21,12 +21,12 @@ import (
"os"
"k8s.io/kubernetes/cmd/kubeadm/app"
"k8s.io/kubernetes/cmd/kubeadm/app/util"
)
// TODO(phase1+): check for root
func main() {
if err := app.Run(); err != nil {
fmt.Printf(app.AlphaWarningOnExit)
fmt.Printf(util.AlphaWarningOnExit)
os.Exit(1)
}
os.Exit(0)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package checks
package initsystem
import (
"fmt"
@ -73,7 +73,7 @@ func (sysd SystemdInitSystem) ServiceIsActive(service string) bool {
// 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 {
func GetInitSystem() InitSystem {
// Assume existence of systemctl in path implies this is a systemd system:
_, err := exec.LookPath("systemctl")
if err == nil {