Repository: fluxcd/flux
Pull request #2952 (2020-03-31 13:32:29)

Add portforward package
Based on https://github.com/justinbarrick/go-k8s-portforward

Fix: #2951 




Patch:
diff --git a/cmd/fluxctl/portforward.go b/cmd/fluxctl/portforward.go
index a1d47530..3e8cef2c 100644
--- a/cmd/fluxctl/portforward.go
+++ b/cmd/fluxctl/portforward.go
@@ -4,7 +4,8 @@ import (
 	"fmt"
 	"strings"
 
-	portforward "github.com/justinbarrick/go-k8s-portforward"
+	"github.com/fluxcd/flux/pkg/portforward"
+
 	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
diff --git a/go.mod b/go.mod
index 82420d33..ce0a2d2c 100644
--- a/go.mod
+++ b/go.mod
@@ -29,38 +29,25 @@ replace (
 replace github.com/fluxcd/flux/pkg/install => ./pkg/install
 
 require (
-	github.com/Azure/go-autorest v11.7.1+incompatible // indirect
 	github.com/Jeffail/gabs v1.4.0
-	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/semver/v3 v3.0.3
-	github.com/Masterminds/sprig v2.22.0+incompatible // indirect
-	github.com/VividCortex/gohistogram v1.0.0 // indirect
 	github.com/aws/aws-sdk-go v1.27.1
 	github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
 	github.com/cheggaaa/pb/v3 v3.0.2
-	github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
 	github.com/docker/distribution v2.7.1+incompatible
-	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
-	github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/evanphx/json-patch v4.5.0+incompatible
 	github.com/fluxcd/flux/pkg/install v0.0.0-00010101000000-000000000000
 	github.com/fluxcd/helm-operator v1.0.0-rc6
-	github.com/fluxcd/helm-operator/pkg/install v0.0.0-00010101000000-000000000000 // indirect
 	github.com/ghodss/yaml v1.0.0
 	github.com/go-kit/kit v0.9.0
-	github.com/gogo/googleapis v1.3.1 // indirect
-	github.com/gogo/status v1.1.0 // indirect
 	github.com/golang/gddo v0.0.0-20190312205958-5a2505f3dbf0
 	github.com/google/go-containerregistry v0.0.0-20200121192426-b0ae1fc74a66
 	github.com/google/go-github/v28 v28.1.1
 	github.com/gorilla/mux v1.7.3
 	github.com/gorilla/websocket v1.4.0
 	github.com/imdario/mergo v0.3.8
-	github.com/justinbarrick/go-k8s-portforward v1.0.4-0.20190722134107-d79fe1b9d79d
-	github.com/ncabatoff/go-seq v0.0.0-20180805175032-b08ef85ed833 // indirect
 	github.com/opencontainers/go-digest v1.0.0-rc1
 	github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9 // indirect
-	github.com/opentracing/opentracing-go v1.1.0 // indirect
 	github.com/pkg/errors v0.8.1
 	github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942
 	github.com/prometheus/client_golang v1.2.1
@@ -69,11 +56,8 @@ require (
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.4.0
-	github.com/uber/jaeger-client-go v2.21.1+incompatible // indirect
-	github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
 	github.com/weaveworks/common v0.0.0-20190410110702-87611edc252e
 	github.com/weaveworks/go-checkpoint v0.0.0-20170503165305-ebbb8b0518ab
-	github.com/weaveworks/promrus v1.2.0 // indirect
 	github.com/whilp/git-urls v0.0.0-20160530060445-31bac0d230fa
 	github.com/xeipuuv/gojsonschema v1.1.0
 	go.mozilla.org/sops/v3 v3.5.0
@@ -81,11 +65,9 @@ require (
 	golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
 	gopkg.in/yaml.v2 v2.2.8
-	helm.sh/helm/v3 v3.0.3 // indirect
 	k8s.io/api v0.17.4
 	k8s.io/apiextensions-apiserver v0.17.4
 	k8s.io/apimachinery v0.17.4
 	k8s.io/client-go v11.0.0+incompatible
-	k8s.io/helm v2.16.1+incompatible // indirect
 	k8s.io/klog v1.0.0
 )
diff --git a/go.sum b/go.sum
index ed139ba3..caa6eeb3 100644
--- a/go.sum
+++ b/go.sum
@@ -432,8 +432,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/justinbarrick/go-k8s-portforward v1.0.2/go.mod h1:klMOboLnC1/UlkyJnYFjcMcbOtwAcKop+LkIZ4r428o=
-github.com/justinbarrick/go-k8s-portforward v1.0.3/go.mod h1:GkvGI25j2iHpJVINl/hZC+sbf9IJ1XkY1MtjSh3Usuk=
-github.com/justinbarrick/go-k8s-portforward v1.0.4-0.20190722134107-d79fe1b9d79d h1:xQ/ZtcWCKzWg5QbOhq6RFPvevl+IE580Vm0Vgxuw3xs=
 github.com/justinbarrick/go-k8s-portforward v1.0.4-0.20190722134107-d79fe1b9d79d/go.mod h1:GkvGI25j2iHpJVINl/hZC+sbf9IJ1XkY1MtjSh3Usuk=
 github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
diff --git a/pkg/portforward/portforward.go b/pkg/portforward/portforward.go
new file mode 100644
index 00000000..c752aad4
--- /dev/null
+++ b/pkg/portforward/portforward.go
@@ -0,0 +1,208 @@
+package portforward
+
+// based on https://github.com/justinbarrick/go-k8s-portforward
+// licensed under the Apache License 2.0
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+
+	"github.com/pkg/errors"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/util/httpstream"
+	"k8s.io/client-go/kubernetes"
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/portforward"
+	"k8s.io/client-go/transport/spdy"
+)
+
+// Used for creating a port forward into a Kubernetes pod
+// in a Kubernetes cluster.
+type PortForward struct {
+	// The parsed Kubernetes configuration file.
+	Config *rest.Config
+	// The initialized Kubernetes client.
+	Clientset kubernetes.Interface
+	// The pod name to use, required if Labels is empty.
+	Name string
+	// The labels to use to find the pod.
+	Labels metav1.LabelSelector
+	// The port on the pod to forward traffic to.
+	DestinationPort int
+	// The port that the port forward should listen to, random if not set.
+	ListenPort int
+	// The namespace to look for the pod in.
+	Namespace string
+	stopChan  chan struct{}
+	readyChan chan struct{}
+}
+
+// Initialize a port forwarder, loads the Kubernetes configuration file and creates the client.
+// You do not need to use this function if you have a client to use already - the PortForward
+// struct can be created directly.
+func NewPortForwarder(namespace string, labels metav1.LabelSelector, port int) (*PortForward, error) {
+	pf := &PortForward{
+		Namespace:       namespace,
+		Labels:          labels,
+		DestinationPort: port,
+	}
+
+	var err error
+	pf.Config, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+		clientcmd.NewDefaultClientConfigLoadingRules(),
+		&clientcmd.ConfigOverrides{},
+	).ClientConfig()
+	if err != nil {
+		return pf, errors.Wrap(err, "Could not load kubernetes configuration file")
+	}
+
+	pf.Clientset, err = kubernetes.NewForConfig(pf.Config)
+	if err != nil {
+		return pf, errors.Wrap(err, "Could not create kubernetes client")
+	}
+
+	return pf, nil
+}
+
+// Start a port forward to a pod - blocks until the tunnel is ready for use.
+func (p *PortForward) Start() error {
+	p.stopChan = make(chan struct{}, 1)
+	readyChan := make(chan struct{}, 1)
+	errChan := make(chan error, 1)
+
+	listenPort, err := p.getListenPort()
+	if err != nil {
+		return errors.Wrap(err, "Could not find a port to bind to")
+	}
+
+	dialer, err := p.dialer()
+	if err != nil {
+		return errors.Wrap(err, "Could not create a dialer")
+	}
+
+	ports := []string{
+		fmt.Sprintf("%d:%d", listenPort, p.DestinationPort),
+	}
+
+	discard := ioutil.Discard
+	pf, err := portforward.New(dialer, ports, p.stopChan, readyChan, discard, discard)
+	if err != nil {
+		return errors.Wrap(err, "Could not port forward into pod")
+	}
+
+	go func() {
+		errChan <- pf.ForwardPorts()
+	}()
+
+	select {
+	case err = <-errChan:
+		return errors.Wrap(err, "Could not create port forward")
+	case <-readyChan:
+		return nil
+	}
+
+	return nil
+}
+
+// Stop a port forward.
+func (p *PortForward) Stop() {
+	p.stopChan <- struct{}{}
+}
+
+// Returns the port that the port forward should listen on.
+// If ListenPort is set, then it returns ListenPort.
+// Otherwise, it will call getFreePort() to find an open port.
+func (p *PortForward) getListenPort() (int, error) {
+	var err error
+
+	if p.ListenPort == 0 {
+		p.ListenPort, err = p.getFreePort()
+	}
+
+	return p.ListenPort, err
+}
+
+// Get a free port on the system by binding to port 0, checking
+// the bound port number, and then closing the socket.
+func (p *PortForward) getFreePort() (int, error) {
+	listener, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return 0, err
+	}
+
+	port := listener.Addr().(*net.TCPAddr).Port
+	err = listener.Close()
+	if err != nil {
+		return 0, err
+	}
+
+	return port, nil
+}
+
+// Create an httpstream.Dialer for use with portforward.New
+func (p *PortForward) dialer() (httpstream.Dialer, error) {
+	pod, err := p.getPodName()
+	if err != nil {
+		return nil, errors.Wrap(err, "Could not get pod name")
+	}
+
+	url := p.Clientset.CoreV1().RESTClient().Post().
+		Resource("pods").
+		Namespace(p.Namespace).
+		Name(pod).
+		SubResource("portforward").URL()
+
+	transport, upgrader, err := spdy.RoundTripperFor(p.Config)
+	if err != nil {
+		return nil, errors.Wrap(err, "Could not create round tripper")
+	}
+
+	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url)
+	return dialer, nil
+}
+
+// Gets the pod name to port forward to, if Name is set, Name is returned. Otherwise,
+// it will call findPodByLabels().
+func (p *PortForward) getPodName() (string, error) {
+	var err error
+	if p.Name == "" {
+		p.Name, err = p.findPodByLabels()
+	}
+	return p.Name, err
+}
+
+// Find the name of a pod by label, returns an error if the label returns
+// more or less than one pod.
+// It searches for the labels specified by labels.
+func (p *PortForward) findPodByLabels() (string, error) {
+	if len(p.Labels.MatchLabels) == 0 && len(p.Labels.MatchExpressions) == 0 {
+		return "", errors.New("No pod labels specified")
+	}
+
+	pods, err := p.Clientset.CoreV1().Pods(p.Namespace).List(metav1.ListOptions{
+		LabelSelector: metav1.FormatLabelSelector(&p.Labels),
+		FieldSelector: fields.OneTermEqualSelector("status.phase", string(v1.PodRunning)).String(),
+	})
+
+	if err != nil {
+		return "", errors.Wrap(err, "Listing pods in kubernetes")
+	}
+
+	formatted := metav1.FormatLabelSelector(&p.Labels)
+
+	if len(pods.Items) == 0 {
+		return "", errors.New(fmt.Sprintf("Could not find running pod for selector: labels \"%s\"", formatted))
+	}
+
+	if len(pods.Items) != 1 {
+		return "", errors.New(fmt.Sprintf("Ambiguous pod: found more than one pod for selector: labels \"%s\"", formatted))
+	}
+
+	return pods.Items[0].ObjectMeta.Name, nil
+}


