diff --git a/cmd/minikube/cmd/dashboard.go b/cmd/minikube/cmd/dashboard.go index 362e888694..3372a82cb4 100644 --- a/cmd/minikube/cmd/dashboard.go +++ b/cmd/minikube/cmd/dashboard.go @@ -17,68 +17,138 @@ limitations under the License. package cmd import ( + "bufio" "fmt" + "net/http" "os" - "text/template" + "os/exec" + "regexp" "time" "github.com/golang/glog" "github.com/pkg/browser" + "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/minikube/pkg/minikube/cluster" + "k8s.io/minikube/pkg/minikube/config" "k8s.io/minikube/pkg/minikube/machine" "k8s.io/minikube/pkg/minikube/service" - commonutil "k8s.io/minikube/pkg/util" + "k8s.io/minikube/pkg/util" ) var ( dashboardURLMode bool + // Matches: 127.0.0.1:8001 + // TODO(tstromberg): Get kubectl to implement a stable supported output format. + hostPortRe = regexp.MustCompile(`127.0.0.1:\d{4,}`) ) // dashboardCmd represents the dashboard command var dashboardCmd = &cobra.Command{ Use: "dashboard", - Short: "Opens/displays the kubernetes dashboard URL for your local cluster", - Long: `Opens/displays the kubernetes dashboard URL for your local cluster`, + Short: "Access the kubernetes dashboard running within the minikube cluster", + Long: `Access the kubernetes dashboard running within the minikube cluster`, Run: func(cmd *cobra.Command, args []string) { api, err := machine.NewAPIClient() + defer func() { + err := api.Close() + if err != nil { + glog.Warningf("Failed to close API: %v", err) + } + }() + if err != nil { - fmt.Fprintf(os.Stderr, "Error getting client: %s\n", err) + fmt.Fprintf(os.Stderr, "Error creating client: %v\n", err) os.Exit(1) } - defer api.Close() - cluster.EnsureMinikubeRunningOrExit(api, 1) - namespace := "kube-system" + + ns := "kube-system" svc := "kubernetes-dashboard" - - if err = commonutil.RetryAfter(20, func() error { return service.CheckService(namespace, svc) }, 6*time.Second); err != nil { - fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", svc, err) + if err = util.RetryAfter(30, func() error { return service.CheckService(ns, svc) }, 1*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "%s:%s is not running: %v\n", ns, svc, err) os.Exit(1) } - urls, err := service.GetServiceURLsForService(api, namespace, svc, template.Must(template.New("dashboardServiceFormat").Parse(defaultServiceFormatTemplate))) + p, hostPort, err := kubectlProxy() if err != nil { - fmt.Fprintln(os.Stderr, err) - fmt.Fprintln(os.Stderr, "Check that minikube is running.") - os.Exit(1) - } - if len(urls) == 0 { - errMsg := "There appears to be no url associated with dashboard, this is not expected, exiting" - glog.Infoln(errMsg) + glog.Fatalf("kubectl proxy: %v", err) + } + url := dashboardURL(hostPort, ns, svc) + + if err = util.RetryAfter(60, func() error { return checkURL(url) }, 1*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "%s is not responding properly: %v\n", url, err) os.Exit(1) } + if dashboardURLMode { - fmt.Fprintln(os.Stdout, urls[0]) + fmt.Fprintln(os.Stdout, url) } else { - fmt.Fprintln(os.Stdout, "Opening kubernetes dashboard in default browser...") - browser.OpenURL(urls[0]) + fmt.Fprintln(os.Stdout, fmt.Sprintf("Opening %s in your default browser...", url)) + if err = browser.OpenURL(url); err != nil { + fmt.Fprintf(os.Stderr, fmt.Sprintf("failed to open browser: %v", err)) + } + } + + glog.Infof("Waiting forever for kubectl proxy to exit ...") + if err = p.Wait(); err != nil { + glog.Errorf("Wait: %v", err) } }, } +// kubectlProxy runs "kubectl proxy", returning host:port +func kubectlProxy() (*exec.Cmd, string, error) { + path, err := exec.LookPath("kubectl") + if err != nil { + return nil, "", errors.Wrap(err, "kubectl not found in PATH") + } + + // port=0 picks a random system port + // config.GetMachineName() respects the -p (profile) flag + cmd := exec.Command(path, "--context", config.GetMachineName(), "proxy", "--port=0") + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, "", errors.Wrap(err, "cmd stdout") + } + + glog.Infof("Executing: %s %s", cmd.Path, cmd.Args) + if err := cmd.Start(); err != nil { + return nil, "", errors.Wrap(err, "proxy start") + } + reader := bufio.NewReader(stdoutPipe) + glog.Infof("proxy started, reading stdout pipe ...") + out, err := reader.ReadString('\n') + if err != nil { + return nil, "", errors.Wrap(err, "reading stdout pipe") + } + glog.Infof("proxy stdout: %s", out) + return cmd, hostPortRe.FindString(out), nil +} + +// dashboardURL generates a URL for accessing the dashboard service +func dashboardURL(proxy string, ns string, svc string) string { + // Reference: https://github.com/kubernetes/dashboard/wiki/Accessing-Dashboard---1.7.X-and-above + return fmt.Sprintf("http://%s/api/v1/namespaces/%s/services/http:%s:/proxy/", proxy, ns, svc) +} + +// checkURL checks if a URL returns 200 HTTP OK +func checkURL(url string) error { + resp, err := http.Get(url) + glog.Infof("%s response: %v %+v", url, err, resp) + if err != nil { + return errors.Wrap(err, "checkURL") + } + if resp.StatusCode != http.StatusOK { + return &util.RetriableError{ + Err: fmt.Errorf("unexpected response code: %d", resp.StatusCode), + } + } + return nil +} + func init() { - dashboardCmd.Flags().BoolVar(&dashboardURLMode, "url", false, "Display the kubernetes dashboard in the CLI instead of opening it in the default browser") + dashboardCmd.Flags().BoolVar(&dashboardURLMode, "url", false, "Display dashboard URL instead of opening a browser") RootCmd.AddCommand(dashboardCmd) } diff --git a/deploy/addons/dashboard/dashboard-svc.yaml b/deploy/addons/dashboard/dashboard-svc.yaml index b39a8001cb..04ccc0b932 100644 --- a/deploy/addons/dashboard/dashboard-svc.yaml +++ b/deploy/addons/dashboard/dashboard-svc.yaml @@ -24,10 +24,8 @@ metadata: kubernetes.io/minikube-addons: dashboard kubernetes.io/minikube-addons-endpoint: dashboard spec: - type: NodePort ports: - port: 80 targetPort: 9090 - nodePort: 30000 selector: app: kubernetes-dashboard diff --git a/pkg/minikube/service/service.go b/pkg/minikube/service/service.go index 2fb7c82a2d..6b4cfeeb5f 100644 --- a/pkg/minikube/service/service.go +++ b/pkg/minikube/service/service.go @@ -25,6 +25,8 @@ import ( "time" "github.com/docker/machine/libmachine" + "github.com/golang/glog" + "github.com/pkg/browser" "github.com/pkg/errors" "k8s.io/api/core/v1" @@ -189,45 +191,23 @@ func printURLsForService(c corev1.CoreV1Interface, ip, service, namespace string return urls, nil } -// CheckService waits for the specified service to be ready by returning an error until the service is up -// The check is done by polling the endpoint associated with the service and when the endpoint exists, returning no error->service-online +// CheckService checks if a service is listening on a port. func CheckService(namespace string, service string) error { client, err := K8s.GetCoreClient() if err != nil { return errors.Wrap(err, "Error getting kubernetes client") } - services := client.Services(namespace) - err = validateService(services, service) - if err != nil { - return errors.Wrap(err, "Error validating service") - } - endpoints := client.Endpoints(namespace) - return checkEndpointReady(endpoints, service) -} -func validateService(s corev1.ServiceInterface, service string) error { - if _, err := s.Get(service, metav1.GetOptions{}); err != nil { - return errors.Wrapf(err, "Error getting service %s", service) - } - return nil -} - -func checkEndpointReady(endpoints corev1.EndpointsInterface, service string) error { - endpoint, err := endpoints.Get(service, metav1.GetOptions{}) + svc, err := client.Services(namespace).Get(service, metav1.GetOptions{}) if err != nil { - return &util.RetriableError{Err: errors.Errorf("Error getting endpoints for service %s", service)} - } - const notReadyMsg = "Waiting, endpoint for service is not ready yet...\n" - if len(endpoint.Subsets) == 0 { - fmt.Fprintf(os.Stderr, notReadyMsg) - return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")} - } - for _, subset := range endpoint.Subsets { - if len(subset.Addresses) == 0 { - fmt.Fprintf(os.Stderr, notReadyMsg) - return &util.RetriableError{Err: errors.New("No endpoints for service are ready yet")} + return &util.RetriableError{ + Err: errors.Wrapf(err, "Error getting service %s", service), } } + if len(svc.Spec.Ports) == 0 { + return fmt.Errorf("%s:%s has no ports", namespace, service) + } + glog.Infof("Found service: %+v", svc) return nil } diff --git a/pkg/minikube/service/service_test.go b/pkg/minikube/service/service_test.go index f28bce3f97..7268e49eaa 100644 --- a/pkg/minikube/service/service_test.go +++ b/pkg/minikube/service/service_test.go @@ -133,44 +133,6 @@ func (e MockEndpointsInterface) Get(name string, _ metav1.GetOptions) (*v1.Endpo return endpoint, nil } -func TestCheckEndpointReady(t *testing.T) { - var tests = []struct { - description string - service string - err bool - }{ - { - description: "Endpoint with no subsets should return an error", - service: "no-subsets", - err: true, - }, - { - description: "Endpoint with no ready endpoints should return an error", - service: "not-ready", - err: true, - }, - { - description: "Endpoint with at least one ready endpoint should not return an error", - service: "one-ready", - err: false, - }, - } - - for _, test := range tests { - test := test - t.Run(test.description, func(t *testing.T) { - t.Parallel() - err := checkEndpointReady(&MockEndpointsInterface{}, test.service) - if err != nil && !test.err { - t.Errorf("Check endpoints returned an error: %+v", err) - } - if err == nil && test.err { - t.Errorf("Check endpoints should have returned an error but returned nil") - } - }) - } -} - type MockServiceInterface struct { fake.FakeServices ServiceList *v1.ServiceList diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 40cc5491e3..783ba13324 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -97,14 +97,17 @@ func Retry(attempts int, callback func() error) (err error) { func RetryAfter(attempts int, callback func() error, d time.Duration) (err error) { m := MultiError{} for i := 0; i < attempts; i++ { + glog.V(1).Infof("retry loop %d", i) err = callback() if err == nil { return nil } m.Collect(err) if _, ok := err.(*RetriableError); !ok { + glog.Infof("non-retriable error: %v", err) return m.ToError() } + glog.V(2).Infof("sleeping %s", d) time.Sleep(d) } return m.ToError() diff --git a/test/integration/addons_test.go b/test/integration/addons_test.go index 6c7ac759e8..208bad7c22 100644 --- a/test/integration/addons_test.go +++ b/test/integration/addons_test.go @@ -19,9 +19,10 @@ limitations under the License. package integration import ( - "errors" "fmt" + "io/ioutil" "net" + "net/http" "net/url" "path/filepath" "strings" @@ -49,34 +50,45 @@ func testDashboard(t *testing.T) { t.Parallel() minikubeRunner := NewMinikubeRunner(t) - var u *url.URL - - checkDashboard := func() error { - var err error - dashboardURL := minikubeRunner.RunCommand("dashboard --url", false) - if dashboardURL == "" { - return errors.New("error getting dashboard URL") - } - u, err = url.Parse(strings.TrimSpace(dashboardURL)) + cmd, out := minikubeRunner.RunDaemon("dashboard --url") + defer func() { + err := cmd.Process.Kill() if err != nil { - return err + t.Logf("Failed to kill mount command: %v", err) } - return nil + }() + + s, err := out.ReadString('\n') + if err != nil { + t.Fatalf("failed to read url: %v", err) } - if err := util.Retry(t, checkDashboard, 2*time.Second, 60); err != nil { - t.Fatalf("error checking dashboard URL: %v", err) + u, err := url.Parse(strings.TrimSpace(s)) + if err != nil { + t.Fatalf("failed to parse %q: %v", s, err) } if u.Scheme != "http" { - t.Fatalf("wrong scheme in dashboard URL, expected http, actual %s", u.Scheme) + t.Errorf("got Scheme %s, expected http", u.Scheme) } - _, port, err := net.SplitHostPort(u.Host) + host, _, err := net.SplitHostPort(u.Host) if err != nil { - t.Fatalf("failed to split dashboard host %s: %v", u.Host, err) + t.Fatalf("failed SplitHostPort: %v", err) } - if port != "30000" { - t.Fatalf("Dashboard is exposed on wrong port, expected 30000, actual %s", port) + if host != "127.0.0.1" { + t.Errorf("got host %s, expected 127.0.0.1", host) + } + + resp, err := http.Get(u.String()) + if err != nil { + t.Fatalf("failed get: %v", err) + } + if resp.StatusCode != http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Unable to read http response body: %v", err) + } + t.Errorf("%s returned status code %d, expected %d.\nbody:\n%s", u, resp.StatusCode, http.StatusOK, body) } } diff --git a/test/integration/mount_test.go b/test/integration/mount_test.go index 9975d205ae..7627d84944 100644 --- a/test/integration/mount_test.go +++ b/test/integration/mount_test.go @@ -46,7 +46,7 @@ func testMounting(t *testing.T) { defer os.RemoveAll(tempDir) mountCmd := fmt.Sprintf("mount %s:/mount-9p", tempDir) - cmd := minikubeRunner.RunDaemon(mountCmd) + cmd, _ := minikubeRunner.RunDaemon(mountCmd) defer func() { err := cmd.Process.Kill() if err != nil { diff --git a/test/integration/util/util.go b/test/integration/util/util.go index 434b236232..68e9ed5fe6 100644 --- a/test/integration/util/util.go +++ b/test/integration/util/util.go @@ -17,6 +17,7 @@ limitations under the License. package util import ( + "bufio" "bytes" "encoding/json" "fmt" @@ -81,15 +82,21 @@ func (m *MinikubeRunner) RunCommand(command string, checkError bool) string { return string(stdout) } -func (m *MinikubeRunner) RunDaemon(command string) *exec.Cmd { +func (m *MinikubeRunner) RunDaemon(command string) (*exec.Cmd, *bufio.Reader) { commandArr := strings.Split(command, " ") path, _ := filepath.Abs(m.BinaryPath) cmd := exec.Command(path, commandArr...) - err := cmd.Start() + stdoutPipe, err := cmd.StdoutPipe() if err != nil { - m.T.Fatalf("Error running command: %s %s", command, err) + m.T.Fatalf("stdout pipe failed: %v", err) } - return cmd + + err = cmd.Start() + if err != nil { + m.T.Fatalf("Error running command: %s %v", command, err) + } + return cmd, bufio.NewReader(stdoutPipe) + } func (m *MinikubeRunner) SSH(command string) (string, error) {