diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e70f175..ab3f538 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ permissions: jobs: lint: runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v4 @@ -79,7 +79,7 @@ jobs: - name: Install Ginkgo run: go install github.com/onsi/ginkgo/v2/ginkgo - + - name: Build and package run: | make build @@ -105,12 +105,75 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: k3s-logs + name: e2e-k3s-logs path: /tmp/k3s.log - + - name: Archive k3k logs uses: actions/upload-artifact@v4 if: always() with: - name: k3k-logs + name: e2e-k3k-logs + path: /tmp/k3k.log + + tests-cli: + runs-on: ubuntu-latest + needs: validate + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install Ginkgo + run: go install github.com/onsi/ginkgo/v2/ginkgo + + - name: Set coverage environment + run: | + mkdir ${{ github.workspace }}/covdata + + echo "COVERAGE=true" >> $GITHUB_ENV + echo "GOCOVERDIR=${{ github.workspace }}/covdata" >> $GITHUB_ENV + + - name: Build and package + run: | + make build + make package + + # add k3kcli to $PATH + echo "${{ github.workspace }}/bin" >> $GITHUB_PATH + + - name: Check k3kcli + run: k3kcli -v + + - name: Run cli tests + run: make test-cli + + - name: Convert coverage data + run: go tool covdata textfmt -i=${{ github.workspace }}/covdata -o ${{ github.workspace }}/covdata/cover.out + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ github.workspace }}/covdata/cover.out + flags: cli + + - name: Archive k3s logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: cli-k3s-logs + path: /tmp/k3s.log + + - name: Archive k3k logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: cli-k3k-logs path: /tmp/k3k.log diff --git a/.gitignore b/.gitignore index 5d1a1c1..b01c05e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ __debug* *-kubeconfig.yaml .envtest +cover.out +covcounters.** +covmeta.** diff --git a/Makefile b/Makefile index 1fd1c53..bb3e335 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ REPO ?= rancher +COVERAGE ?= false VERSION ?= $(shell git describe --tags --always --dirty --match="v[0-9]*") ## Dependencies @@ -29,7 +30,7 @@ version: ## Print the current version .PHONY: build build: ## Build the the K3k binaries (k3k, k3k-kubelet and k3kcli) - @VERSION=$(VERSION) ./scripts/build + @VERSION=$(VERSION) COVERAGE=$(COVERAGE) ./scripts/build .PHONY: package package: package-k3k package-k3k-kubelet ## Package the k3k and k3k-kubelet Docker images @@ -68,7 +69,11 @@ test-kubelet-controller: ## Run the controller tests (pkg/controller) .PHONY: test-e2e test-e2e: ## Run the e2e tests - $(GINKGO) $(GINKGO_FLAGS) tests + $(GINKGO) $(GINKGO_FLAGS) --label-filter=e2e tests + +.PHONY: test-cli +test-cli: ## Run the cli tests + $(GINKGO) $(GINKGO_FLAGS) --label-filter=cli tests .PHONY: generate generate: ## Generate the CRDs specs diff --git a/scripts/build b/scripts/build index 5e7b8af..04accee 100755 --- a/scripts/build +++ b/scripts/build @@ -4,12 +4,20 @@ set -eou pipefail LDFLAGS="-X \"github.com/rancher/k3k/pkg/buildinfo.Version=${VERSION}\"" +build_args=() + +# Check if coverage is enabled, e.g., in CI or when manually set +if [[ "${COVERAGE:-false}" == "true" ]]; then + echo "Coverage build enabled." + build_args+=("-cover" "-coverpkg=./..." "-covermode=atomic") +fi + echo "Building k3k... [cli os/arch: $(go env GOOS)/$(go env GOARCH)]" echo "Current TAG: ${VERSION} " export CGO_ENABLED=0 -GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o bin/k3k -GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o bin/k3k-kubelet ./k3k-kubelet +GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" "${build_args[@]}" -o bin/k3k +GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" "${build_args[@]}" -o bin/k3k-kubelet ./k3k-kubelet # build the cli for the local OS and ARCH -go build -ldflags="${LDFLAGS}" -o bin/k3kcli ./cli +go build -ldflags="${LDFLAGS}" "${build_args[@]}" -o bin/k3kcli ./cli diff --git a/tests/cli_test.go b/tests/cli_test.go new file mode 100644 index 0000000..9caa1a3 --- /dev/null +++ b/tests/cli_test.go @@ -0,0 +1,119 @@ +package k3k_test + +import ( + "bytes" + "context" + "os/exec" + "time" + + "k8s.io/apimachinery/pkg/util/rand" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func K3kcli(args ...string) (string, string, error) { + stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} + + cmd := exec.CommandContext(context.Background(), "k3kcli", args...) + cmd.Stdout = stdout + cmd.Stderr = stderr + + err := cmd.Run() + + return stdout.String(), stderr.String(), err +} + +var _ = When("using the k3kcli", Label("cli"), func() { + It("can get the version", func() { + stdout, _, err := K3kcli("--version") + Expect(err).To(Not(HaveOccurred())) + Expect(stdout).To(ContainSubstring("k3kcli Version: v")) + }) + + When("trying the cluster commands", func() { + It("can create, list and delete a cluster", func() { + var ( + stdout string + stderr string + err error + ) + + clusterName := "cluster-" + rand.String(5) + clusterNamespace := "k3k-" + clusterName + + _, stderr, err = K3kcli("cluster", "create", clusterName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(ContainSubstring("You can start using the cluster")) + + stdout, stderr, err = K3kcli("cluster", "list") + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring(clusterNamespace)) + + _, stderr, err = K3kcli("cluster", "delete", clusterName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(ContainSubstring("Deleting [%s] cluster in namespace [%s]", clusterName, clusterNamespace)) + + // The deletion could take a bit + Eventually(func() string { + stdout, stderr, err := K3kcli("cluster", "list", "-n", clusterNamespace) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + return stdout + stderr + }). + WithTimeout(time.Second * 5). + WithPolling(time.Second). + Should(BeEmpty()) + }) + }) + + When("trying the policy commands", func() { + It("can create, list and delete a policy", func() { + var ( + stdout string + stderr string + err error + ) + + policyName := "policy-" + rand.String(5) + + _, stderr, err = K3kcli("policy", "create", policyName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(ContainSubstring("Creating policy [%s]", policyName)) + + stdout, stderr, err = K3kcli("policy", "list") + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(ContainSubstring(policyName)) + + stdout, stderr, err = K3kcli("policy", "delete", policyName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stdout).To(BeEmpty()) + Expect(stderr).To(BeEmpty()) + + stdout, stderr, err = K3kcli("policy", "list") + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stdout).To(BeEmpty()) + Expect(stderr).To(BeEmpty()) + }) + }) + + When("trying the kubeconfig command", func() { + It("can generate a kubeconfig", func() { + var ( + stderr string + err error + ) + + clusterName := "cluster-" + rand.String(5) + + _, stderr, err = K3kcli("cluster", "create", clusterName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(ContainSubstring("You can start using the cluster")) + + _, stderr, err = K3kcli("kubeconfig", "generate", "--name", clusterName) + Expect(err).To(Not(HaveOccurred()), string(stderr)) + Expect(stderr).To(ContainSubstring("You can start using the cluster")) + }) + }) +}) diff --git a/tests/tests_suite_test.go b/tests/tests_suite_test.go index 14a82ac..4646b95 100644 --- a/tests/tests_suite_test.go +++ b/tests/tests_suite_test.go @@ -40,17 +40,20 @@ func TestTests(t *testing.T) { } var ( - k3sContainer *k3s.K3sContainer - hostIP string - restcfg *rest.Config - k8s *kubernetes.Clientset - k8sClient client.Client + k3sContainer *k3s.K3sContainer + hostIP string + restcfg *rest.Config + k8s *kubernetes.Clientset + k8sClient client.Client + kubeconfigPath string ) var _ = BeforeSuite(func() { var err error ctx := context.Background() + GinkgoWriter.Println("GOCOVERDIR:", os.Getenv("GOCOVERDIR")) + k3sContainer, err = k3s.Run(ctx, "rancher/k3s:v1.32.1-k3s1") Expect(err).To(Not(HaveOccurred())) @@ -62,6 +65,19 @@ var _ = BeforeSuite(func() { kubeconfig, err := k3sContainer.GetKubeConfig(context.Background()) Expect(err).To(Not(HaveOccurred())) + tmpFile, err := os.CreateTemp("", "kubeconfig-") + Expect(err).To(Not(HaveOccurred())) + + _, err = tmpFile.Write(kubeconfig) + Expect(err).To(Not(HaveOccurred())) + Expect(tmpFile.Close()).To(Succeed()) + + kubeconfigPath = tmpFile.Name() + + Expect(os.Setenv("KUBECONFIG", kubeconfigPath)).To(Succeed()) + + DeferCleanup(os.Remove, kubeconfigPath) + initKubernetesClient(kubeconfig) installK3kChart(kubeconfig) }) @@ -205,7 +221,7 @@ func readFileWithinPod(ctx context.Context, client *kubernetes.Clientset, config output := new(bytes.Buffer) - stderr, err := exec(ctx, client, config, namespace, name, command, nil, output) + stderr, err := podExec(ctx, client, config, namespace, name, command, nil, output) if err != nil || len(stderr) > 0 { return nil, fmt.Errorf("faile to read the following file %s: %v", path, err) } @@ -213,7 +229,7 @@ func readFileWithinPod(ctx context.Context, client *kubernetes.Clientset, config return output.Bytes(), nil } -func exec(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, namespace, name string, command []string, stdin io.Reader, stdout io.Writer) ([]byte, error) { +func podExec(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, namespace, name string, command []string, stdin io.Reader, stdout io.Writer) ([]byte, error) { req := clientset.CoreV1().RESTClient().Post(). Resource("pods"). Name(name).