Skip to content

Commit

Permalink
#13075 adds --all feature for service to allow forwarding all service…
Browse files Browse the repository at this point in the history
…s in a namespace, also supports multiple service-name arguments to forward any specifically named services.
  • Loading branch information
ckannon committed Jan 17, 2022
1 parent 3f205a0 commit 3629b50
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 33 deletions.
122 changes: 90 additions & 32 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,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 <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(svc.URLs, "\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})
}
exit.Error(reason.SvcTimeout, "Error opening service", err)
}

service.PrintServiceList(os.Stdout, data)
if driver.NeedsPortForward(co.Config.Driver) {
startKicServiceTunnel(svc, cname)
return
startKicServiceTunnels(args, services, cname)
}

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 string) {
func startKicServiceTunnels(args []string, services service.URLs, configName string) {
ctrlC := make(chan os.Signal, 1)
signal.Notify(ctrlC, os.Interrupt)

Expand All @@ -120,33 +169,42 @@ func startKicServiceTunnel(svc, configName string) {
exit.Error(reason.InternalKubernetesClient, "error creating clientset", err)
}

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")
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)
}

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)

openURLs(svc, urls)
if shouldOpen(args) {
openURLs(services[0].Name, services[0].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)
for _, tunnel := range tunnels {
err = tunnel.Stop()
if err != nil {
exit.Error(reason.SvcTunnelStop, "error stopping tunnel", err)
}
}
}

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

0 comments on commit 3629b50

Please sign in to comment.