Merge pull request #13367 from ckannon/master
Adds 'minikube service --all' feature to allow forwarding all services in a namespacepull/13591/head
commit
d6120f131e
|
|
@ -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.
|
||||
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})
|
||||
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")
|
||||
|
|
@ -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,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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue