From f71c248dcc42f0b1491541b84ba6fbc183c49c16 Mon Sep 17 00:00:00 2001 From: andoriyaprashant Date: Wed, 29 Oct 2025 17:21:03 +0530 Subject: [PATCH] E2E Signed-off-by: andoriyaprashant --- Makefile | 10 ++ e2e/serviceexport_test.go | 69 +++++++++++ e2e/serviceimport_test.go | 79 +++++++++++++ e2e/slice_test.go | 114 ++++++++++++++++++ e2e/slicegateway_test.go | 109 ++++++++++++++++++ e2e/suite_test.go | 237 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 618 insertions(+) create mode 100644 e2e/serviceexport_test.go create mode 100644 e2e/serviceimport_test.go create mode 100644 e2e/slice_test.go create mode 100644 e2e/slicegateway_test.go create mode 100644 e2e/suite_test.go diff --git a/Makefile b/Makefile index 5b9b9bb42..e9169a75d 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,16 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. go vet ./... +.PHONY: e2e-test +e2e-test: ## Run end-to-end tests located in the ./e2e directory. + @echo " Running E2E tests..." + @if ! command -v ginkgo &> /dev/null; then \ + echo " Installing Ginkgo..."; \ + go install github.com/onsi/ginkgo/v2/ginkgo@latest; \ + fi + @echo " Using Ginkgo binary at: $$(go env GOPATH)/bin/ginkgo" + $$(go env GOPATH)/bin/ginkgo run --v --randomize-all --fail-fast --timeout=15m ./e2e + .PHONY: test test: fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v -coverprofile=coverage.out -coverpkg ./... ./... diff --git a/e2e/serviceexport_test.go b/e2e/serviceexport_test.go new file mode 100644 index 000000000..0638cc21b --- /dev/null +++ b/e2e/serviceexport_test.go @@ -0,0 +1,69 @@ +package e2e + +import ( + "context" + "time" + + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("ServiceExport E2E", func() { + var ( + ctx context.Context + namespace string + sliceName string + ) + + BeforeEach(func() { + ctx = context.Background() + namespace = "test-namespace" + sliceName = "e2e-slice" + + // Create namespace if not exists + createNamespaceIfNotExists(ctx, namespace) + + }) + + It("should create a ServiceExport and update status correctly", func() { + // Deploy app pod matching ServiceExport selector + labels := map[string]string{"app": "test-app"} + createTestPod(ctx, namespace, "app-pod", labels) + + // Create slice first (required for ServiceExport to reconcile) + createSlice(ctx, sliceName) + + // Create ServiceExport object + se := &kubeslicev1beta1.ServiceExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-export", + Namespace: namespace, + }, + Spec: kubeslicev1beta1.ServiceExportSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Slice: sliceName, + Ports: []kubeslicev1beta1.ServicePort{ + { + Name: "http", + Protocol: "TCP", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, se)).To(Succeed()) + + Eventually(func(g Gomega) { + updated := &kubeslicev1beta1.ServiceExport{} + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: se.Name, Namespace: namespace}, updated)).To(Succeed()) + g.Expect(updated.Status.ExportStatus).To(Equal(kubeslicev1beta1.ExportStatusReady)) + g.Expect(updated.Status.AvailableEndpoints).To(BeNumerically(">", 0)) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + }) + +}) diff --git a/e2e/serviceimport_test.go b/e2e/serviceimport_test.go new file mode 100644 index 000000000..b79898659 --- /dev/null +++ b/e2e/serviceimport_test.go @@ -0,0 +1,79 @@ +package e2e + +import ( + "context" + "time" + + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("ServiceImport E2E", func() { + var ( + ctx context.Context + namespace string + sliceName string + importName string + ) + + BeforeEach(func() { + ctx = context.Background() + namespace = "test-namespace" + // sliceName = "test-slice" + sliceName = "e2e-slice" + importName = "test-serviceimport" + + createNamespaceIfNotExists(ctx, namespace) + + // createSlice(ctx, sliceName) + }) + + It("should create a ServiceImport and reconcile status correctly", func() { + // Deploy a test pod that will act as an endpoint + labels := map[string]string{"app": "test-app"} + createTestPod(ctx, namespace, "test-pod", labels) + + svcImport := &kubeslicev1beta1.ServiceImport{ + ObjectMeta: metav1.ObjectMeta{ + Name: importName, + Namespace: namespace, + }, + Spec: kubeslicev1beta1.ServiceImportSpec{ + Slice: sliceName, + DNSName: "test-app.slice.local", + Ports: []kubeslicev1beta1.ServicePort{ + { + Name: "http", + ContainerPort: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + Aliases: []string{"alias-test-app"}, + }, + } + + Expect(k8sClient.Create(ctx, svcImport)).To(Succeed()) + + Eventually(func(g Gomega) { + updated := &kubeslicev1beta1.ServiceImport{} + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: importName, Namespace: namespace}, updated)).To(Succeed()) + g.Expect(updated.Status.ImportStatus).To(Equal(kubeslicev1beta1.ImportStatusReady)) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + + // if ExposedPorts field is populated + Eventually(func(g Gomega) { + updated := &kubeslicev1beta1.ServiceImport{} + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: importName, Namespace: namespace}, updated)).To(Succeed()) + g.Expect(updated.Status.ExposedPorts).NotTo(BeEmpty()) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + + // Check AvailableEndpoints count (should be >= 0 — may depend on controller behavior) + updated := &kubeslicev1beta1.ServiceImport{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: importName, Namespace: namespace}, updated)).To(Succeed()) + Expect(updated.Status.AvailableEndpoints).To(BeNumerically(">=", 0)) + }) +}) diff --git a/e2e/slice_test.go b/e2e/slice_test.go new file mode 100644 index 000000000..eb49d1651 --- /dev/null +++ b/e2e/slice_test.go @@ -0,0 +1,114 @@ +package e2e + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" +) + +var _ = Describe("Slice E2E", func() { + var ( + ctx context.Context + namespace string + sliceName string + ) + + BeforeEach(func() { + ctx = context.Background() + namespace = "test-namespace" + // sliceName = "test-slice" + sliceName = "e2e-slice" + + // Create namespace if not exists + createNamespaceIfNotExists(ctx, namespace) + }) + + It("should create a Slice and reconcile its status", func() { + // Create Slice object + slice := &kubeslicev1beta1.Slice{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceName, + Namespace: namespace, + Labels: map[string]string{ + "kubeslice.io/origin": "hub", + }, + Annotations: map[string]string{ + "kubeslice.io/reconciled-from-hub": "true", + }, + }, + Spec: kubeslicev1beta1.SliceSpec{}, + } + Expect(k8sClient.Create(ctx, slice)).To(Succeed()) + + Eventually(func(g Gomega) { + updated := &kubeslicev1beta1.Slice{} + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: sliceName, Namespace: namespace}, updated)).To(Succeed()) + g.Expect(updated.Status.SliceConfig).NotTo(BeNil()) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + }) + + It("should update app pods in Slice status when pods are created", func() { + // Create the Slice first + slice := &kubeslicev1beta1.Slice{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceName, + Namespace: namespace, + Labels: map[string]string{ + "kubeslice.io/origin": "hub", + }, + Annotations: map[string]string{ + "kubeslice.io/reconciled-from-hub": "true", + }, + }, + Spec: kubeslicev1beta1.SliceSpec{}, + } + Expect(k8sClient.Create(ctx, slice)).To(Succeed()) + + // Create a test app pod with the application namespace selector label + labels := map[string]string{ + "app": "test-app", + "kubeslice.io/application-namespace": sliceName, + } + + createTestPod(ctx, namespace, "app-pod", labels) + + Eventually(func(g Gomega) { + updated := &kubeslicev1beta1.Slice{} + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: sliceName, Namespace: namespace}, updated)).To(Succeed()) + g.Expect(len(updated.Status.AppPods)).To(BeNumerically(">", 0)) + g.Expect(updated.Status.AppPods[0].PodName).To(Equal("app-pod")) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + }) + + It("should delete Slice successfully", func() { + // Create Slice + slice := &kubeslicev1beta1.Slice{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceName, + Namespace: namespace, + Labels: map[string]string{ + "kubeslice.io/origin": "hub", + }, + Annotations: map[string]string{ + "kubeslice.io/reconciled-from-hub": "true", + }, + }, + Spec: kubeslicev1beta1.SliceSpec{}, + } + Expect(k8sClient.Create(ctx, slice)).To(Succeed()) + + Expect(k8sClient.Delete(ctx, slice)).To(Succeed()) + + // Verify Slice is deleted + Eventually(func() error { + s := &kubeslicev1beta1.Slice{} + return k8sClient.Get(ctx, client.ObjectKey{Name: sliceName, Namespace: namespace}, s) + }).WithTimeout(1 * time.Minute).WithPolling(2 * time.Second).ShouldNot(Succeed()) + }) +}) diff --git a/e2e/slicegateway_test.go b/e2e/slicegateway_test.go new file mode 100644 index 000000000..e56e2eb0e --- /dev/null +++ b/e2e/slicegateway_test.go @@ -0,0 +1,109 @@ +package e2e + +import ( + "context" + "time" + + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("SliceGateway E2E", Ordered, func() { + var ( + ctx context.Context + sliceGwName string + sliceGwNamespace string + sliceName string + siteName string + ) + + BeforeAll(func() { + ctx = context.Background() + sliceGwName = "e2e-slicegateway" + sliceGwNamespace = "kubeslice-system" // control plane namespace + sliceName = "e2e-slice" + siteName = "e2e-site" + + // Create the namespace so the test doesn't fail + createNamespaceIfNotExists(ctx, sliceGwNamespace) + }) + + AfterAll(func() { + // Cleanup resources at the end of test + gw := &kubeslicev1beta1.SliceGateway{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: sliceGwName, Namespace: sliceGwNamespace}, gw) + if err == nil { + _ = k8sClient.Delete(ctx, gw) + } + }) + + It("should create a SliceGateway CR successfully", func() { + gw := &kubeslicev1beta1.SliceGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceGwName, + Namespace: sliceGwNamespace, + }, + Spec: kubeslicev1beta1.SliceGatewaySpec{ + SliceName: sliceName, + SiteName: siteName, + }, + } + + By("creating the SliceGateway resource") + Expect(k8sClient.Create(ctx, gw)).To(Succeed()) + + By("verifying the resource exists") + created := &kubeslicev1beta1.SliceGateway{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: sliceGwName, Namespace: sliceGwNamespace}, created) + }, 60*time.Second, 5*time.Second).Should(Succeed()) + + Expect(created.Spec.SliceName).To(Equal(sliceName)) + Expect(created.Spec.SiteName).To(Equal(siteName)) + }) + + It("should update SliceGateway status when reconciled", func() { + By("waiting for controller to reconcile") + Eventually(func() string { + gw := &kubeslicev1beta1.SliceGateway{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: sliceGwName, Namespace: sliceGwNamespace}, gw); err != nil { + return "" + } + return gw.Status.Config.SliceGatewayStatus + }, 2*time.Minute, 5*time.Second).ShouldNot(BeEmpty()) + }) + + It("should have gateway pod created by controller", func() { + By("verifying a gateway pod exists") + Eventually(func() int { + podList := &corev1.PodList{} + err := k8sClient.List(ctx, podList, client.InNamespace(sliceGwNamespace), + client.MatchingLabels(map[string]string{ + "networking.kubeslice.io/slicegateway": sliceGwName, + })) + if err != nil { + return 0 + } + return len(podList.Items) + }, 2*time.Minute, 10*time.Second).Should(BeNumerically(">", 0)) + }) + + It("should delete SliceGateway successfully", func() { + By("deleting the SliceGateway") + gw := &kubeslicev1beta1.SliceGateway{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: sliceGwName, Namespace: sliceGwNamespace}, gw)).To(Succeed()) + Expect(k8sClient.Delete(ctx, gw)).To(Succeed()) + + By("verifying the resource is deleted") + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: sliceGwName, Namespace: sliceGwNamespace}, gw) + return err != nil + }, 60*time.Second, 5*time.Second).Should(BeTrue()) + }) +}) diff --git a/e2e/suite_test.go b/e2e/suite_test.go new file mode 100644 index 000000000..487e65632 --- /dev/null +++ b/e2e/suite_test.go @@ -0,0 +1,237 @@ +package e2e + +import ( + "context" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + monitoringEvents "github.com/kubeslice/kubeslice-monitoring/pkg/events" + ossEvents "github.com/kubeslice/worker-operator/events" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + controllerv1alpha1 "github.com/kubeslice/apis/pkg/controller/v1alpha1" + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + "github.com/kubeslice/worker-operator/controllers" + slicecontroller "github.com/kubeslice/worker-operator/controllers/slice" + slicegatewaycontroller "github.com/kubeslice/worker-operator/controllers/slicegateway" +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + scheme = runtime.NewScheme() + mgrCtx context.Context + mgrCancel context.CancelFunc +) + +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Worker Operator E2E Test Suite") +} + +var _ = BeforeSuite(func() { + log.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "config", "crd", "bases"), + }, + ErrorIfCRDPathMissing: true, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) + Expect(kubeslicev1beta1.AddToScheme(scheme)).To(Succeed()) + Expect(controllerv1alpha1.AddToScheme(scheme)).To(Succeed()) + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + k8sClient = mgr.GetClient() + + ctx := context.Background() + createNamespaceIfNotExists(ctx, controllers.ControlPlaneNamespace) + createDNSSvc(ctx) + createWorkerConfigMap(ctx) + createSliceConfigSecret(ctx) + + // Use monitoringEvents.NewEventRecorder (matches main.go) + recorder := monitoringEvents.NewEventRecorder( + mgr.GetClient(), + scheme, + ossEvents.EventsMap, + monitoringEvents.EventRecorderOptions{ + Version: "v1", + Slice: "test-slice", + Cluster: "test-cluster", + Project: "test-project", + Component: "slice-controller", + Namespace: controllers.ControlPlaneNamespace, + }, + ) + + // Pass recorder to reconciler (as pointer) + err = (&slicecontroller.SliceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: &recorder, + }).SetupWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&slicegatewaycontroller.SliceGwReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: &recorder, + }).SetupWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + mgrCtx, mgrCancel = context.WithCancel(context.Background()) + go func() { + defer GinkgoRecover() + err = mgr.Start(mgrCtx) + Expect(err).NotTo(HaveOccurred()) + }() +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + if mgrCancel != nil { + mgrCancel() + time.Sleep(200 * time.Millisecond) + } + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// +// Helper functions +// + +func createWorkerConfigMap(ctx context.Context) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeslice-worker-config", + Namespace: controllers.ControlPlaneNamespace, + }, + Data: map[string]string{ + "cluster_name": "test-cluster", + "project_name": "test-project", + }, + } + _ = k8sClient.Create(ctx, cm) +} + +func createSliceConfigSecret(ctx context.Context) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeslice-worker-secret", + Namespace: controllers.ControlPlaneNamespace, + }, + Data: map[string][]byte{ + "ca.crt": []byte("fake-ca"), + "tls.crt": []byte("fake-cert"), + "tls.key": []byte("fake-key"), + }, + } + _ = k8sClient.Create(ctx, secret) +} + +func createNamespaceIfNotExists(ctx context.Context, name string) { + ns := &corev1.Namespace{} + err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, ns) + if err == nil { + return + } + + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + Expect(k8sClient.Create(ctx, ns)).To(Succeed()) +} + +func createTestPod(ctx context.Context, namespace, podName string, labels map[string]string) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: namespace, + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx:latest", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, pod)).To(Succeed()) +} + +func createSlice(ctx context.Context, sliceName string) { + slice := &kubeslicev1beta1.Slice{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceName, + Namespace: controllers.ControlPlaneNamespace, + Labels: map[string]string{ + "kubeslice.io/origin": "hub", + }, + Annotations: map[string]string{ + "kubeslice.io/reconciled-from-hub": "true", + }, + }, + Spec: kubeslicev1beta1.SliceSpec{}, + Status: kubeslicev1beta1.SliceStatus{ + SliceConfig: &kubeslicev1beta1.SliceConfig{ + SliceID: "test-id", + SliceType: "test-type", + SliceDisplayName: "test-display", + SliceOverlayNetworkDeploymentMode: controllerv1alpha1.NONET, + }, + }, + } + + Expect(k8sClient.Create(ctx, slice)).To(Succeed()) +} + +func createDNSSvc(ctx context.Context) { + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllers.DNSDeploymentName, + Namespace: controllers.ControlPlaneNamespace, + }, + Spec: corev1.ServiceSpec{ + ClusterIP: "10.0.0.10", + Ports: []corev1.ServicePort{ + { + Name: "dns", + Protocol: corev1.ProtocolUDP, + Port: 53, + }, + }, + }, + } + _ = k8sClient.Create(ctx, svc) +}