From 95d21fa7682e88a642e6847f33f10569695e72af Mon Sep 17 00:00:00 2001
From: Guillaume Tardif <guillaume.tardif@gmail.com>
Date: Mon, 1 Feb 2021 12:08:40 +0100
Subject: [PATCH] First kube e2e. Adapted context create kubernetes command to
 allow non interactive mode.

Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>
---
 .github/workflows/kube-tests.yml | 63 ++++++++++++++++++++++++++++++++
 Makefile                         |  3 ++
 cli/cmd/context/create_kube.go   |  3 +-
 kube/context.go                  | 21 +++++++----
 kube/e2e/compose_test.go         | 45 ++++++++++++++++-------
 utils/e2e/framework.go           | 31 ++++++++++++++++
 6 files changed, 145 insertions(+), 21 deletions(-)
 create mode 100644 .github/workflows/kube-tests.yml

diff --git a/.github/workflows/kube-tests.yml b/.github/workflows/kube-tests.yml
new file mode 100644
index 000000000..a39d8c6b4
--- /dev/null
+++ b/.github/workflows/kube-tests.yml
@@ -0,0 +1,63 @@
+name: Kube integration tests
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+
+jobs:
+  check-optional-tests:
+    name: Check if needs to run Kube tests
+    runs-on: ubuntu-latest
+    outputs:
+      trigger-kube: ${{steps.runkubetest.outputs.triggered}}
+    steps:
+      - uses: khan/pull-request-comment-trigger@master
+        name: Check if test Kube
+        if: github.event_name == 'pull_request'
+        id: runkubetest
+        with:
+          trigger: '/test-kube'
+
+
+  kube-tests:
+    name: Kube e2e tests
+    runs-on: ubuntu-latest
+    env:
+      GO111MODULE: "on"
+    needs: check-optional-tests
+    if: github.ref == 'refs/heads/main' || needs.check-optional-tests.outputs.trigger-kube == 'true'
+    steps:
+      - name: Set up Go 1.15
+        uses: actions/setup-go@v1
+        with:
+          go-version: 1.15
+        id: go
+
+      - name: Setup docker CLI
+        run: |
+          curl https://download.docker.com/linux/static/stable/x86_64/docker-20.10.2.tgz | tar xz
+          sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version
+
+      - name: Setup Kube tools
+        run: |
+          sudo apt-get install jq && jq --version
+          curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.10.0/kind-linux-amd64 && chmod +x ./kind && sudo mv ./kind /usr/bin/ && kind version
+          curl -LO "https://dl.k8s.io/release/v1.20.2/bin/linux/amd64/kubectl" && sudo mv kubectl /usr/bin/ && kubectl version --client
+
+      - name: Checkout code into the Go module directory
+        uses: actions/checkout@v2
+
+      - uses: actions/cache@v2
+        with:
+          path: ~/go/pkg/mod
+          key: go-${{ hashFiles('**/go.sum') }}
+
+      - name: Build for Kube e2e tests
+        env:
+          BUILD_TAGS: kube
+        run: make -f builder.Makefile cli
+
+      - name: Kube e2e Test
+        run: make e2e-kube
diff --git a/Makefile b/Makefile
index 45486e63a..4c168a15d 100644
--- a/Makefile
+++ b/Makefile
@@ -49,6 +49,9 @@ e2e-local: ## Run End to end local tests. Set E2E_TEST=TestName to run a single
 e2e-win-ci: ## Run end to end local tests on Windows CI, no Docker for Linux containers available ATM. Set E2E_TEST=TestName to run a single test
 	go test -count=1 -v $(TEST_FLAGS) ./local/e2e/cli-only
 
+e2e-kube: ## Run End to end Kube tests. Set E2E_TEST=TestName to run a single test
+	go test -timeout 10m -count=1 -v $(TEST_FLAGS) ./kube/e2e
+
 e2e-aci: ## Run End to end ACI tests. Set E2E_TEST=TestName to run a single test
 	go test -timeout 15m -count=1 -v $(TEST_FLAGS) ./aci/e2e
 
diff --git a/cli/cmd/context/create_kube.go b/cli/cmd/context/create_kube.go
index b42370e9a..609716e38 100644
--- a/cli/cmd/context/create_kube.go
+++ b/cli/cmd/context/create_kube.go
@@ -50,7 +50,8 @@ func createKubeCommand() *cobra.Command {
 	}
 
 	addDescriptionFlag(cmd, &opts.Description)
