Merge pull request #3210 from tstromberg/dashboard_on_demand
Use "kubectl proxy" instead of a NodePort to expose the dashboard.pull/3215/head
commit
583937ac3e
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue