diff --git a/cmd/minikube/cmd/service.go b/cmd/minikube/cmd/service.go index cc6e059c5467..ef7337f766af 100644 --- a/cmd/minikube/cmd/service.go +++ b/cmd/minikube/cmd/service.go @@ -30,7 +30,6 @@ import ( "time" "github.com/spf13/cobra" - "k8s.io/klog/v2" "k8s.io/minikube/pkg/drivers/kic/oci" "k8s.io/minikube/pkg/kapi" @@ -50,6 +49,7 @@ const defaultServiceFormatTemplate = "http://{{.IP}}:{{.Port}}" var ( namespace string + all bool https bool serviceURLMode bool serviceURLFormat string @@ -62,7 +62,7 @@ var ( var serviceCmd = &cobra.Command{ Use: "service [flags] SERVICE", Short: "Returns a URL to connect to a service", - Long: `Returns the Kubernetes URL for a service in your local cluster. In the case of multiple URLs they will be printed one at a time.`, + Long: `Returns the Kubernetes URL(s) for service(s) in your local cluster. In the case of multiple URLs they will be printed one at a time.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { t, err := template.New("serviceURL").Parse(serviceURLFormat) if err != nil { @@ -73,37 +73,94 @@ var serviceCmd = &cobra.Command{ RootCmd.PersistentPreRun(cmd, args) }, Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 || len(args) > 1 { - exit.Message(reason.Usage, "You must specify a service name") + if len(args) == 0 && !all || (len(args) > 0 && all) { + exit.Message(reason.Usage, "You must specify service name(s) or --all") } - svc := args[0] + svcArgs := make(map[string]bool) + for _, v := range args { + svcArgs[v] = true + } cname := ClusterFlagValue() co := mustload.Healthy(cname) - - urls, err := service.WaitForService(co.API, co.Config.Name, namespace, svc, serviceURLTemplate, serviceURLMode, https, wait, interval) + var services service.URLs + services, err := service.GetServiceURLs(co.API, co.Config.Name, namespace, serviceURLTemplate) if err != nil { - var s *service.SVCNotFoundError - if errors.As(err, &s) { - exit.Message(reason.SvcNotFound, `Service '{{.service}}' was not found in '{{.namespace}}' namespace. + out.FatalT("Failed to get service URL: {{.error}}", out.V{"error": err}) + out.ErrT(style.Notice, "Check that minikube is running and that you have specified the correct namespace (-n flag) if required.") + os.Exit(reason.ExSvcUnavailable) + } + + if len(args) >= 1 { + var newServices service.URLs + for _, svc := range services { + if _, ok := svcArgs[svc.Name]; ok { + newServices = append(newServices, svc) + } + } + services = newServices + } + + var data [][]string + var openUrls []string + for _, svc := range services { + openUrls, err := service.WaitForService(co.API, co.Config.Name, namespace, svc.Name, serviceURLTemplate, true, https, wait, interval) + + if err != nil { + var s *service.SVCNotFoundError + if errors.As(err, &s) { + exit.Message(reason.SvcNotFound, `Service '{{.service}}' was not found in '{{.namespace}}' namespace. You may select another namespace by using 'minikube service {{.service}} -n '. Or list out all the services using 'minikube service list'`, out.V{"service": svc, "namespace": namespace}) + } + exit.Error(reason.SvcTimeout, "Error opening service", err) + } + + if len(openUrls) == 0 { + data = append(data, []string{svc.Namespace, svc.Name, "No node port"}) + } else { + servicePortNames := strings.Join(svc.PortNames, "\n") + serviceURLs := strings.Join(openUrls, "\n") + + // if we are running Docker on OSX we empty the internal service URLs + if runtime.GOOS == "darwin" && co.Config.Driver == oci.Docker { + serviceURLs = "" + } + + data = append(data, []string{svc.Namespace, svc.Name, servicePortNames, serviceURLs}) + } + } + + if (!serviceURLMode && serviceURLFormat != defaultServiceFormatTemplate && !all) || all { + service.PrintServiceList(os.Stdout, data) + } else if serviceURLMode && !all { + for _, u := range data { + out.String(fmt.Sprintf("%s\n", u[3])) } - exit.Error(reason.SvcTimeout, "Error opening service", err) } if driver.NeedsPortForward(co.Config.Driver) { - startKicServiceTunnel(svc, cname, co.Config.Driver) + startKicServiceTunnel(args, services, cname, co.Config.Driver) return } - openURLs(svc, urls) + if !serviceURLMode && !all && len(args) == 1 { + openURLs(args[0], openUrls) + } }, } +func shouldOpen(args []string) bool { + if !serviceURLMode && !all && len(args) == 1 { + return true + } + return false +} + 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(&all, "all", false, "Forwards all services in a namespace (defaults to \"false\")") serviceCmd.Flags().BoolVar(&https, "https", false, "Open the service URL with https instead of http (defaults to \"false\")") serviceCmd.Flags().IntVar(&wait, "wait", service.DefaultWait, "Amount of time to wait for a service in seconds") serviceCmd.Flags().IntVar(&interval, "interval", service.DefaultInterval, "The initial time interval for each check that wait performs in seconds") @@ -111,7 +168,7 @@ func init() { serviceCmd.PersistentFlags().StringVar(&serviceURLFormat, "format", defaultServiceFormatTemplate, "Format to output service URL in. This format will be applied to each url individually and they will be printed one at a time.") } -func startKicServiceTunnel(svc, configName, driverName string) { +func startKicServiceTunnel(args []string, services service.URLs, configName, driverName string) { ctrlC := make(chan os.Signal, 1) signal.Notify(ctrlC, os.Interrupt) @@ -120,34 +177,39 @@ func startKicServiceTunnel(svc, configName, driverName string) { exit.Error(reason.InternalKubernetesClient, "error creating clientset", err) } - port, err := oci.ForwardedPort(driverName, configName, 22) - if err != nil { - exit.Error(reason.DrvPortForward, "error getting ssh port", err) - } - sshPort := strconv.Itoa(port) - sshKey := filepath.Join(localpath.MiniPath(), "machines", configName, "id_rsa") + var tunnels []*kic.ServiceTunnel + var data [][]string + for _, svc := range services { + port, err := oci.ForwardedPort(driverName, configName, 22) + if err != nil { + exit.Error(reason.DrvPortForward, "error getting ssh port", err) + } + sshPort := strconv.Itoa(port) + sshKey := filepath.Join(localpath.MiniPath(), "machines", configName, "id_rsa") - serviceTunnel := kic.NewServiceTunnel(sshPort, sshKey, clientset.CoreV1()) - urls, err := serviceTunnel.Start(svc, namespace) - if err != nil { - exit.Error(reason.SvcTunnelStart, "error starting tunnel", err) + serviceTunnel := kic.NewServiceTunnel(sshPort, sshKey, clientset.CoreV1()) + tunnels = append(tunnels, serviceTunnel) + urls, err := serviceTunnel.Start(svc.Name, namespace) + if err != nil { + exit.Error(reason.SvcTunnelStart, "error starting tunnel", err) + } + defer serviceTunnel.Stop() + data = append(data, []string{namespace, svc.Name, "", strings.Join(urls, "\n")}) } - // wait for tunnel to come up time.Sleep(1 * time.Second) - data := [][]string{{namespace, svc, "", strings.Join(urls, "\n")}} - service.PrintServiceList(os.Stdout, data) + if !serviceURLMode && serviceURLFormat != defaultServiceFormatTemplate && !all { + service.PrintServiceList(os.Stdout, data) + } + + if shouldOpen(args) { + openURLs(services[0].Name, services[0].URLs) + } - openURLs(svc, urls) out.WarningT("Because you are using a Docker driver on {{.operating_system}}, the terminal needs to be open to run it.", out.V{"operating_system": runtime.GOOS}) <-ctrlC - - err = serviceTunnel.Stop() - if err != nil { - exit.Error(reason.SvcTunnelStop, "error stopping tunnel", err) - } } func openURLs(svc string, urls []string) { diff --git a/cmd/minikube/cmd/service_test.go b/cmd/minikube/cmd/service_test.go new file mode 100644 index 000000000000..e91a4c27d918 --- /dev/null +++ b/cmd/minikube/cmd/service_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestServiceForwardOpen(t *testing.T) { + var tests = []struct { + name string + serviceURLMode bool + all bool + args []string + want bool + }{ + { + name: "multiple_urls", + serviceURLMode: false, + all: false, + args: []string{"test-service-1", "test-service-2"}, + want: false, + }, + { + name: "service_url_mode", + serviceURLMode: true, + all: false, + args: []string{"test-service-1"}, + want: false, + }, + { + name: "all", + serviceURLMode: false, + all: true, + args: []string{"test-service-1", "test-service-2"}, + want: false, + }, + { + name: "single_url", + serviceURLMode: false, + all: false, + args: []string{"test-service-1"}, + want: true, + }, + } + + for _, tc := range tests { + serviceURLMode = tc.serviceURLMode + all = tc.all + t.Run(tc.name, func(t *testing.T) { + got := shouldOpen(tc.args) + if got != tc.want { + t.Errorf("bool(%+v) = %t, want: %t", "shouldOpen", got, tc.want) + } + }) + } +} diff --git a/pkg/minikube/tunnel/kic/ssh_conn.go b/pkg/minikube/tunnel/kic/ssh_conn.go index 2dc1c2131042..60e6dcd4d2d1 100644 --- a/pkg/minikube/tunnel/kic/ssh_conn.go +++ b/pkg/minikube/tunnel/kic/ssh_conn.go @@ -18,6 +18,7 @@ package kic import ( "fmt" + "os" "os/exec" "runtime" @@ -159,7 +160,12 @@ func (c *sshConn) stop() error { if c.activeConn { c.activeConn = false out.Step(style.Stopping, "Stopping tunnel for service {{.service}}.", out.V{"service": c.service}) - return c.cmd.Process.Kill() + err := c.cmd.Process.Kill() + if err == os.ErrProcessDone { + // No need to return an error here + return nil + } + return err } out.Step(style.Stopping, "Stopped tunnel for service {{.service}}.", out.V{"service": c.service}) return nil diff --git a/site/content/en/docs/commands/service.md b/site/content/en/docs/commands/service.md index 8de8c06a0856..9741425b85a6 100644 --- a/site/content/en/docs/commands/service.md +++ b/site/content/en/docs/commands/service.md @@ -20,6 +20,7 @@ minikube service [flags] SERVICE ### Options ``` + --all Prints URL and port-forwards (if needed) all services in a namespace --format string Format to output service URL in. This format will be applied to each url individually and they will be printed one at a time. (default "http://{{.IP}}:{{.Port}}") --https Open the service URL with https instead of http (defaults to "false") --interval int The initial time interval for each check that wait performs in seconds (default 1) diff --git a/test/integration/functional_test.go b/test/integration/functional_test.go index 72ac787e385d..1babda7456b5 100644 --- a/test/integration/functional_test.go +++ b/test/integration/functional_test.go @@ -1473,7 +1473,14 @@ func validateServiceCmd(ctx context.Context, t *testing.T, profile string) { t.Errorf("expected stderr to be empty but got *%q* . args %q", rr.Stderr, rr.Command()) } - endpoint := strings.TrimSpace(rr.Stdout.String()) + splits := strings.Split(rr.Stdout.String(), "|") + var endpoint string + // get the last endpoint in the output to test http to https + for _, v := range splits { + if strings.Contains(v, "http") { + endpoint = strings.TrimSpace(v) + } + } t.Logf("found endpoint: %s", endpoint) u, err := url.Parse(endpoint)