-	cmd.Flags().StringVar(&opts.KubeconfigPath, "kubeconfig", "", "The endpoint of the Kubernetes manager")
+	cmd.Flags().StringVar(&opts.KubeConfigPath, "kubeconfig", "", "The endpoint of the Kubernetes manager")
+	cmd.Flags().StringVar(&opts.KubeContextName, "kubecontext", "", "The name of the context to use in kubeconfig")
 	cmd.Flags().BoolVar(&opts.FromEnvironment, "from-env", false, "Get endpoint and creds from env vars")
 	return cmd
 }
diff --git a/kube/context.go b/kube/context.go
index 0b9201b00..a9b4f180b 100644
--- a/kube/context.go
+++ b/kube/context.go
@@ -29,9 +29,9 @@ import (
 
 // ContextParams options for creating a Kubernetes context
 type ContextParams struct {
-	ContextName     string
+	KubeContextName string
 	Description     string
-	KubeconfigPath  string
+	KubeConfigPath  string
 	FromEnvironment bool
 }
 
@@ -45,7 +45,7 @@ func (cp ContextParams) CreateContextData() (interface{}, string, error) {
 	}
 	user := prompt.User{}
 	selectContext := func() error {
-		contexts, err := kubernetes.ListAvailableKubeConfigContexts(cp.KubeconfigPath)
+		contexts, err := kubernetes.ListAvailableKubeConfigContexts(cp.KubeConfigPath)
 		if err != nil {
 			return err
 		}
@@ -57,11 +57,18 @@ func (cp ContextParams) CreateContextData() (interface{}, string, error) {
 			}
 			return err
 		}
-		cp.ContextName = contexts[selected]
+		cp.KubeContextName = contexts[selected]
 		return nil
 	}
 
