Skip to content

Commit

Permalink
minikube #13075 adds --all feature for service
Browse files Browse the repository at this point in the history
  • Loading branch information
ckannon committed Feb 8, 2022
1 parent 89795d1 commit cfd661f
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 35 deletions.
128 changes: 95 additions & 33 deletions cmd/minikube/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -50,6 +49,7 @@ const defaultServiceFormatTemplate = "http://{{.IP}}:{{.Port}}"

var (
namespace string
all bool
https bool
serviceURLMode bool
serviceURLFormat string
Expand All @@ -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 {
Expand All @@ -73,45 +73,102 @@ 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 <namespace>'. 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")

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)

Expand All @@ -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(oci.Docker, 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) {
Expand Down
71 changes: 71 additions & 0 deletions cmd/minikube/cmd/service_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
8 changes: 7 additions & 1 deletion pkg/minikube/tunnel/kic/ssh_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package kic

import (
"fmt"
"os"
"os/exec"
"runtime"

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions site/content/en/docs/commands/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion test/integration/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit cfd661f

Please sign in to comment.