diff --git a/build/Dockerfile b/build/Dockerfile index d7327142..dc7955e4 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,6 +1,6 @@ # To run (from repo root): docker build -t fission -f ./build/Dockerfile . ARG NOBUILD -ARG GOLANG_VERSION=1.9.4 +ARG GOLANG_VERSION=1.10.0 FROM golang:$GOLANG_VERSION AS builder WORKDIR /go/src/github.com/fission/fission-workflows diff --git a/cmd/wfcli/main.go b/cmd/wfcli/main.go index 972ca703..2764dc47 100644 --- a/cmd/wfcli/main.go +++ b/cmd/wfcli/main.go @@ -14,6 +14,18 @@ import ( // This is a prototype of the CLI (and will be integrated into the Fission CLI eventually). func main() { + // fetch the FISSION_URL env variable. If not set, port-forward to controller. + var value string + fissionUrl := os.Getenv("FISSION_URL") + if len(fissionUrl) == 0 { + fissionNamespace := getFissionNamespace() + kubeConfig := getKubeConfigPath() + localPort := setupPortForward(kubeConfig, fissionNamespace, "application=fission-api") + value = "http://127.0.0.1:" + localPort + } else { + value = fissionUrl + } + app := cli.NewApp() app.Author = "Erwin van Eyk" app.Email = "erwin@platform9.com" @@ -24,7 +36,7 @@ func main() { app.Flags = []cli.Flag{ cli.StringFlag{ Name: "url, u", - Value: "http://localhost:31313", + Value: value, EnvVar: "FISSION_URL", Usage: "Url to the Fission apiserver", }, diff --git a/cmd/wfcli/portforward.go b/cmd/wfcli/portforward.go new file mode 100644 index 00000000..d556f3dc --- /dev/null +++ b/cmd/wfcli/portforward.go @@ -0,0 +1,186 @@ +package main + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/tools/remotecommand" +) + +func getFissionNamespace() string { + fissionNamespace := os.Getenv("FISSION_NAMESPACE") + return fissionNamespace +} + +func getKubeConfigPath() string { + kubeConfig := os.Getenv("KUBECONFIG") + if len(kubeConfig) == 0 { + home := os.Getenv("HOME") + kubeConfig = filepath.Join(home, ".kube", "config") + + if _, err := os.Stat(kubeConfig); os.IsNotExist(err) { + panic("Couldn't find kubeconfig file. Set the KUBECONFIG environment variable to your kubeconfig's path.") + } + } + return kubeConfig +} + +func findFreePort() (string, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return "", err + } + + port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) + file, err := listener.(*net.TCPListener).File() + if err != nil { + return "", nil + } + + err = listener.Close() + if err != nil { + return "", err + } + + err = file.Close() + if err != nil { + return "", err + } + + return port, nil +} + +// runPortForward creates a local port forward to the specified pod +func runPortForward(kubeConfig string, labelSelector string, localPort string, fissionNamespace string) error { + config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + if err != nil { + panic(fmt.Sprintf("Failed to connect to Kubernetes: %s", err)) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + panic(fmt.Sprintf("Failed to connect to Kubernetes: %s", err)) + } + + // if fission namespace is unset, try to find a fission pod in any namespace + if len(fissionNamespace) == 0 { + fissionNamespace = meta_v1.NamespaceAll + } + + // get the pod; if there is more than one, ask the user to disambiguate + podList, err := clientset.CoreV1().Pods(fissionNamespace). + List(meta_v1.ListOptions{LabelSelector: labelSelector}) + if err != nil || len(podList.Items) == 0 { + panic("Error getting controller pod for port-forwarding") + } + + // make a useful error message if there is more than one install + if len(podList.Items) > 1 { + namespaces := make([]string, 0) + for _, p := range podList.Items { + namespaces = append(namespaces, p.Namespace) + } + panic(fmt.Sprintf("Found %v fission installs, set FISSION_NAMESPACE to one of: %v", + len(podList.Items), strings.Join(namespaces, " "))) + } + + // pick the first pod + podName := podList.Items[0].Name + podNameSpace := podList.Items[0].Namespace + + // get the service and the target port + svcs, err := clientset.CoreV1().Services(podNameSpace). + List(meta_v1.ListOptions{LabelSelector: labelSelector}) + if err != nil { + panic(fmt.Sprintf("Error getting %v service :%v", labelSelector, err.Error())) + } + if len(svcs.Items) == 0 { + panic(fmt.Sprintf("Service %v not found", labelSelector)) + } + service := &svcs.Items[0] + + var targetPort string + for _, servicePort := range service.Spec.Ports { + targetPort = servicePort.TargetPort.String() + } + + stopChannel := make(chan struct{}, 1) + readyChannel := make(chan struct{}) + + // create request URL + req := clientset.CoreV1Client.RESTClient().Post().Resource("pods"). + Namespace(podNameSpace).Name(podName).SubResource("portforward") + url := req.URL() + + // create ports slice + portCombo := localPort + ":" + targetPort + ports := []string{portCombo} + + // actually start the port-forwarding process here + dialer, err := remotecommand.NewExecutor(config, "POST", url) + if err != nil { + msg := fmt.Sprintf("newexecutor errored out :%v", err.Error()) + panic(msg) + } + + fw, err := portforward.New(dialer, ports, stopChannel, readyChannel, nil, os.Stderr) + if err != nil { + msg := fmt.Sprintf("portforward.new errored out :%v", err.Error()) + panic(msg) + } + + return fw.ForwardPorts() +} + +// Port forward a free local port to a pod on the cluster. The pod is +// found in the specified namespace by labelSelector. The pod's port +// is found by looking for a service in the same namespace and using +// its targetPort. Once the port forward is started, wait for it to +// start accepting connections before returning. +func setupPortForward(kubeConfig, namespace, labelSelector string) string { + localPort, err := findFreePort() + if err != nil { + panic(fmt.Sprintf("Error finding unused port :%v", err.Error())) + } + + for { + conn, _ := net.DialTimeout("tcp", + net.JoinHostPort("", localPort), time.Millisecond) + if conn != nil { + conn.Close() + } else { + break + } + time.Sleep(time.Millisecond * 50) + } + + go func() { + err := runPortForward(kubeConfig, labelSelector, localPort, namespace) + if err != nil { + panic(fmt.Sprintf("Error forwarding to controller port: %s", err.Error())) + } + }() + + for { + conn, _ := net.DialTimeout("tcp", + net.JoinHostPort("", localPort), time.Millisecond) + if conn != nil { + conn.Close() + break + } + time.Sleep(time.Millisecond * 50) + } + + return localPort +} diff --git a/glide.lock b/glide.lock index a75ef377..ff7b1974 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: b9ec998ab73dd238a9e49987eee099c27bf8975385fa1ee2379c9fa98e5a0cf4 -updated: 2018-03-16T16:24:48.132614+01:00 +hash: 19ea3b0c11225444d627f1bdce20680adf473e3764bf4613f08ded1978fc4a71 +updated: 2018-03-21T18:46:59.684912+01:00 imports: - name: cloud.google.com/go version: 3b1ae45394a234c385be014e9a488f2bb6eef821 @@ -26,6 +26,10 @@ imports: subpackages: - digest - reference +- name: github.com/docker/spdystream + version: 449fdfce4d962303d702fec724ef0ad181c92528 + subpackages: + - spdy - name: github.com/emicklei/go-restful version: ff4f55a206334ef123e4f79bbf348980da81ca46 subpackages: @@ -288,7 +292,7 @@ imports: - name: gopkg.in/yaml.v2 version: 7f97868eec74b32b0982dd158a51a446d1da7eb5 - name: k8s.io/apiextensions-apiserver - version: fe88520b48a5daf72bc874786850afc7ac1f402d + version: c17db11fb5a24436cebfbfccaf2f3b4b69be77ec subpackages: - pkg/apis/apiextensions - pkg/apis/apiextensions/v1beta1 @@ -329,10 +333,13 @@ imports: - pkg/util/diff - pkg/util/errors - pkg/util/framer + - pkg/util/httpstream + - pkg/util/httpstream/spdy - pkg/util/intstr - pkg/util/json - pkg/util/net - pkg/util/rand + - pkg/util/remotecommand - pkg/util/runtime - pkg/util/sets - pkg/util/validation @@ -341,6 +348,7 @@ imports: - pkg/util/yaml - pkg/version - pkg/watch + - third_party/forked/golang/netutil - third_party/forked/golang/reflect - name: k8s.io/client-go version: d92e8497f71b7b4e0494e5bd204b48d34bd6f254 @@ -422,8 +430,11 @@ imports: - tools/clientcmd/api/latest - tools/clientcmd/api/v1 - tools/metrics + - tools/portforward + - tools/remotecommand - transport - util/cert + - util/exec - util/flowcontrol - util/homedir - util/integer diff --git a/hack/codegen-swagger-client.sh b/hack/codegen-swagger-client.sh deleted file mode 100644 index 84ee2ec3..00000000 --- a/hack/codegen-swagger-client.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -e - -mkdir -p cmd/wfcli/swagger-client/ -swagger generate client -f api/swagger/apiserver.swagger.json -t cmd/wfcli/swagger-client/ \ No newline at end of file diff --git a/test/e2e/utils.sh b/test/e2e/utils.sh index 13a0cb97..c6982040 100644 --- a/test/e2e/utils.sh +++ b/test/e2e/utils.sh @@ -247,7 +247,7 @@ dump_system_info() { echo "--- fission ---" fission -v echo "--- wfcli ---" - wfcli -v + wfcli version echo "--- End System Info ---" }