diff --git a/Makefile b/Makefile index edb2c34..9e8092d 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -v -ginkgo.v -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..1f24749 --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,253 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os/exec" + "strings" + "time" + + k8sapi "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "context" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +type HTTPResponse struct { + Body string + StatusCode int +} + +type HTTPheader struct { + name string + value string +} + +var k8sClient client.Client +var testEnv *envtest.Environment + +func createRole(k8sClient client.Client, nsName string, roleName string) { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Namespace: nsName, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appstudio.redhat.com"}, + Resources: []string{"applications", "components"}, + Verbs: []string{"create", "list", "watch", "delete"}, + }, + }, + } + err := k8sClient.Create(context.Background(), role) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error creating 'Role' resource: %v", err)) +} + +func createRoleBinding(k8sClient client.Client, bindingName string, nsName string, userName string, roleName string) { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: nsName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "User", + Name: userName, + APIGroup: "rbac.authorization.k8s.io", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: roleName, + APIGroup: "rbac.authorization.k8s.io", + }, + } + err := k8sClient.Create(context.Background(), roleBinding) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error creating 'roleBinding' resource: %v", err)) +} + +func createNamespace(k8sClient client.Client, name string) { + namespaced := &k8sapi.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "konflux.ci/type": "user", + "kubernetes.io/metadata.name": name, + }, + }, + } + err := k8sClient.Create(context.Background(), namespaced) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error creating 'Namespace' resource: %v", err)) +} + +func performHTTPGetCall(url string, header HTTPheader) (*HTTPResponse, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("Error creating request: %s", err) + return nil, err + } + if header.name != "" { + req.Header.Add(header.name, header.value) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Error making request: %s", err) + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading response body: %s", err) + return nil, err + } + response := &HTTPResponse{ + Body: string(body), + StatusCode: resp.StatusCode, + } + return response, nil +} + +func TestCmd(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Main Suite") +} + +var _ = Describe("Signup endpoint", func() { + Context("Calling the signup endpoint with GET", func() { + It("responds with ready and signedup", func() { + url := "http://localhost:5000/api/v1/signup" + expectedCode := http.StatusOK + expectedBody := `{"status":{"ready":true,"reason":"SignedUp"}}` + + resp, err := performHTTPGetCall(url, HTTPheader{}) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Unexpected error testing the \"%s\" endpoint: %v", url, err)) + Expect(resp.StatusCode).To(Equal(expectedCode)) + Expect(strings.TrimSpace(expectedBody)).To(Equal(strings.TrimSpace(resp.Body))) + }) + }) +}) + +var _ = DescribeTable("Workspace endpoint", func(header HTTPheader, expectedCode int, expectedBody string) { + url := "http://localhost:5000/workspaces" + resp, err := performHTTPGetCall(url, header) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Unexpected error testing the \"%s\" endpoint: %v", url, err)) + Expect(resp.StatusCode).To(Equal(expectedCode)) + Expect(strings.TrimSpace(expectedBody)).To(Equal(strings.TrimSpace(resp.Body))) +}, + Entry( + "Calling the workspace endpoint for user1 responds only with the 'test-tenant' workspace info", + HTTPheader{"X-Email", "user1@konflux.dev"}, + http.StatusOK, + `{"kind":"WorkspaceList","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{},"items":[{"kind":"Workspace","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{"name":"test-tenant","creationTimestamp":null},"status":{"namespaces":[{"name":"test-tenant","type":"default"}]}}]}`), + Entry( + "Workspace endpoint for user2 responds with 2 namespaces info", + HTTPheader{"X-Email", "user2@konflux.dev"}, + http.StatusOK, + `{"kind":"WorkspaceList","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{},"items":[{"kind":"Workspace","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{"name":"test-tenant","creationTimestamp":null},"status":{"namespaces":[{"name":"test-tenant","type":"default"}]}},{"kind":"Workspace","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{"name":"test-tenant-2","creationTimestamp":null},"status":{"namespaces":[{"name":"test-tenant-2","type":"default"}]}}]}`), + Entry( + "Workspace endpoint with no header", + HTTPheader{}, + 500, + `{"message":"Internal Server Error"}`), +) + +var _ = DescribeTable("Specific workspace endpoint", func(endpoint string, header HTTPheader, expectedCode int, expectedBody string) { + url := "http://localhost:5000/workspaces/" + endpoint + resp, err := performHTTPGetCall(url, header) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Unexpected error testing the \"%s\" endpoint: %v", url, err)) + Expect(resp.StatusCode).To(Equal(expectedCode)) + Expect(strings.TrimSpace(expectedBody)).To(Equal(strings.TrimSpace(resp.Body))) +}, + Entry( + "Calling the workspace endpoint for the test-tenant workspace for user2", + "test-tenant", + HTTPheader{"X-Email", "user2@konflux.dev"}, + http.StatusOK, + `{"kind":"Workspace","apiVersion":"toolchain.dev.openshift.com/v1alpha1","metadata":{"name":"test-tenant","creationTimestamp":null},"status":{"namespaces":[{"name":"test-tenant","type":"default"}]}}`), + Entry( + "Specific workspace endpoint for test-tenant-2 for user1 only", + "test-tenant-2", + HTTPheader{"X-Email", "user1@konflux.dev"}, + 404, + `{"message":"Not Found"}`), +) + +func CreateKubeconfigFileForRestConfig(restConfig rest.Config) string { + clusters := make(map[string]*clientcmdapi.Cluster) + clusters["default-cluster"] = &clientcmdapi.Cluster{ + Server: restConfig.Host, + CertificateAuthorityData: restConfig.CAData, + } + contexts := make(map[string]*clientcmdapi.Context) + contexts["default-context"] = &clientcmdapi.Context{ + Cluster: "default-cluster", + AuthInfo: "default-user", + } + authinfos := make(map[string]*clientcmdapi.AuthInfo) + authinfos["default-user"] = &clientcmdapi.AuthInfo{ + ClientCertificateData: restConfig.CertData, + ClientKeyData: restConfig.KeyData, + } + clientConfig := clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: clusters, + Contexts: contexts, + CurrentContext: "default-context", + AuthInfos: authinfos, + } + kubeConfigFile, _ := os.CreateTemp("", "kubeconfig") + _ = clientcmd.WriteToFile(clientConfig, kubeConfigFile.Name()) + return kubeConfigFile.Name() +} + +var serverProcess *exec.Cmd + +var _ = BeforeSuite(func() { + testEnv = &envtest.Environment{} + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error creating the envtest environment during test setup: %v", err)) + kubeconfigPath := CreateKubeconfigFileForRestConfig(*cfg) + os.Setenv("KUBECONFIG", kubeconfigPath) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error creating the client during test setup: %v", err)) + Expect(k8sClient).NotTo(BeNil()) + + user1 := "user1@konflux.dev" + user2 := "user2@konflux.dev" + createNamespace(k8sClient, "test-tenant") + createNamespace(k8sClient, "test-tenant-2") + createNamespace(k8sClient, "test-tenant-3") + createRole(k8sClient, "test-tenant", "namespace-access") + createRole(k8sClient, "test-tenant-2", "namespace-access-2") + createRoleBinding(k8sClient, "namespace-access-user-binding", "test-tenant", user1, "namespace-access") + createRoleBinding(k8sClient, "namespace-access-user-binding-2", "test-tenant", user2, "namespace-access") + createRoleBinding(k8sClient, "namespace-access-user-binding-3", "test-tenant-2", user2, "namespace-access-2") + serverProcess = exec.Command("go", "run", "main.go") + err = serverProcess.Start() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error starting the server during test setup: %v", err)) + time.Sleep(5 * time.Second) +}) + +var _ = AfterSuite(func() { + Expect(os.Unsetenv("KUBECONFIG")).To(Succeed()) + if serverProcess != nil && serverProcess.Process != nil { + err := serverProcess.Process.Kill() + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Error killing the server during test teardown: %v", err)) + } +}) diff --git a/go.mod b/go.mod index 6574189..a50fee6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.20 require ( github.com/codeready-toolchain/api v0.0.0-20240322110702-5ab3840476e9 github.com/labstack/echo/v4 v4.11.4 + github.com/onsi/ginkgo/v2 v2.1.4 + github.com/onsi/gomega v1.19.0 k8s.io/api v0.25.0 k8s.io/apimachinery v0.25.0 k8s.io/client-go v0.25.0 @@ -53,6 +55,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.25.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect diff --git a/go.sum b/go.sum index f674619..65063f7 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -134,6 +135,7 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -182,7 +184,9 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/openshift/api v0.0.0-20230213134911-7ba313770556 h1:7W2fOhJicyEff24VaF7ASNzPtYvr+iSCVft4SIBAzaE= github.com/openshift/api v0.0.0-20230213134911-7ba313770556/go.mod h1:aQ6LDasvHMvHZXqLHnX2GRmnfTWCF/iIwz8EMTTIE9A= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -504,10 +508,12 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0= k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk= k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= +k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= +k8s.io/component-base v0.25.0 h1:haVKlLkPCFZhkcqB6WCvpVxftrg6+FK5x1ZuaIDaQ5Y= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=