From 8997ff7022eebc00c1f28d149ed8e0e5276fb00d Mon Sep 17 00:00:00 2001 From: Gabe Alford Date: Fri, 13 Oct 2023 15:19:25 -0600 Subject: [PATCH] feat: Add admission controller test suite - Fix typos in sidecar test suite --- .../falconadmission_controller_test.go | 172 ++++++++++++++++++ controllers/{ => admission}/suite_test.go | 4 +- .../falconcontainer_controller_test.go | 2 +- controllers/falcon_container/suite_test.go | 1 - test/e2e/e2e_test.go | 105 ++++++++++- 5 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 controllers/admission/falconadmission_controller_test.go rename controllers/{ => admission}/suite_test.go (91%) diff --git a/controllers/admission/falconadmission_controller_test.go b/controllers/admission/falconadmission_controller_test.go new file mode 100644 index 00000000..101b766e --- /dev/null +++ b/controllers/admission/falconadmission_controller_test.go @@ -0,0 +1,172 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + k8sutils "github.com/crowdstrike/falcon-operator/internal/controller/common" + "github.com/crowdstrike/falcon-operator/pkg/common" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("FalconAdmission controller", func() { + Context("FalconAdmission controller test", func() { + + const AdmissionControllerName = "test-falconadmissioncontroller" + const AdmissionControllerNamespace = "falcon-kac" + admissionImage := "example.com/image:test" + falconCID := "1234567890ABCDEF1234567890ABCDEF-12" + + ctx := context.Background() + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: AdmissionControllerNamespace, + Namespace: AdmissionControllerNamespace, + }, + } + + typeNamespaceName := types.NamespacedName{Name: AdmissionControllerName, Namespace: AdmissionControllerNamespace} + + BeforeEach(func() { + By("Creating the Namespace to perform the tests") + err := k8sClient.Create(ctx, namespace) + Expect(err).To(Not(HaveOccurred())) + }) + + AfterEach(func() { + // TODO(user): Attention if you improve this code by adding other context test you MUST + // be aware of the current delete namespace limitations. More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations + By("Deleting the Namespace to perform the tests") + _ = k8sClient.Delete(ctx, namespace) + }) + + It("should successfully reconcile a custom resource for FalconAdmission", func() { + By("Creating the custom resource for the Kind FalconAdmission") + falconAdmission := &falconv1alpha1.FalconAdmission{} + err := k8sClient.Get(ctx, typeNamespaceName, falconAdmission) + if err != nil && errors.IsNotFound(err) { + // Let's mock our custom resource at the same way that we would + // apply on the cluster the manifest under config/samples + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: AdmissionControllerName, + Namespace: AdmissionControllerNamespace, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: "falcon-kac", + Image: admissionImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + DepUpdateStrategy: falconv1alpha1.FalconAdmissionUpdateStrategy{ + RollingUpdate: appsv1.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{IntVal: 1}, + MaxSurge: &intstr.IntOrString{IntVal: 1}, + }, + }, + }, + }, + } + + err = k8sClient.Create(ctx, falconAdmission) + Expect(err).To(Not(HaveOccurred())) + } + + By("Checking if the custom resource was successfully created") + Eventually(func() error { + found := &falconv1alpha1.FalconAdmission{} + return k8sClient.Get(ctx, typeNamespaceName, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Reconciling the custom resource created") + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err = falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespaceName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Checking if Service Account was successfully created in the reconciliation") + Eventually(func() error { + found := &corev1.ServiceAccount{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "falcon-operator-admission-controller", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if ResourceQuota was successfully created in the reconciliation") + Eventually(func() error { + found := &corev1.ResourceQuota{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "test-falconadmissioncontroller", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if TLS Secret was successfully created in the reconciliation") + Eventually(func() error { + found := &corev1.Secret{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "test-falconadmissioncontroller-tls", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if ConfigMap was successfully created in the reconciliation") + Eventually(func() error { + found := &corev1.ConfigMap{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "test-falconadmissioncontroller-config", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if Service was successfully created in the reconciliation") + Eventually(func() error { + found := &corev1.Service{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "test-falconadmissioncontroller", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if Deployment was successfully created in the reconciliation") + Eventually(func() error { + found := &appsv1.Deployment{} + return k8sClient.Get(ctx, types.NamespacedName{Name: "test-falconadmissioncontroller", Namespace: AdmissionControllerNamespace}, found) + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking if pods were successfully created in the reconciliation") + Eventually(func() error { + pod, err := k8sutils.GetReadyPod(k8sClient, ctx, AdmissionControllerNamespace, map[string]string{common.FalconComponentKey: common.FalconAdmissionController}) + if err != nil && err.Error() != "No webhook service pod found in a Ready state" { + return err + } + if pod.Name == "" { + _, err = falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespaceName, + }) + } + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("Checking the latest Status Condition added to the FalconAdmission instance") + Eventually(func() error { + if falconAdmission.Status.Conditions != nil && len(falconAdmission.Status.Conditions) != 0 { + latestStatusCondition := falconAdmission.Status.Conditions[len(falconAdmission.Status.Conditions)-1] + expectedLatestStatusCondition := metav1.Condition{Type: falconv1alpha1.ConditionDeploymentReady, + Status: metav1.ConditionTrue, Reason: falconv1alpha1.ReasonInstallSucceeded, + Message: "FalconAdmission installation completed"} + if latestStatusCondition != expectedLatestStatusCondition { + return fmt.Errorf("The latest status condition added to the FalconAdmission instance is not as expected") + } + } + return nil + }, time.Minute, time.Second).Should(Succeed()) + }) + }) +}) diff --git a/controllers/suite_test.go b/controllers/admission/suite_test.go similarity index 91% rename from controllers/suite_test.go rename to controllers/admission/suite_test.go index 2c1d31f3..de6bf9b1 100644 --- a/controllers/suite_test.go +++ b/controllers/admission/suite_test.go @@ -28,7 +28,7 @@ var testEnv *envtest.Environment func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Controller Suite") + RunSpecs(t, "Admission Controller Controller Suite") } var _ = BeforeSuite(func() { @@ -36,7 +36,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } diff --git a/controllers/falcon_container/falconcontainer_controller_test.go b/controllers/falcon_container/falconcontainer_controller_test.go index 3756d640..c1be0322 100644 --- a/controllers/falcon_container/falconcontainer_controller_test.go +++ b/controllers/falcon_container/falconcontainer_controller_test.go @@ -149,7 +149,7 @@ var _ = Describe("FalconContainer controller", func() { By("Checking if pods were successfully created in the reconciliation") Eventually(func() error { pod, err := k8sutils.GetReadyPod(k8sClient, ctx, SidecarSensorNamespace, map[string]string{common.FalconComponentKey: common.FalconSidecarSensor}) - if err != nil && err.Error() != "No Injector pod found in a Ready state" { + if err != nil && err.Error() != "No webhook service pod found in a Ready state" { return err } if pod.Name == "" { diff --git a/controllers/falcon_container/suite_test.go b/controllers/falcon_container/suite_test.go index 9cdc75b6..a2906ef5 100644 --- a/controllers/falcon_container/suite_test.go +++ b/controllers/falcon_container/suite_test.go @@ -41,7 +41,6 @@ var _ = BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - WebhookInstallOptions: envtest.WebhookInstallOptions{}, } var err error diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 97927f85..10d0db4b 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -28,9 +28,10 @@ var _ = Describe("falcon", Ordered, func() { BeforeAll(func() { // The namespace can be created when we run make install // However, in this test we want ensure that the solution - // can run in a ns labeled as restricted. Therefore, we are + // can run in a ns labeled as privileged. Therefore, we are // creating the namespace an lebeling it. By("creating manager namespace") + cmd := exec.Command("kubectl", "create", "ns", namespace) _, _ = utils.Run(cmd) @@ -83,8 +84,7 @@ var _ = Describe("falcon", Ordered, func() { outputMake, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - fmt.Println(outputMake) - By("validating that manager Pod/container(s) are restricted") + By("validating that manager Pod/container(s) are not restricted") ExpectWithOffset(1, outputMake).NotTo(ContainSubstring("Warning: would violate PodSecurity")) By("validating that the controller-manager pod is running as expected") @@ -308,11 +308,108 @@ var _ = Describe("falcon", Ordered, func() { fmt.Println(string(status)) ExpectWithOffset(2, err).NotTo(HaveOccurred()) if len(status) > 0 { - return fmt.Errorf("falcon-node-sensor pod in %s status", status) + return fmt.Errorf("falcon-sidecar-sensor pod in %s status", status) } return nil } EventuallyWithOffset(1, getFalconNodeSensorPodStatus, time.Minute, time.Second).Should(Succeed()) }) }) + + Context("Falcon Admission Controller", func() { + It("should deploy successfully", func() { + projectDir, _ := utils.GetProjectDir() + + var falconClientID = "" + var falconClientSecret = "" + if clientID, ok := os.LookupEnv("FALCON_CLIENT_ID"); ok { + falconClientID = clientID + } + + if clientSecret, ok := os.LookupEnv("FALCON_CLIENT_SECRET"); ok { + falconClientSecret = clientSecret + } + + if falconClientID != "" && falconClientSecret != "" { + err := utils.ReplaceInFile(filepath.Join(projectDir, + "./config/samples/falcon_v1alpha1_falconadmission.yaml"), + "client_id: PLEASE_FILL_IN", fmt.Sprintf("client_id: %s", falconClientID)) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + err = utils.ReplaceInFile(filepath.Join(projectDir, + "./config/samples/falcon_v1alpha1_falconadmission.yaml"), + "client_secret: PLEASE_FILL_IN", fmt.Sprintf("client_secret: %s", falconClientSecret)) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("creating an instance of the FalconAdmission Operand(CR)") + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "apply", "-f", filepath.Join(projectDir, + "./config/samples/falcon_v1alpha1_falconadmission.yaml"), "-n", namespace) + _, err := utils.Run(cmd) + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("validating that pod(s) status.phase=Running") + getFalconSidecarPodStatus := func() error { + cmd := exec.Command("kubectl", "get", + "pods", "-A", "-l", "crowdstrike.com/component=admission_controller", + "-o", "jsonpath={.items[*].status}", "-n", namespace, + ) + status, err := utils.Run(cmd) + fmt.Println(string(status)) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(status), "\"phase\":\"Running\"") { + return fmt.Errorf(" pod in %s status", status) + } + return nil + } + EventuallyWithOffset(1, getFalconSidecarPodStatus, time.Minute, time.Second).Should(Succeed()) + + By("validating that the status of the custom resource created is updated or not") + getStatus := func() error { + cmd := exec.Command("kubectl", "get", "falconadmission", + "falcon-admission", "-A", "-o", "jsonpath={.status.conditions}", + "-n", namespace, + ) + status, err := utils.Run(cmd) + fmt.Println(string(status)) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(status), "Success") { + return fmt.Errorf("status condition with type Success should be set") + } + return nil + } + Eventually(getStatus, time.Minute, time.Second).Should(Succeed()) + }) + }) + + Context("Falcon Admission Controller", func() { + It("should cleanup successfully", func() { + projectDir, _ := utils.GetProjectDir() + + By("deleting an instance of the FalconAdmission Operand(CR)") + EventuallyWithOffset(1, func() error { + cmd := exec.Command("kubectl", "delete", "-f", filepath.Join(projectDir, + "./config/samples/falcon_v1alpha1_falconadmission.yaml"), "-n", namespace) + _, err := utils.Run(cmd) + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("validating that pod(s) status.phase!=Running") + getFalconAdmissionPodStatus := func() error { + cmd := exec.Command("kubectl", "get", + "pods", "-A", "-l", "crowdstrike.com/component=admission_controller", "--field-selector=status.phase=Running", + "-o", "jsonpath={.items[*].status}", "-n", namespace, + ) + status, err := utils.Run(cmd) + fmt.Println(string(status)) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if len(status) > 0 { + return fmt.Errorf("falcon-admission pod in %s status", status) + } + return nil + } + EventuallyWithOffset(1, getFalconAdmissionPodStatus, time.Minute, time.Second).Should(Succeed()) + }) + }) })