-	if cp.KubeconfigPath != "" {
+	if cp.KubeConfigPath != "" {
+		if cp.KubeContextName != "" {
+			return store.KubeContext{
+				ContextName:     cp.KubeContextName,
+				KubeconfigPath:  cp.KubeConfigPath,
+				FromEnvironment: cp.FromEnvironment,
+			}, cp.Description, nil
+		}
 		err := selectContext()
 		if err != nil {
 			return nil, "", err
@@ -95,8 +102,8 @@ func (cp ContextParams) CreateContextData() (interface{}, string, error) {
 		}
 	}
 	return store.KubeContext{
-		ContextName:     cp.ContextName,
-		KubeconfigPath:  cp.KubeconfigPath,
+		ContextName:     cp.KubeContextName,
+		KubeconfigPath:  cp.KubeConfigPath,
 		FromEnvironment: cp.FromEnvironment,
 	}, cp.Description, nil
 }
diff --git a/kube/e2e/compose_test.go b/kube/e2e/compose_test.go
index d435310c5..688843e1c 100644
--- a/kube/e2e/compose_test.go
+++ b/kube/e2e/compose_test.go
@@ -18,8 +18,8 @@ package e2e
 
 import (
 	"fmt"
-	"net/http"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -48,31 +48,50 @@ func TestComposeUp(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 
 	const projectName = "compose-kube-demo"
+	kubeconfig := filepath.Join(c.ConfigDir, "kubeconfig")
+	kindClusterName := "e2e"
+	kubeContextName := "kind-" + kindClusterName
+	dockerContextName := "kube-e2e-ctx"
+
+	t.Run("create kube cluster", func(t *testing.T) {
+		c.RunCmd("kind", "create", "cluster", "--name", kindClusterName, "--kubeconfig", kubeconfig, "--wait", "180s")
+	})
+	defer func() {
+		c.RunDockerCmd("context", "use", "default")
+		c.RunCmd("kind", "delete", "cluster", "--name", kindClusterName, "--kubeconfig", kubeconfig)
+	}()
 
 	t.Run("create kube context", func(t *testing.T) {
-		res := c.RunDockerCmd("context", "create", "kubernetes", "--kubeconfig", "/Users/gtardif/.kube/config", "--kubecontext", "docker-desktop", "kube-e2e")
-		res.Assert(t, icmd.Expected{Out: `Successfully created kube context "kube-e2e"`})
-		c.RunDockerCmd("context", "use", "kube-e2e")
+		res := c.RunDockerCmd("context", "create", "kubernetes", "--kubeconfig", kubeconfig, "--kubecontext", kubeContextName, dockerContextName)
+		res.Assert(t, icmd.Expected{Out: fmt.Sprintf("Successfully created kube context %q", dockerContextName)})
+		c.RunDockerCmd("context", "use", dockerContextName)
 	})
 
 	t.Run("up", func(t *testing.T) {
 		c.RunDockerCmd("compose", "-f", "./kube-simple-demo/demo_sentences.yaml", "--project-name", projectName, "up", "-d")
 	})
 
-	t.Run("check running project", func(t *testing.T) {
-		res := c.RunDockerCmd("compose", "-p", projectName, "ps")
-		res.Assert(t, icmd.Expected{Out: `web`})
-
-		endpoint := "http://localhost:95"
-		output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
-		assert.Assert(t, strings.Contains(output, `"word":`))
+	t.Run("compose ls", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "ls", "--format", "json")
+		res.Assert(t, icmd.Expected{Out: `[{"Name":"compose-kube-demo","Status":"deployed"}]`})
 	})
+
+	t.Run("check running project", func(t *testing.T) {
+		// Docker Desktop kube cluster automatically exposes ports on the host, this is not the case with kind on Desktop,
+		//we need to connect to the clusterIP, from the kind container
+		res := c.RunCmd("sh", "-c", "kubectl --kubeconfig "+kubeconfig+" get service/web -o json | jq -r '.spec.clusterIP'")
+		clusterIP := strings.ReplaceAll(strings.TrimSpace(res.Stdout()), `"`, "")
+
+		endpoint := fmt.Sprintf("http://%s:80/words/noun", clusterIP)
+		c.WaitForCmdResult(icmd.Command("docker", "--context", "default", "exec", "e2e-control-plane", "curl", endpoint), StdoutContains(`"word":`), 3*time.Minute, 3*time.Second)
+	})
+
 	t.Run("down", func(t *testing.T) {
 		_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
 	})
 
-	t.Run("check containers after down", func(t *testing.T) {
-		res := c.RunDockerCmd("ps", "--all")
+	t.Run("check stack after down", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "ls")
 		assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
 	})
 }
diff --git a/utils/e2e/framework.go b/utils/e2e/framework.go
index a525bb9c8..ce9360e4a 100644
--- a/utils/e2e/framework.go
+++ b/utils/e2e/framework.go
@@ -203,6 +203,15 @@ func (c *E2eCLI) RunDockerOrExitError(args ...string) *icmd.Result {
 	return icmd.RunCmd(c.NewDockerCmd(args...))
 }
 
+// RunCmd runs a command, expects no error and returns a result
+func (c *E2eCLI) RunCmd(args ...string) *icmd.Result {
+	fmt.Printf("	[%s] %s\n", c.test.Name(), strings.Join(args, " "))
+	assert.Assert(c.test, len(args) >= 1, "require at least one command in parameters")
+	res := icmd.RunCmd(c.NewCmd(args[0], args[1:]...))
+	res.Assert(c.test, icmd.Success)
+	return res
+}
+
 // RunDockerCmd runs a docker command, expects no error and returns a result
 func (c *E2eCLI) RunDockerCmd(args ...string) *icmd.Result {
 	res := c.RunDockerOrExitError(args...)
@@ -210,6 +219,28 @@ func (c *E2eCLI) RunDockerCmd(args ...string) *icmd.Result {
 	return res
 }
 
+// StdoutContains returns a predicate on command result expecting a string in stdout
+func StdoutContains(expected string) func(*icmd.Result) bool {
+	return func(res *icmd.Result) bool {
+		return strings.Contains(res.Stdout(), expected)
+	}
+}
+
+// WaitForCmdResult try to execute a cmd until resulting output matches given predicate
+func (c *E2eCLI) WaitForCmdResult(command icmd.Cmd, predicate func(*icmd.Result) bool, timeout time.Duration, delay time.Duration) {
+	assert.Assert(c.test, timeout.Nanoseconds() > delay.Nanoseconds(), "timeout must be greater than delay")
+	var res *icmd.Result
+	checkStopped := func(logt poll.LogT) poll.Result {
+		fmt.Printf("	[%s] %s\n", c.test.Name(), strings.Join(command.Command, " "))
+		res = icmd.RunCmd(command)
+		if !predicate(res) {
+			return poll.Continue("Cmd output did not match requirement: %q", res.Combined())
+		}
+		return poll.Success()
+	}
+	poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
+}
+
 // PathEnvVar returns path (os sensitive) for running test
 func (c *E2eCLI) PathEnvVar() string {
 	path := c.BinDir + ":" + os.Getenv("PATH")