From f7b2b980b172c8e549ea13c4543ebceaf15ec320 Mon Sep 17 00:00:00 2001 From: myan Date: Fri, 31 May 2024 11:19:17 +0000 Subject: [PATCH] add e2e test for api Signed-off-by: myan --- Makefile | 12 +- test/e2e/pkg/api_test.go | 202 ++++++++++++++++++++++++++++++++ test/e2e/pkg/resources_test.go | 44 +++---- test/e2e/pkg/serverside_test.go | 3 +- test/e2e/pkg/suite_test.go | 65 +++++----- 5 files changed, 259 insertions(+), 67 deletions(-) create mode 100644 test/e2e/pkg/api_test.go diff --git a/Makefile b/Makefile index 424361e6..f6c4c8d1 100755 --- a/Makefile +++ b/Makefile @@ -406,8 +406,14 @@ e2e-test/teardown: ./test/e2e/setup/e2e_teardown.sh .PHONY: e2e-test/teardown -e2e-test: e2e-test/teardown e2e-test/setup +e2e-test: + @if [ -z "$$consumer_name" ] || [ -z "$$api_server" ] || [ -z "$$consumer_kubeconfig" ]; then \ + make e2e-test/teardown; \ + make e2e-test/setup; \ + fi ginkgo --output-dir="${PWD}/test/e2e/report" --json-report=report.json --junit-report=report.xml \ - ${PWD}/test/e2e/pkg -- -consumer_name=$(shell cat ${PWD}/test/e2e/.consumer_name) \ - -api-server=https://$(shell cat ${PWD}/test/e2e/.external_host_ip):30080 -kubeconfig=${PWD}/test/e2e/.kubeconfig + ${PWD}/test/e2e/pkg -- \ + -api-server=$${api_server:-https://$$(cat ${PWD}/test/e2e/.external_host_ip):30080} \ + -consumer-name=$${consumer_name:-$$(cat ${PWD}/test/e2e/.consumer_name)} \ + -consumer-kubeconfig=$${consumer_kubeconfig:-${PWD}/test/e2e/.kubeconfig} .PHONY: e2e-test diff --git a/test/e2e/pkg/api_test.go b/test/e2e/pkg/api_test.go new file mode 100644 index 00000000..8023a2df --- /dev/null +++ b/test/e2e/pkg/api_test.go @@ -0,0 +1,202 @@ +package e2e_test + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openshift-online/maestro/pkg/api/openapi" +) + +// go test -v ./test/e2e/pkg -args -api-server=$api_server -consumer-name=$consumer_name -consumer-kubeconfig=$consumer_kubeconfig -ginkgo.focus "Server Side API" +var _ = Describe("Server Side API", Ordered, func() { + var testConsumerName string + var testConsumerID string + BeforeAll(func() { + testConsumerName = "test-consumer" + }) + + Context("consumer API", func() { + It("list the default consumer", func() { + consumerList, resp, err := apiClient.DefaultApi.ApiMaestroV1ConsumersGet(ctx).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(consumerList).NotTo(BeNil()) + Expect(len(consumerList.Items) > 0).To(BeTrue()) + + got := false + for _, consumer := range consumerList.Items { + if *consumer.Name == consumerOpts.Name { + got = true + } + } + Expect(got).To(BeTrue()) + }) + + It("create consumer", func() { + consumer := openapi.Consumer{Name: &testConsumerName} + created, resp, err := apiClient.DefaultApi.ApiMaestroV1ConsumersPost(ctx).Consumer(consumer).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(*created.Id).NotTo(BeEmpty()) + testConsumerID = *created.Id + + got, resp, err := apiClient.DefaultApi.ApiMaestroV1ConsumersIdGet(ctx, testConsumerID).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(got).NotTo(BeNil()) + }) + + It("patch consumer", func() { + labels := &map[string]string{"hello": "world"} + patched, resp, err := apiClient.DefaultApi.ApiMaestroV1ConsumersIdPatch(ctx, testConsumerID). + ConsumerPatchRequest(openapi.ConsumerPatchRequest{Labels: labels}).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + _, ok := patched.GetLabelsOk() + Expect(ok).To(BeTrue()) + + got, resp, err := apiClient.DefaultApi.ApiMaestroV1ConsumersIdGet(ctx, testConsumerID).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(got).NotTo(BeNil()) + eq := reflect.DeepEqual(*labels, *got.Labels) + Expect(eq).To(BeTrue()) + }) + + AfterAll(func() { + // TODO: add the conusmer deletion + }) + }) + + Context("resource API on fake consumer", func() { + var resourceID string + var resourceVersion *int32 + It("create resource", func() { + manifestIn, err := GetDeployManifest(1) + Expect(err).To(Succeed()) + + res := openapi.Resource{ + Manifest: manifestIn, + ConsumerName: &testConsumerName, + } + created, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesPost(ctx).Resource(res).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(*created.Id).ShouldNot(BeEmpty()) + resourceID = *created.Id + + got, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdGet(ctx, *created.Id).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(got).NotTo(BeNil()) + resourceVersion = got.Version + + // assert consumer name + Expect(*got.ConsumerName).To(Equal(*res.ConsumerName)) + + // assert manifest: job + manifestOut := got.Manifest + Expect(err).To(Succeed()) + Expect(reflect.DeepEqual(manifestIn, manifestOut)).To(BeTrue()) + }) + + It("patch resource", func() { + manifestIn, err := GetDeployManifest(2) + Expect(err).To(Succeed()) + + patchRequest := openapi.ResourcePatchRequest{ + Manifest: manifestIn, + Version: resourceVersion, + } + patched, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdPatch(ctx, resourceID).ResourcePatchRequest( + patchRequest).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + _, ok := patched.GetManifestOk() + Expect(ok).To(BeTrue()) + + got, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdGet(ctx, resourceID).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(got).NotTo(BeNil()) + + Expect(*got.ConsumerName).To(Equal(testConsumerName)) + Expect(reflect.DeepEqual(manifestIn, got.Manifest)).To(BeTrue()) + }) + + It("delete resource", func() { + resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdDelete(ctx, resourceID).Execute() + Expect(err).To(Succeed()) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + + Eventually(func() error { + resourceList, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesGet(ctx).Execute() + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("statusCode: want %d, but got %d", http.StatusOK, resp.StatusCode) + } + if resourceList == nil { + return errors.New("the resource List shouldn't be nil") + } + + got := false + for _, resource := range resourceList.Items { + metadata := resource.Manifest["metadata"].(map[string]interface{}) + if metadata["name"] == "nginx-api" { + got = true + } + } + if got { + return nil + } + return fmt.Errorf("the deploy should not be deleted from the consumer %s", testConsumerName) + }, 1*time.Minute, 1*time.Second).ShouldNot(HaveOccurred()) + }) + }) +}) + +func GetDeployManifest(replicas int) (map[string]interface{}, error) { + const deployTemplate = `{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "nginx-api", + "namespace": "default" + }, + "spec": { + "replicas": %d, + "selector": { + "matchLabels": { + "app": "nginx-api" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx-api" + } + }, + "spec": { + "containers": [ + { + "image": "nginxinc/nginx-unprivileged", + "name": "nginx-api" + } + ] + } + } + } + } + ` + manifest := map[string]interface{}{} + err := json.Unmarshal([]byte(fmt.Sprintf(deployTemplate, replicas)), &manifest) + return manifest, err +} diff --git a/test/e2e/pkg/resources_test.go b/test/e2e/pkg/resources_test.go index 7e400d6b..1873ba4a 100644 --- a/test/e2e/pkg/resources_test.go +++ b/test/e2e/pkg/resources_test.go @@ -13,14 +13,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// go test -v ./test/e2e/pkg -args -api-server=$api_server -consumer-name=$consumer_name -consumer-kubeconfig=$consumer_kubeconfig -ginkgo.focus "Resources" var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { - var resource *openapi.Resource Context("Resource CRUD Tests", func() { - It("post the nginx resource to the maestro api", func() { - res := helper.NewAPIResource(consumer_name, 1) + res := helper.NewAPIResource(consumerOpts.Name, 1) var resp *http.Response var err error resource, resp, err = apiClient.DefaultApi.ApiMaestroV1ResourcesPost(context.Background()).Resource(res).Execute() @@ -29,7 +28,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { Expect(*resource.Id).ShouldNot(BeEmpty()) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { return err } @@ -38,8 +37,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("patch the nginx resource", func() { - - newRes := helper.NewAPIResource(consumer_name, 2) + newRes := helper.NewAPIResource(consumerOpts.Name, 2) patchedResource, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdPatch(context.Background(), *resource.Id). ResourcePatchRequest(openapi.ResourcePatchRequest{Version: resource.Version, Manifest: newRes.Manifest}).Execute() Expect(err).ShouldNot(HaveOccurred()) @@ -47,7 +45,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { Expect(*patchedResource.Version).To(Equal(*resource.Version + 1)) Eventually(func() error { - deploy, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + deploy, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { return err } @@ -59,13 +57,12 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("delete the nginx resource", func() { - resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdDelete(context.Background(), *resource.Id).Execute() Expect(err).ShouldNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -78,7 +75,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) Context("Resource Delete Option Tests", func() { - res := helper.NewAPIResource(consumer_name, 1) + res := helper.NewAPIResource(consumerOpts.Name, 1) It("post the nginx resource to the maestro api", func() { var resp *http.Response var err error @@ -88,7 +85,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { Expect(*resource.Id).ShouldNot(BeEmpty()) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { return err } @@ -97,17 +94,14 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("patch the nginx resource with orphan delete option", func() { - patchedResource, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdPatch(context.Background(), *resource.Id). ResourcePatchRequest(openapi.ResourcePatchRequest{Version: resource.Version, Manifest: res.Manifest, DeleteOption: map[string]interface{}{"propagationPolicy": "Orphan"}}).Execute() Expect(err).ShouldNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusOK)) Expect(*patchedResource.Version).To(Equal(*resource.Version + 1)) - }) It("delete the nginx resource from the maestro api", func() { - resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdDelete(context.Background(), *resource.Id).Execute() Expect(err).ShouldNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) @@ -115,8 +109,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { retry := 0 Eventually(func() error { // Attempt to retrieve the "nginx" deployment in the "default" namespace - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) - + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) // If an error occurs if err != nil { // Return any other errors directly @@ -137,11 +130,11 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("delete the nginx deployment", func() { - err := kubeClient.AppsV1().Deployments("default").Delete(context.Background(), "nginx", metav1.DeleteOptions{}) + err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Delete(context.Background(), "nginx", metav1.DeleteOptions{}) Expect(err).ShouldNot(HaveOccurred()) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -151,13 +144,11 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { return fmt.Errorf("nginx deployment still exists") }, 1*time.Minute, 1*time.Second).ShouldNot(HaveOccurred()) }) - }) Context("Resource Update Strategy Tests", func() { - It("post the nginx resource to the maestro api with createOnly updateStrategy", func() { - res := helper.NewAPIResource(consumer_name, 1) + res := helper.NewAPIResource(consumerOpts.Name, 1) var resp *http.Response var err error res.UpdateStrategy = map[string]interface{}{"type": "CreateOnly"} @@ -167,7 +158,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { Expect(*resource.Id).ShouldNot(BeEmpty()) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { return err } @@ -176,8 +167,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("patch the nginx resource", func() { - - newRes := helper.NewAPIResource(consumer_name, 2) + newRes := helper.NewAPIResource(consumerOpts.Name, 2) patchedResource, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdPatch(context.Background(), *resource.Id). ResourcePatchRequest(openapi.ResourcePatchRequest{Version: resource.Version, Manifest: newRes.Manifest}).Execute() Expect(err).ShouldNot(HaveOccurred()) @@ -185,7 +175,7 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { Expect(*patchedResource.Version).To(Equal(*resource.Version + 1)) Eventually(func() error { - deploy, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + deploy, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { return err } @@ -197,13 +187,12 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }) It("delete the nginx resource", func() { - resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesIdDelete(context.Background(), *resource.Id).Execute() Expect(err).ShouldNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) Eventually(func() error { - _, err := kubeClient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) + _, err := consumerOpts.Kubeclient.AppsV1().Deployments("default").Get(context.Background(), "nginx", metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -214,5 +203,4 @@ var _ = Describe("Resources", Ordered, Label("e2e-tests-resources"), func() { }, 1*time.Minute, 1*time.Second).ShouldNot(HaveOccurred()) }) }) - }) diff --git a/test/e2e/pkg/serverside_test.go b/test/e2e/pkg/serverside_test.go index 61675c22..00bca4e6 100644 --- a/test/e2e/pkg/serverside_test.go +++ b/test/e2e/pkg/serverside_test.go @@ -59,7 +59,7 @@ var _ = Describe("Server Side Apply", func() { res := openapi.Resource{ Manifest: manifest, - ConsumerName: &consumer_name, + ConsumerName: &consumerOpts.Name, } created, resp, err := apiClient.DefaultApi.ApiMaestroV1ResourcesPost(context.Background()).Resource(res).Execute() @@ -108,6 +108,5 @@ var _ = Describe("Server Side Apply", func() { resp, err = apiClient.DefaultApi.ApiMaestroV1ResourcesIdDelete(context.Background(), resourceID).Execute() Expect(err).ShouldNot(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) - }) }) diff --git a/test/e2e/pkg/suite_test.go b/test/e2e/pkg/suite_test.go index 3872974a..0ff3c999 100644 --- a/test/e2e/pkg/suite_test.go +++ b/test/e2e/pkg/suite_test.go @@ -1,10 +1,11 @@ package e2e_test import ( + "context" "crypto/tls" "flag" - "fmt" "net/http" + "strings" "testing" "time" @@ -19,40 +20,32 @@ import ( ) var ( + consumerOpts *ConsumerOptions apiServerAddress string - kubeconfig string - consumer_name string - kubeClient *kubernetes.Clientset apiClient *openapi.APIClient helper *test.Helper - T *testing.T + ctx context.Context + cancel context.CancelFunc ) func TestE2E(t *testing.T) { + helper = &test.Helper{T: t} RegisterFailHandler(Fail) - T = t RunSpecs(t, "End-to-End Test Suite") } func init() { + consumerOpts = &ConsumerOptions{} klog.SetOutput(GinkgoWriter) flag.StringVar(&apiServerAddress, "api-server", "", "Maestro API server address") - flag.StringVar(&consumer_name, "consumer_name", "", "Consumer name is used to identify the consumer") - flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file") + flag.StringVar(&consumerOpts.Name, "consumer-name", "", "Consumer name is used to identify the consumer") + flag.StringVar(&consumerOpts.Kubeconfig, "consumer-kubeconfig", "", "Path to kubeconfig file") } var _ = BeforeSuite(func() { - // initialize the help - helper = &test.Helper{ - T: T, - } - // initialize the api client - tlsConfig := &tls.Config{ - //nolint:gosec - InsecureSkipVerify: true, - } - tr := &http.Transport{TLSClientConfig: tlsConfig} + ctx, cancel = context.WithCancel(context.Background()) + // initialize the api client cfg := &openapi.Configuration{ DefaultHeader: make(map[string]string), UserAgent: "OpenAPI-Generator/1.0.0/go", @@ -65,28 +58,32 @@ var _ = BeforeSuite(func() { }, OperationServers: map[string]openapi.ServerConfigurations{}, HTTPClient: &http.Client{ - Transport: tr, - Timeout: 10 * time.Second, + Transport: &http.Transport{TLSClientConfig: &tls.Config{ + //nolint:gosec + InsecureSkipVerify: true, + }}, + Timeout: 10 * time.Second, }, } apiClient = openapi.NewAPIClient(cfg) - // validate the kubeconfig file - restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) - if err != nil { - panic(fmt.Sprintf("failed to build kubeconfig: %v", err)) - } - kubeClient, err = kubernetes.NewForConfig(restConfig) - if err != nil { - panic(fmt.Sprintf("failed to create kube client: %v", err)) - } + // validate the consumer kubeconfig file + restConfig, err := clientcmd.BuildConfigFromFlags("", consumerOpts.Kubeconfig) + Expect(err).To(Succeed(), "failed to build consumer kubeconfig") + consumerOpts.Kubeclient, err = kubernetes.NewForConfig(restConfig) + Expect(err).To(Succeed(), "failed to create consumer kubeclient") - // validate the consumer_id - if consumer_name == "" { - panic("consumer_id is not provided") - } + // validate the consumer_name + consumerOpts.Name = strings.ReplaceAll(consumerOpts.Name, `"`, "") + Expect(consumerOpts.Name).NotTo(BeEmpty(), "consumer name is not provided") }) var _ = AfterSuite(func() { - // later... + cancel() }) + +type ConsumerOptions struct { + Name string + Kubeconfig string + Kubeclient *kubernetes.Clientset +}