Use "kubectl proxy" instead of a NodePort to expose the dashboard.

This provides an additional level of security, by enforcing host checking, applying port randomization, and requiring explicit user intent to expose the service to the host.
pull/3210/head
Thomas Stromberg 2018-10-02 22:25:45 -07:00
parent 8e99e283c2
commit df54c6a5b4
4 changed files with 73 additions and 24 deletions

View File

@ -17,13 +17,16 @@ limitations under the License.
package cmd
import (
"bufio"
"fmt"
"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/machine"
@ -34,6 +37,8 @@ import (
var (
dashboardURLMode bool
// Matches: 127.0.0.1:8001
hostPortRe = regexp.MustCompile(`127.0.0.1:\d{4,}`)
)
// dashboardCmd represents the dashboard command
@ -42,6 +47,7 @@ var dashboardCmd = &cobra.Command{
Short: "Opens/displays the kubernetes dashboard URL for your local cluster",
Long: `Opens/displays the kubernetes dashboard URL for your local cluster`,
Run: func(cmd *cobra.Command, args []string) {
glog.Infof("Setting up dashboard ...")
api, err := machine.NewAPIClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting client: %s\n", err)
@ -58,26 +64,59 @@ var dashboardCmd = &cobra.Command{
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)
os.Exit(1)
glog.Fatalf("kubectl proxy: %v", err)
}
url := dashboardURL(hostPort, namespace, svc)
if dashboardURLMode {
fmt.Fprintln(os.Stdout, urls[0])
} else {
fmt.Fprintln(os.Stdout, "Opening kubernetes dashboard in default browser...")
browser.OpenURL(urls[0])
fmt.Fprintln(os.Stdout, url)
return
}
fmt.Fprintln(os.Stdout, fmt.Sprintf("Opening %s in your default browser...", url))
browser.OpenURL(url)
p.Wait()
},
}
// kubectlProxy runs "kubectl proxy", returning host:port
func kubectlProxy() (*exec.Cmd, string, error) {
glog.Infof("Searching for kubectl ...")
path, err := exec.LookPath("kubectl")
if err != nil {
return nil, "", errors.Wrap(err, "Unable to find kubectl in PATH")
}
cmd := exec.Command(path, "proxy", "--port=0")
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return nil, "", errors.Wrap(err, "stdout")
}
glog.Infof("Executing: %s %s", cmd.Path, cmd.Args)
if err := cmd.Start(); err != nil {
return nil, "", errors.Wrap(err, "start")
}
glog.Infof("proxy should be running ...")
reader := bufio.NewReader(stdoutPipe)
glog.Infof("Reading stdout pipe ...")
out, err := reader.ReadString('\n')
if err != nil {
return nil, "", errors.Wrap(err, "read")
}
return cmd, parseHostPort(out), nil
}
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)
}
func parseHostPort(out string) string {
// Starting to serve on 127.0.0.1:8001
glog.Infof("Parsing: %s ...", out)
return hostPortRe.FindString(out)
}
func init() {
dashboardCmd.Flags().BoolVar(&dashboardURLMode, "url", false, "Display the kubernetes dashboard in the CLI instead of opening it in the default browser")
RootCmd.AddCommand(dashboardCmd)

View File

@ -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
- port: 80
targetPort: 9090
selector:
app: kubernetes-dashboard
k8s-app: kubernetes-dashboard

View File

@ -25,6 +25,7 @@ import (
"time"
"github.com/docker/machine/libmachine"
"github.com/pkg/browser"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
@ -35,6 +36,7 @@ import (
"text/template"
"github.com/golang/glog"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/labels"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@ -197,27 +199,34 @@ func CheckService(namespace string, service string) error {
return errors.Wrap(err, "Error getting kubernetes client")
}
services := client.Services(namespace)
glog.Infof("services: %+v", services)
err = validateService(services, service)
if err != nil {
return errors.Wrap(err, "Error validating service")
}
// Add logic here to switch between needing external endpoints or not.
endpoints := client.Endpoints(namespace)
return checkEndpointReady(endpoints, service)
glog.Infof("%s:%s endpoints: %+v", namespace, service, endpoints)
return nil
// return checkEndpointReady(endpoints, service)
}
func validateService(s corev1.ServiceInterface, service string) error {
if _, err := s.Get(service, metav1.GetOptions{}); err != nil {
svc, err := s.Get(service, metav1.GetOptions{})
if err != nil {
return errors.Wrapf(err, "Error getting service %s", service)
}
glog.Infof("%s: %+v", service, svc)
return nil
}
func checkEndpointReady(endpoints corev1.EndpointsInterface, service string) error {
endpoint, err := endpoints.Get(service, metav1.GetOptions{})
glog.Infof("%s endpoint: %+v", service, endpoint)
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"
notReadyMsg := fmt.Sprintf("Waiting, endpoint for %s is not ready yet...\n", service)
if len(endpoint.Subsets) == 0 {
fmt.Fprintf(os.Stderr, notReadyMsg)
return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")}

View File

@ -71,12 +71,15 @@ func testDashboard(t *testing.T) {
if u.Scheme != "http" {
t.Fatalf("wrong scheme in dashboard URL, expected http, actual %s", u.Scheme)
}
_, port, err := net.SplitHostPort(u.Host)
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
t.Fatalf("failed to split dashboard host %s: %v", u.Host, err)
}
if port != "30000" {
t.Fatalf("Dashboard is exposed on wrong port, expected 30000, actual %s", port)
t.Errorf("Dashboard is exposed on wrong port, expected 30000, actual %s", port)
}
if host != "127.0.0.1" {
t.Errorf("host is %s, expected 127.0.0.1", host)
}
}