Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds 'minikube service --all' feature to allow forwarding all services in a namespace #13367

Merged
merged 1 commit into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 93 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,37 @@ 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 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())
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)
}
})
}
}
6 changes: 2 additions & 4 deletions pkg/minikube/tunnel/kic/service_tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ func (t *ServiceTunnel) Start(svcName, namespace string) ([]string, error) {
}

// Stop ...
func (t *ServiceTunnel) Stop() error {
func (t *ServiceTunnel) Stop() {
err := t.sshConn.stop()
if err != nil {
return errors.Wrap(err, "stopping ssh tunnel")
klog.Warningf("Failed to stop ssh tunnel", err)
}

return nil
}
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