Skip to content

Commit

Permalink
Merge pull request #3210 from tstromberg/dashboard_on_demand
Browse files Browse the repository at this point in the history
Use "kubectl proxy" instead of a NodePort to expose the dashboard.
  • Loading branch information
tstromberg authored Oct 4, 2018
2 parents 36d76c7 + 687b62c commit 583937a
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 116 deletions.
114 changes: 92 additions & 22 deletions cmd/minikube/cmd/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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)
ns := "kube-system"
svc := "kubernetes-dashboard"
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)
glog.Fatalf("kubectl proxy: %v", err)
}
if len(urls) == 0 {
errMsg := "There appears to be no url associated with dashboard, this is not expected, exiting"
glog.Infoln(errMsg)
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)
}
2 changes: 0 additions & 2 deletions deploy/addons/dashboard/dashboard-svc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 10 additions & 30 deletions pkg/minikube/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
38 changes: 0 additions & 38 deletions pkg/minikube/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
50 changes: 31 additions & 19 deletions test/integration/addons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ limitations under the License.
package integration

import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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 host != "127.0.0.1" {
t.Errorf("got host %s, expected 127.0.0.1", host)
}
if port != "30000" {
t.Fatalf("Dashboard is exposed on wrong port, expected 30000, actual %s", port)

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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/integration/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 583937a

Please sign in to comment.