diff --git a/cmd/minikube/cmd/start.go b/cmd/minikube/cmd/start.go index b8c3dbdf526f..711fde4b1e64 100644 --- a/cmd/minikube/cmd/start.go +++ b/cmd/minikube/cmd/start.go @@ -525,14 +525,14 @@ func bootstrapCluster(bs bootstrapper.Bootstrapper, r cruntime.Manager, runner b if preexisting { console.OutStyle("restarting", "Relaunching Kubernetes %s using %s ... ", kc.KubernetesVersion, bsName) if err := bs.RestartCluster(kc); err != nil { - exit.WithProblems("Error restarting cluster", err, logs.FindProblems(r, bs, runner)) + exit.WithLogEntries("Error restarting cluster", err, logs.FindProblems(r, bs, runner)) } return } console.OutStyle("launch", "Launching Kubernetes %s using %s ... ", kc.KubernetesVersion, bsName) if err := bs.StartCluster(kc); err != nil { - exit.WithProblems("Error starting cluster", err, logs.FindProblems(r, bs, runner)) + exit.WithLogEntries("Error starting cluster", err, logs.FindProblems(r, bs, runner)) } } @@ -549,7 +549,7 @@ func validateCluster(bs bootstrapper.Bootstrapper, r cruntime.Manager, runner bo } err := pkgutil.RetryAfter(20, k8sStat, 3*time.Second) if err != nil { - exit.WithProblems("kubelet checks failed", err, logs.FindProblems(r, bs, runner)) + exit.WithLogEntries("kubelet checks failed", err, logs.FindProblems(r, bs, runner)) } aStat := func() (err error) { st, err := bs.GetAPIServerStatus(net.ParseIP(ip)) @@ -562,7 +562,7 @@ func validateCluster(bs bootstrapper.Bootstrapper, r cruntime.Manager, runner bo err = pkgutil.RetryAfter(30, aStat, 10*time.Second) if err != nil { - exit.WithProblems("apiserver checks failed", err, logs.FindProblems(r, bs, runner)) + exit.WithLogEntries("apiserver checks failed", err, logs.FindProblems(r, bs, runner)) } console.OutLn("") } diff --git a/pkg/minikube/console/style.go b/pkg/minikube/console/style.go index 1a7f45636f29..04893b8ed20d 100644 --- a/pkg/minikube/console/style.go +++ b/pkg/minikube/console/style.go @@ -66,6 +66,9 @@ var styles = map[string]style{ "log-entry": {Prefix: " "}, // Indent "crushed": {Prefix: "💔 "}, "url": {Prefix: "👉 "}, + "documentation": {Prefix: "🗎 "}, + "issues": {Prefix: "📚 "}, + "issue": {Prefix: " ▪ "}, // Indented bullet // Specialized purpose styles "iso-download": {Prefix: "💿 ", LowPrefix: "@ "}, diff --git a/pkg/minikube/exit/exit.go b/pkg/minikube/exit/exit.go index afff9bf0c83a..c20139f93421 100644 --- a/pkg/minikube/exit/exit.go +++ b/pkg/minikube/exit/exit.go @@ -20,11 +20,10 @@ package exit import ( "fmt" "os" - "strings" "github.com/golang/glog" - "github.com/pkg/errors" "k8s.io/minikube/pkg/minikube/console" + "k8s.io/minikube/pkg/minikube/problem" ) // Exit codes based on sysexits(3) @@ -40,8 +39,8 @@ const ( Config = 78 // Config represents an unconfigured or misconfigured state Permissions = 77 // Permissions represents a permissions error - // MaxProblems controls the number of problems to show for each source - MaxProblems = 3 + // MaxLogEntries controls the number of log entries to show for each source + MaxLogEntries = 3 ) // Usage outputs a usage error and exits with error code 64 @@ -60,21 +59,33 @@ func WithCode(code int, format string, a ...interface{}) { // WithError outputs an error and exits. func WithError(msg string, err error) { + p := problem.FromError(err) + if p != nil { + WithProblem(msg, p) + } displayError(msg, err) - // Here is where we would insert code to optionally upload a stack trace. - - // We can be smarter about guessing exit codes, but EX_SOFTWARE should suffice. os.Exit(Software) } -// WithProblems outputs an error along with any autodetected problems, and exits. -func WithProblems(msg string, err error, problems map[string][]string) { +// WithProblem outputs info related to a known problem and exits. +func WithProblem(msg string, p *problem.Problem) { + console.Err("\n") + console.Fatal(msg) + p.Display() + console.Err("\n") + console.ErrStyle("sad", "If the advice does not help, please let us know: ") + console.ErrStyle("url", "https://github.com/kubernetes/minikube/issues/new") + os.Exit(Config) +} + +// WithLogEntries outputs an error along with any important log entries, and exits. +func WithLogEntries(msg string, err error, entries map[string][]string) { displayError(msg, err) - for name, lines := range problems { + for name, lines := range entries { console.OutStyle("failure", "Problems detected in %q:", name) - if len(lines) > MaxProblems { - lines = lines[:MaxProblems] + if len(lines) > MaxLogEntries { + lines = lines[:MaxLogEntries] } for _, l := range lines { console.OutStyle("log-entry", l) @@ -86,17 +97,9 @@ func WithProblems(msg string, err error, problems map[string][]string) { func displayError(msg string, err error) { // use Warning because Error will display a duplicate message to stderr glog.Warningf(fmt.Sprintf("%s: %v", msg, err)) + console.Err("\n") console.Fatal(msg+": %v", err) console.Err("\n") - // unfortunately Cause only supports one level of actual error wrapping - cause := errors.Cause(err) - text := cause.Error() - if strings.Contains(text, "VBoxManage not found. Make sure VirtualBox is installed and VBoxManage is in the path") || - strings.Contains(text, "Driver \"kvm2\" not found. Do you have the plugin binary \"docker-machine-driver-kvm2\" accessible in your PATH?") { - console.ErrStyle("usage", "Make sure to install all necessary requirements, according to the documentation:") - console.ErrStyle("url", "https://kubernetes.io/docs/tasks/tools/install-minikube/") - } else { - console.ErrStyle("sad", "Sorry that minikube crashed. If this was unexpected, we would love to hear from you:") - console.ErrStyle("url", "https://github.com/kubernetes/minikube/issues/new") - } + console.ErrStyle("sad", "Sorry that minikube crashed. If this was unexpected, we would love to hear from you:") + console.ErrStyle("url", "https://github.com/kubernetes/minikube/issues/new") } diff --git a/pkg/minikube/problem/err_map.go b/pkg/minikube/problem/err_map.go new file mode 100644 index 000000000000..da9abeae0c34 --- /dev/null +++ b/pkg/minikube/problem/err_map.go @@ -0,0 +1,143 @@ +/* +Copyright 2019 The Kubernetes Authors All rights reserved. + +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 problem + +import "regexp" + +// re is a shortcut around regexp.MustCompile +func re(s string) *regexp.Regexp { + return regexp.MustCompile(s) +} + +// vmProblems are VM related problems +var vmProblems = map[string]match{ + "VBOX_NOT_FOUND": { + Regexp: re(`VBoxManage not found. Make sure VirtualBox is installed and VBoxManage is in the path`), + Advice: "Install VirtualBox, ensure that VBoxManage is executable and in path, or select an alternative value for --vm-driver", + URL: "https://www.virtualbox.org/wiki/Downloads", + Issues: []int{3784, 3776}, + }, + "VBOX_VTX_DISABLED": { + Regexp: re(`This computer doesn't have VT-X/AMD-v enabled`), + Advice: "In some environments, this message is incorrect. Try 'minikube start --no-vtx-check'", + Issues: []int{3900}, + }, + "KVM2_NOT_FOUND": { + Regexp: re(`Driver "kvm2" not found. Do you have the plugin binary .* accessible in your PATH`), + Advice: "Please install the minikube kvm2 VM driver, or select an alternative --vm-driver", + URL: "https://github.com/kubernetes/minikube/blob/master/docs/drivers.md#kvm2-driver", + }, + "KVM2_NO_IP": { + Regexp: re(`Error starting stopped host: Machine didn't return an IP after 120 seconds`), + Advice: "The KVM driver is unable to ressurect this old VM. Please run `minikube delete` to delete it and try again.", + Issues: []int{3901, 3566, 3434}, + }, + "VM_DOES_NOT_EXIST": { + Regexp: re(`Error getting state for host: machine does not exist`), + Advice: "Your system no longer knows about the VM previously created by minikube. Run 'minikube delete' to reset your local state.", + Issues: []int{3864}, + }, + "VM_IP_NOT_FOUND": { + Regexp: re(`Error getting ssh host name for driver: IP not found`), + Advice: "The minikube VM is offline. Please run 'minikube start' to start it again.", + Issues: []int{3849, 3648}, + }, +} + +// proxyDoc is the URL to proxy documentation +const proxyDoc = "https://github.com/kubernetes/minikube/blob/master/docs/http_proxy.md" + +// netProblems are network related problems. +var netProblems = map[string]match{ + "GCR_UNAVAILABLE": { + Regexp: re(`gcr.io.*443: connect: invalid argument`), + Advice: "minikube is unable to access the Google Container Registry. You may need to configure it to use a HTTP proxy.", + URL: proxyDoc, + Issues: []int{3860}, + }, + "DOWNLOAD_RESET_BY_PEER": { + Regexp: re(`Error downloading .*connection reset by peer`), + Advice: "A firewall is likely blocking minikube from reaching the internet. You may need to configure minikube to use a proxy.", + URL: proxyDoc, + Issues: []int{3909}, + }, + "DOWNLOAD_IO_TIMEOUT": { + Regexp: re(`Error downloading .*timeout`), + Advice: "A firewall is likely blocking minikube from reaching the internet. You may need to configure minikube to use a proxy.", + URL: proxyDoc, + Issues: []int{3846}, + }, + "DOWNLOAD_TLS_OVERSIZED": { + Regexp: re(`failed to download.*tls: oversized record received with length`), + Advice: "A firewall is interfering with minikube's ability to make outgoing HTTPS requests. You may need to configure minikube to use a proxy.", + URL: proxyDoc, + Issues: []int{3857, 3759}, + }, + "ISO_DOWNLOAD_FAILED": { + Regexp: re(`iso: failed to download`), + Advice: "A firewall is likely blocking minikube from reaching the internet. You may need to configure minikube to use a proxy.", + URL: proxyDoc, + Issues: []int{3922}, + }, + "PULL_TIMEOUT_EXCEEDED": { + Regexp: re(`failed to pull image k8s.gcr.io.*Client.Timeout exceeded while awaiting headers`), + Advice: "A firewall is blocking Docker within the minikube VM from reaching the internet. You may need to configure it to use a proxy.", + URL: proxyDoc, + Issues: []int{3898}, + }, + "SSH_AUTH_FAILURE": { + Regexp: re(`ssh: handshake failed: ssh: unable to authenticate.*, no supported methods remain`), + Advice: "Your host is failing to route packets to the minikube VM. If you have VPN software, try turning it off or configuring it so that it does not re-route traffic to the VM IP. If not, check your VM environment routing options.", + Issues: []int{3930}, + }, + "SSH_TCP_FAILURE": { + Regexp: re(`dial tcp .*:22: connectex: A connection attempt failed because the connected party did not properly respond`), + Advice: "Your host is failing to route packets to the minikube VM. If you have VPN software, try turning it off or configuring it so that it does not re-route traffic to the VM IP. If not, check your VM environment routing options.", + Issues: []int{3388}, + }, + "INVALID_PROXY_HOSTNAME": { + Regexp: re(`dial tcp: lookup.*: no such host`), + Advice: "Verify that your HTTP_PROXY and HTTPS_PROXY environment variables are set correctly.", + URL: proxyDoc, + }, +} + +// deployProblems are Kubernetes deployment problems. +var deployProblems = map[string]match{ + "DOCKER_UNAVAILABLE": { + Regexp: re(`Error configuring auth on host: OS type not recognized`), + Advice: "Docker inside the VM is unavailable. Try running 'minikube delete' to reset the VM.", + Issues: []int{3952}, + }, + "INVALID_KUBERNETES_VERSION": { + Regexp: re(`No Major.Minor.Patch elements found`), + Advice: "Specify --kubernetes-version in v. form. example: 'v1.1.14'", + }, + "KUBERNETES_VERSION_MISSING_V": { + Regexp: re(`strconv.ParseUint: parsing "": invalid syntax`), + Advice: "Check that your --kubernetes-version has a leading 'v'. For example: 'v1.1.14'", + }, +} + +// osProblems are operating-system specific issues +var osProblems = map[string]match{ + "NON_C_DRIVE": { + Regexp: re(`.iso: The system cannot find the path specified.`), + Advice: "Run minikube from the C: drive.", + Issues: []int{1574}, + }, +} diff --git a/pkg/minikube/problem/problem.go b/pkg/minikube/problem/problem.go new file mode 100644 index 000000000000..a9a6b40ff079 --- /dev/null +++ b/pkg/minikube/problem/problem.go @@ -0,0 +1,87 @@ +/* +Copyright 2019 The Kubernetes Authors All rights reserved. + +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 problem helps deliver actionable feedback to a user based on an error message. +package problem + +import ( + "regexp" + + "k8s.io/minikube/pkg/minikube/console" +) + +const issueBase = "https://github.com/kubernetes/minikube/issue" + +// Problem represents a known problem in minikube. +type Problem struct { + ID string + Err error + Advice string + URL string + Issues []int +} + +// match maps a regular expression to problem metadata. +type match struct { + Regexp *regexp.Regexp + Advice string + URL string + Issues []int +} + +// Display problem metadata to the console +func (p *Problem) Display() { + console.ErrStyle("failure", "Error: [%s] %v", p.ID, p.Err) + console.ErrStyle("tip", "Advice: %s", p.Advice) + if p.URL != "" { + console.ErrStyle("documentation", "Documentation: %s", p.URL) + } + if len(p.Issues) == 0 { + return + } + console.ErrStyle("issues", "Related issues:") + issues := p.Issues + if len(issues) > 3 { + issues = issues[0:3] + } + for _, i := range issues { + console.ErrStyle("issue", "%s/%d", issueBase, i) + } +} + +// FromError returns a known problem from an error. +func FromError(err error) *Problem { + maps := []map[string]match{ + osProblems, + vmProblems, + netProblems, + deployProblems, + } + for _, m := range maps { + for k, v := range m { + if v.Regexp.MatchString(err.Error()) { + return &Problem{ + Err: err, + Advice: v.Advice, + URL: v.URL, + ID: k, + Issues: v.Issues, + } + } + } + } + return nil +}