Test patch:
diff --git a/pkg/portforward/portforward_test.go b/pkg/portforward/portforward_test.go
new file mode 100644
index 00000000..3399fb11
--- /dev/null
+++ b/pkg/portforward/portforward_test.go
@@ -0,0 +1,195 @@
+package portforward
+
+// based on https://github.com/justinbarrick/go-k8s-portforward
+// licensed under the Apache License 2.0
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/fake"
+)
+
+func newPod(name string, labels map[string]string) *corev1.Pod {
+	return &corev1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Pod",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Labels: labels,
+			Name:   name,
+		},
+	}
+}
+
+func TestFindPodByLabels(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod1", map[string]string{
+				"name": "other",
+			}),
+			newPod("mypod2", map[string]string{
+				"name": "flux",
+			}),
+			newPod("mypod3", map[string]string{})),
+		Labels: metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"name": "flux",
+			},
+		},
+	}
+
+	pod, err := pf.findPodByLabels()
+	assert.Nil(t, err)
+	assert.Equal(t, "mypod2", pod)
+}
+
+func TestFindPodByLabelsNoneExist(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod1", map[string]string{
+				"name": "other",
+			})),
+		Labels: metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"name": "flux",
+			},
+		},
+	}
+
+	_, err := pf.findPodByLabels()
+	assert.NotNil(t, err)
+	assert.Equal(t, "Could not find running pod for selector: labels \"name=flux\"", err.Error())
+}
+
+func TestFindPodByLabelsMultiple(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod1", map[string]string{
+				"name": "flux",
+			}),
+			newPod("mypod2", map[string]string{
+				"name": "flux",
+			}),
+			newPod("mypod3", map[string]string{})),
+		Labels: metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"name": "flux",
+			},
+		},
+	}
+
+	_, err := pf.findPodByLabels()
+	assert.NotNil(t, err)
+	assert.Equal(t, "Ambiguous pod: found more than one pod for selector: labels \"name=flux\"", err.Error())
+}
+
+func TestFindPodByLabelsExpression(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod1", map[string]string{
+				"name": "lol",
+			}),
+			newPod("mypod2", map[string]string{
+				"name": "fluxd",
+			}),
+			newPod("mypod3", map[string]string{})),
+		Labels: metav1.LabelSelector{
+			MatchExpressions: []metav1.LabelSelectorRequirement{
+				metav1.LabelSelectorRequirement{
+					Key:      "name",
+					Operator: metav1.LabelSelectorOpIn,
+					Values:   []string{"flux", "fluxd"},
+				},
+			},
+		},
+	}
+
+	pod, err := pf.findPodByLabels()
+	assert.Nil(t, err)
+	assert.Equal(t, "mypod2", pod)
+}
+
+func TestFindPodByLabelsExpressionNotFound(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod1", map[string]string{
+				"name": "lol",
+			}),
+			newPod("mypod2", map[string]string{
+				"name": "lol",
+			}),
+			newPod("mypod3", map[string]string{})),
+		Labels: metav1.LabelSelector{
+			MatchExpressions: []metav1.LabelSelectorRequirement{
+				metav1.LabelSelectorRequirement{
+					Key:      "name",
+					Operator: metav1.LabelSelectorOpIn,
+					Values:   []string{"flux", "fluxd"},
+				},
+			},
+		},
+	}
+
+	_, err := pf.findPodByLabels()
+	assert.NotNil(t, err)
+	assert.Equal(t, "Could not find running pod for selector: labels \"name in (flux,fluxd)\"", err.Error())
+}
+
+func TestGetPodNameNameSet(t *testing.T) {
+	pf := PortForward{
+		Name: "hello",
+	}
+
+	pod, err := pf.getPodName()
+	assert.Nil(t, err)
+	assert.Equal(t, "hello", pod)
+}
+
+func TestGetPodNameNoNameSet(t *testing.T) {
+	pf := PortForward{
+		Clientset: fake.NewSimpleClientset(
+			newPod("mypod", map[string]string{
+				"name": "flux",
+			})),
+		Labels: metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				"name": "flux",
+			},
+		},
+	}
+
+	pod, err := pf.getPodName()
+	assert.Nil(t, err)
+	assert.Equal(t, "mypod", pod)
+	assert.Equal(t, pf.Name, pod)
+}
+
+func TestGetFreePort(t *testing.T) {
+	pf := PortForward{}
+	port, err := pf.getFreePort()
+	assert.Nil(t, err)
+	assert.NotZero(t, port)
+}
+
+func TestGetListenPort(t *testing.T) {
+	pf := PortForward{
+		ListenPort: 80,
+	}
+
+	port, err := pf.getListenPort()
+	assert.Nil(t, err)
+	assert.Equal(t, 80, port)
+}
+
+func TestGetListenPortRandom(t *testing.T) {
+	pf := PortForward{}
+
+	port, err := pf.getListenPort()
+	assert.Nil(t, err)
+	assert.NotZero(t, port)
+	assert.Equal(t, pf.ListenPort, port)
+}