diff --git a/cmd/minikube/cmd/dashboard.go b/cmd/minikube/cmd/dashboard.go index 1a1dc62cb4..dd3b665d5a 100644 --- a/cmd/minikube/cmd/dashboard.go +++ b/cmd/minikube/cmd/dashboard.go @@ -24,7 +24,7 @@ import ( "github.com/docker/machine/libmachine" "github.com/pkg/browser" "github.com/spf13/cobra" - cmdUtil "k8s.io/minikube/cmd/util" + cmdutil "k8s.io/minikube/cmd/util" "k8s.io/minikube/pkg/minikube/cluster" "k8s.io/minikube/pkg/minikube/constants" @@ -49,15 +49,15 @@ var dashboardCmd = &cobra.Command{ service := "kubernetes-dashboard" if err := commonutil.RetryAfter(20, func() error { return CheckService(namespace, service) }, 6*time.Second); err != nil { - fmt.Fprintln(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s", service, err) - cmdUtil.MaybeReportErrorAndExit(err) + fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", service, err) + cmdutil.MaybeReportErrorAndExit(err) } - url, err := cluster.GetServiceURL(api, namespace, service) + url, err := cluster.GetServiceURL(api, namespace, service, nil) if err != nil { fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, "Check that minikube is running.") - cmdUtil.MaybeReportErrorAndExit(err) + cmdutil.MaybeReportErrorAndExit(err) } if dashboardURLMode { fmt.Fprintln(os.Stdout, url) diff --git a/cmd/minikube/cmd/service.go b/cmd/minikube/cmd/service.go index 1a6cd3138a..fee51206df 100644 --- a/cmd/minikube/cmd/service.go +++ b/cmd/minikube/cmd/service.go @@ -20,23 +20,26 @@ import ( "fmt" "os" "strings" + "text/template" "time" "github.com/docker/machine/libmachine" "github.com/pkg/browser" "github.com/pkg/errors" "github.com/spf13/cobra" - kubeApi "k8s.io/kubernetes/pkg/api" - cmdUtil "k8s.io/minikube/cmd/util" + kubeapi "k8s.io/kubernetes/pkg/api" + cmdutil "k8s.io/minikube/cmd/util" "k8s.io/minikube/pkg/minikube/cluster" "k8s.io/minikube/pkg/minikube/constants" "k8s.io/minikube/pkg/util" ) var ( - namespace string - https bool - serviceURLMode bool + namespace string + https bool + serviceURLMode bool + serviceURLFormat string + serviceURLTemplate *template.Template ) // serviceCmd represents the service command @@ -44,11 +47,19 @@ var serviceCmd = &cobra.Command{ Use: "service [flags] SERVICE", Short: "Gets the kubernetes URL for the specified service in your local cluster", Long: `Gets the kubernetes URL for the specified service in your local cluster`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + t, err := template.New("serviceURL").Parse(serviceURLFormat) + if err != nil { + fmt.Fprintln(os.Stderr, "The value passed to --format is invalid:\n\n", err) + os.Exit(1) + } + serviceURLTemplate = t + }, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 || len(args) > 1 { errText := "Please specify a service name." fmt.Fprintln(os.Stderr, errText) - cmdUtil.MaybeReportErrorAndExit(errors.New(errText)) + cmdutil.MaybeReportErrorAndExit(errors.New(errText)) } service := args[0] @@ -57,15 +68,15 @@ var serviceCmd = &cobra.Command{ cluster.EnsureMinikubeRunningOrExit(api, 1) if err := util.RetryAfter(20, func() error { return CheckService(namespace, service) }, 6*time.Second); err != nil { - fmt.Fprintln(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s", service, err) - cmdUtil.MaybeReportErrorAndExit(err) + fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", service, err) + cmdutil.MaybeReportErrorAndExit(err) } - url, err := cluster.GetServiceURL(api, namespace, service) + url, err := cluster.GetServiceURL(api, namespace, service, serviceURLTemplate) if err != nil { fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, "Check that minikube is running and that you have specified the correct namespace (-n flag).") - cmdUtil.MaybeReportErrorAndExit(err) + cmdutil.MaybeReportErrorAndExit(err) } if https { url = strings.Replace(url, "http", "https", 1) @@ -83,6 +94,9 @@ func init() { serviceCmd.Flags().StringVarP(&namespace, "namespace", "n", "default", "The service namespace") serviceCmd.Flags().BoolVar(&serviceURLMode, "url", false, "Display the kubernetes service URL in the CLI instead of opening it in the default browser") serviceCmd.Flags().BoolVar(&https, "https", false, "Open the service URL with https instead of http") + + serviceCmd.PersistentFlags().StringVar(&serviceURLFormat, "format", "http://{{.IP}}:{{.Port}}", "Format to output service URL in") + RootCmd.AddCommand(serviceCmd) } @@ -102,7 +116,7 @@ func CheckService(namespace string, service string) error { const notReadyMsg = "Waiting, endpoint for service is not ready yet...\n" -func CheckEndpointReady(endpoint *kubeApi.Endpoints) error { +func CheckEndpointReady(endpoint *kubeapi.Endpoints) error { if len(endpoint.Subsets) == 0 { fmt.Fprintf(os.Stderr, notReadyMsg) return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")} diff --git a/docs/minikube_service.md b/docs/minikube_service.md index f41e1baa69..10851a14c4 100644 --- a/docs/minikube_service.md +++ b/docs/minikube_service.md @@ -14,6 +14,7 @@ minikube service [flags] SERVICE ### Options ``` + --format string Format to output service URL in (default "http://{{.IP}}:{{.Port}}") --https Open the service URL with https instead of http -n, --namespace string The service namespace (default "default") --url Display the kubernetes service URL in the CLI instead of opening it in the default browser diff --git a/pkg/minikube/cluster/cluster.go b/pkg/minikube/cluster/cluster.go index d95ce5399e..fbcd67e005 100644 --- a/pkg/minikube/cluster/cluster.go +++ b/pkg/minikube/cluster/cluster.go @@ -30,7 +30,9 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" + "text/template" "time" "github.com/docker/machine/drivers/virtualbox" @@ -41,7 +43,7 @@ import ( "github.com/docker/machine/libmachine/state" "github.com/golang/glog" "github.com/pkg/errors" - kubeApi "k8s.io/kubernetes/pkg/api" + kubeapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" @@ -545,7 +547,12 @@ func CreateSSHShell(api libmachine.API, args []string) error { return client.Shell(strings.Join(args, " ")) } -func GetServiceURL(api libmachine.API, namespace, service string) (string, error) { +type ipPort struct { + IP string + Port int +} + +func GetServiceURL(api libmachine.API, namespace, service string, t *template.Template) (string, error) { host, err := CheckIfApiExistsAndLoad(api) if err != nil { return "", errors.Wrap(err, "Error checking if api exist and loading it") @@ -556,52 +563,89 @@ func GetServiceURL(api libmachine.API, namespace, service string) (string, error return "", errors.Wrap(err, "Error getting ip from host") } - port, err := getServicePort(namespace, service) + client, err := getKubernetesClient() if err != nil { - return "", errors.Wrapf(err, "Error getting service port from %s, %s", namespace, service) + return "", err } - return fmt.Sprintf("http://%s:%d", ip, port), nil + return getServiceURLWithClient(client, ip, namespace, service, t) +} + +func getServiceURLWithClient(client *unversioned.Client, ip, namespace, service string, t *template.Template) (string, error) { + port, err := getServicePort(client, namespace, service) + if err != nil { + return "", err + } + + if t == nil { + return fmt.Sprintf("http://%s", net.JoinHostPort(ip, strconv.Itoa(port))), nil + } + + var doc bytes.Buffer + err = t.Execute(&doc, ipPort{ip, port}) + if err != nil { + return "", err + } + + u, err := url.Parse(doc.String()) + if err != nil { + return "", err + } + + return u.String(), nil } type serviceGetter interface { - Get(name string) (*kubeApi.Service, error) + Get(name string) (*kubeapi.Service, error) } type endpointGetter interface { - Get(name string) (*kubeApi.Endpoints, error) + Get(name string) (*kubeapi.Endpoints, error) } -func getServicePort(namespace, service string) (int, error) { - services, err := GetKubernetesServicesWithNamespace(namespace) - if err != nil { - return 0, errors.Wrapf(err, "Error getting kubernetes service with namespace", namespace) - } +func getServicePort(client *unversioned.Client, namespace, service string) (int, error) { + services := getKubernetesServicesWithNamespace(client, namespace) return getServicePortFromServiceGetter(services, service) } -func getServicePortFromServiceGetter(services serviceGetter, service string) (int, error) { +type MissingNodePortError struct { + service *kubeapi.Service +} + +func (e MissingNodePortError) Error() string { + return fmt.Sprintf("Service %s/%s does not have a node port. To have one assigned automatically, the service type must be NodePort or LoadBalancer, but this service is of type %s.", e.service.Namespace, e.service.Name, e.service.Spec.Type) +} + +func getServiceFromServiceGetter(services serviceGetter, service string) (*kubeapi.Service, error) { svc, err := services.Get(service) if err != nil { - return 0, errors.Wrapf(err, "Error getting %s service: %s", service) + return nil, fmt.Errorf("Error getting %s service: %s", service, err) + } + return svc, nil +} + +func getServicePortFromServiceGetter(services serviceGetter, service string) (int, error) { + svc, err := getServiceFromServiceGetter(services, service) + if err != nil { + return 0, err } nodePort := 0 if len(svc.Spec.Ports) > 0 { nodePort = int(svc.Spec.Ports[0].NodePort) } if nodePort == 0 { - return 0, errors.Errorf("Service %s does not have a node port. To have one assigned automatically, the service type must be NodePort or LoadBalancer, but this service is of type %s.", service, svc.Spec.Type) + return 0, MissingNodePortError{svc} } return nodePort, nil } -func GetKubernetesClient() (*unversioned.Client, error) { +func getKubernetesClient() (*unversioned.Client, error) { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - configOverrides := &clientcmd.ConfigOverrides{CurrentContext: constants.MinikubeContext} + configOverrides := &clientcmd.ConfigOverrides{} kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) config, err := kubeConfig.ClientConfig() if err != nil { - return nil, errors.Wrap(err, "Error creating kubeConfig: %s") + return nil, fmt.Errorf("Error creating kubeConfig: %s", err) } client, err := unversioned.New(config) if err != nil { @@ -610,17 +654,12 @@ func GetKubernetesClient() (*unversioned.Client, error) { return client, nil } -func GetKubernetesServicesWithNamespace(namespace string) (serviceGetter, error) { - client, err := GetKubernetesClient() - if err != nil { - return nil, errors.Wrap(err, "Error getting kubernetes client") - } - services := client.Services(namespace) - return services, nil +func getKubernetesServicesWithNamespace(client *unversioned.Client, namespace string) serviceGetter { + return client.Services(namespace) } func GetKubernetesEndpointsWithNamespace(namespace string) (endpointGetter, error) { - client, err := GetKubernetesClient() + client, err := getKubernetesClient() if err != nil { return nil, errors.Wrap(err, "Error getting kubernetes client") }