diff --git a/cmd/minikube/cmd/service.go b/cmd/minikube/cmd/service.go index 65e1f69297f8..de0205fbcc2c 100644 --- a/cmd/minikube/cmd/service.go +++ b/cmd/minikube/cmd/service.go @@ -20,8 +20,13 @@ import ( "fmt" "net/url" "os" + "os/signal" + "path/filepath" "runtime" + "strconv" + "strings" "text/template" + "time" "github.com/golang/glog" "github.com/pkg/browser" @@ -32,9 +37,11 @@ import ( "k8s.io/minikube/pkg/minikube/config" pkg_config "k8s.io/minikube/pkg/minikube/config" "k8s.io/minikube/pkg/minikube/exit" + "k8s.io/minikube/pkg/minikube/localpath" "k8s.io/minikube/pkg/minikube/machine" "k8s.io/minikube/pkg/minikube/out" "k8s.io/minikube/pkg/minikube/service" + "k8s.io/minikube/pkg/minikube/tunnel/kic" ) const defaultServiceFormatTemplate = "http://{{.IP}}:{{.Port}}" @@ -85,34 +92,17 @@ var serviceCmd = &cobra.Command{ exit.WithError("Error getting config", err) } + if runtime.GOOS == "darwin" && cfg.Driver == oci.Docker { + startKicServiceTunnel(svc, cfg.Name) + return + } + urls, err := service.WaitForService(api, namespace, svc, serviceURLTemplate, serviceURLMode, https, wait, interval) if err != nil { exit.WithError("Error opening service", err) } - if runtime.GOOS == "darwin" && cfg.Driver == oci.Docker { - out.FailureT("Opening service in browser is not implemented yet for docker driver on Mac.\nThe following issue is tracking the in progress work:\nhttps://github.com/kubernetes/minikube/issues/6778") - exit.WithCodeT(exit.Unavailable, "Not yet implemented for docker driver on MacOS.") - } - - for _, u := range urls { - _, err := url.Parse(u) - if err != nil { - glog.Warningf("failed to parse url %q: %v (will not open)", u, err) - out.String(fmt.Sprintf("%s\n", u)) - continue - } - - if serviceURLMode { - out.String(fmt.Sprintf("%s\n", u)) - continue - } - - out.T(out.Celebrate, "Opening service {{.namespace_name}}/{{.service_name}} in default browser...", out.V{"namespace_name": namespace, "service_name": svc}) - if err := browser.OpenURL(u); err != nil { - exit.WithError(fmt.Sprintf("open url failed: %s", u), err) - } - } + openURLs(svc, urls) }, } @@ -126,3 +116,65 @@ 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 string) { + ctrlC := make(chan os.Signal, 1) + signal.Notify(ctrlC, os.Interrupt) + + clientset, err := service.K8s.GetClientset(1 * time.Second) + if err != nil { + exit.WithError("error creating clientset", err) + } + + port, err := oci.HostPortBinding(oci.Docker, configName, 22) + if err != nil { + exit.WithError("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.WithError("error starting tunnel", err) + } + + // 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) + + <-ctrlC + + err = serviceTunnel.Stop() + if err != nil { + exit.WithError("error stopping tunnel", err) + } +} + +func openURLs(svc string, urls []string) { + for _, u := range urls { + _, err := url.Parse(u) + if err != nil { + glog.Warningf("failed to parse url %q: %v (will not open)", u, err) + out.String(fmt.Sprintf("%s\n", u)) + continue + } + + if serviceURLMode { + out.String(fmt.Sprintf("%s\n", u)) + continue + } + + out.T(out.Celebrate, "Opening service {{.namespace_name}}/{{.service_name}} in default browser...", out.V{"namespace_name": namespace, "service_name": svc}) + if err := browser.OpenURL(u); err != nil { + exit.WithError(fmt.Sprintf("open url failed: %s", u), err) + } + + out.T(out.WarningType, "Because you are using docker driver on Mac, the terminal needs to be open to run it.") + } + +} diff --git a/pkg/minikube/tunnel/kic/service_tunnel.go b/pkg/minikube/tunnel/kic/service_tunnel.go new file mode 100644 index 000000000000..e1be04d97d04 --- /dev/null +++ b/pkg/minikube/tunnel/kic/service_tunnel.go @@ -0,0 +1,81 @@ +/* +Copyright 2020 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 kic + +import ( + "fmt" + + "github.com/golang/glog" + "github.com/pkg/errors" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + typed_core "k8s.io/client-go/kubernetes/typed/core/v1" +) + +// ServiceTunnel ... +type ServiceTunnel struct { + sshPort string + sshKey string + v1Core typed_core.CoreV1Interface + sshConn *sshConn +} + +// NewServiceTunnel ... +func NewServiceTunnel(sshPort, sshKey string, v1Core typed_core.CoreV1Interface) *ServiceTunnel { + return &ServiceTunnel{ + sshPort: sshPort, + sshKey: sshKey, + v1Core: v1Core, + } +} + +// Start ... +func (t *ServiceTunnel) Start(svcName, namespace string) ([]string, error) { + svc, err := t.v1Core.Services(namespace).Get(svcName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "getting service") + } + + t.sshConn, err = createSSHConnWithRandomPorts(svcName, t.sshPort, t.sshKey, svc) + if err != nil { + return nil, errors.Wrap(err, "creating ssh conn") + } + + go func() { + err = t.sshConn.startAndWait() + if err != nil { + glog.Errorf("error starting ssh tunnel: %v", err) + } + }() + + urls := make([]string, 0, len(svc.Spec.Ports)) + for _, port := range t.sshConn.ports { + urls = append(urls, fmt.Sprintf("http://127.0.0.1:%d", port)) + } + + return urls, nil +} + +// Stop ... +func (t *ServiceTunnel) Stop() error { + err := t.sshConn.stop() + if err != nil { + return errors.Wrap(err, "stopping ssh tunnel") + } + + return nil +} diff --git a/pkg/minikube/tunnel/kic/ssh_conn.go b/pkg/minikube/tunnel/kic/ssh_conn.go index 53c7304a3c40..d619a44bc508 100644 --- a/pkg/minikube/tunnel/kic/ssh_conn.go +++ b/pkg/minikube/tunnel/kic/ssh_conn.go @@ -20,6 +20,8 @@ import ( "fmt" "os/exec" + "github.com/phayes/freeport" + v1 "k8s.io/api/core/v1" ) @@ -27,9 +29,10 @@ type sshConn struct { name string service string cmd *exec.Cmd + ports []int } -func createSSHConn(name, sshPort, sshKey string, svc v1.Service) *sshConn { +func createSSHConn(name, sshPort, sshKey string, svc *v1.Service) *sshConn { // extract sshArgs sshArgs := []string{ // TODO: document the options here @@ -61,6 +64,47 @@ func createSSHConn(name, sshPort, sshKey string, svc v1.Service) *sshConn { } } +func createSSHConnWithRandomPorts(name, sshPort, sshKey string, svc *v1.Service) (*sshConn, error) { + // extract sshArgs + sshArgs := []string{ + // TODO: document the options here + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking no", + "-N", + "docker@127.0.0.1", + "-p", sshPort, + "-i", sshKey, + } + + usedPorts := make([]int, 0, len(svc.Spec.Ports)) + + for _, port := range svc.Spec.Ports { + freeport, err := freeport.GetFreePort() + if err != nil { + return nil, err + } + + arg := fmt.Sprintf( + "-L %d:%s:%d", + freeport, + svc.Spec.ClusterIP, + port.Port, + ) + + sshArgs = append(sshArgs, arg) + usedPorts = append(usedPorts, freeport) + } + + cmd := exec.Command("ssh", sshArgs...) + + return &sshConn{ + name: name, + service: svc.Name, + cmd: cmd, + ports: usedPorts, + }, nil +} + func (c *sshConn) startAndWait() error { fmt.Printf("starting tunnel for %s\n", c.service) err := c.cmd.Start() diff --git a/pkg/minikube/tunnel/kic/ssh_tunnel.go b/pkg/minikube/tunnel/kic/ssh_tunnel.go index 3dca0a1d3460..bc5fd53a4fbc 100644 --- a/pkg/minikube/tunnel/kic/ssh_tunnel.go +++ b/pkg/minikube/tunnel/kic/ssh_tunnel.go @@ -104,7 +104,7 @@ func (t *SSHTunnel) startConnection(svc v1.Service) { } // create new ssh conn - newSSHConn := createSSHConn(uniqName, t.sshPort, t.sshKey, svc) + newSSHConn := createSSHConn(uniqName, t.sshPort, t.sshKey, &svc) t.conns[newSSHConn.name] = newSSHConn go func() {