From a2e042c5e4ad6c833aa321cce0fbf91467e577b9 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Wed, 27 May 2026 16:48:18 -0500 Subject: [PATCH 01/10] feat: Add tolerations to KAC and IAR --- api/falcon/v1alpha1/falconadmission_types.go | 5 + .../v1alpha1/falconimageanalyzer_types.go | 5 + api/falcon/v1alpha1/zz_generated.deepcopy.go | 14 + ...lcon.crowdstrike.com_falconadmissions.yaml | 42 +++ ...con.crowdstrike.com_falcondeployments.yaml | 83 ++++++ ....crowdstrike.com_falconimageanalyzers.yaml | 41 +++ deploy/falcon-operator.yaml | 166 +++++++++++ .../admission/falconadmission_controller.go | 35 +++ .../falconadmission_controller_test.go | 265 ++++++++++++++++++ internal/controller/admission/rbac.go | 7 +- internal/controller/assets/deployment.go | 2 + .../falconimage_controller.go | 138 ++++++--- .../falconimage_controller_test.go | 251 +++++++++++++++++ test/e2e/cr_config_test.go | 2 +- test/e2e/e2e_common_test.go | 57 ++++ test/e2e/e2e_test.go | 191 ++++++++++++- 16 files changed, 1258 insertions(+), 46 deletions(-) diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index 654f237d..70c3fd3a 100644 --- a/api/falcon/v1alpha1/falconadmission_types.go +++ b/api/falcon/v1alpha1/falconadmission_types.go @@ -205,6 +205,11 @@ type FalconAdmissionConfigSpec struct { // Specifies node affinity for scheduling the Admission Controller. // +operator-sdk:csv:customresourcedefinitions:type=spec,order=19 NodeAffinity *corev1.NodeAffinity `json:"nodeAffinity,omitempty"` + + // Specifies tolerations for scheduling the Admission Controller. + // +kubebuilder:default:={} + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=19 + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } type FalconAdmissionServiceAccount struct { diff --git a/api/falcon/v1alpha1/falconimageanalyzer_types.go b/api/falcon/v1alpha1/falconimageanalyzer_types.go index e82cc9be..d259fef5 100644 --- a/api/falcon/v1alpha1/falconimageanalyzer_types.go +++ b/api/falcon/v1alpha1/falconimageanalyzer_types.go @@ -58,6 +58,11 @@ type FalconImageAnalyzerSpec struct { // Specifies node affinity for scheduling the Sensor. // +operator-sdk:csv:customresourcedefinitions:type=spec,order=8 NodeAffinity *corev1.NodeAffinity `json:"nodeAffinity,omitempty"` + + // Specifies tolerations for scheduling the Sensor. + // +kubebuilder:default:={} + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=9 + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } type FalconImageAnalyzerConfigSpec struct { diff --git a/api/falcon/v1alpha1/zz_generated.deepcopy.go b/api/falcon/v1alpha1/zz_generated.deepcopy.go index d11d124a..aea42a74 100644 --- a/api/falcon/v1alpha1/zz_generated.deepcopy.go +++ b/api/falcon/v1alpha1/zz_generated.deepcopy.go @@ -424,6 +424,13 @@ func (in *FalconAdmissionConfigSpec) DeepCopyInto(out *FalconAdmissionConfigSpec *out = new(corev1.NodeAffinity) (*in).DeepCopyInto(*out) } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconAdmissionConfigSpec. @@ -1208,6 +1215,13 @@ func (in *FalconImageAnalyzerSpec) DeepCopyInto(out *FalconImageAnalyzerSpec) { *out = new(corev1.NodeAffinity) (*in).DeepCopyInto(*out) } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconImageAnalyzerSpec. diff --git a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml index 3c6c4ff2..594d4290 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml @@ -643,6 +643,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: diff --git a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml index ed934b4b..38290ffc 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -699,6 +699,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: @@ -4028,6 +4070,47 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: diff --git a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml index 3e3bccd1..7f4eb1d0 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -611,6 +611,47 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: 6.31, 6.31.0, diff --git a/deploy/falcon-operator.yaml b/deploy/falcon-operator.yaml index 88e6dd41..e5117ac7 100644 --- a/deploy/falcon-operator.yaml +++ b/deploy/falcon-operator.yaml @@ -657,6 +657,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: @@ -4286,6 +4328,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: @@ -7607,6 +7691,47 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: @@ -8995,6 +9120,47 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: 6.31, 6.31.0, diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index 3138ca01..ec78380b 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -340,6 +340,8 @@ func (r *FalconAdmissionReconciler) reconcileResourceQuota(ctx context.Context, podLimit := resource.MustParse(defaultPodLimit) if existingRQ.Spec.Hard["pods"] != podLimit { + // Set GVK on rq since it's not populated when retrieved from the API server + rq.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ResourceQuota")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, rq) if err != nil { return err @@ -446,6 +448,8 @@ func (r *FalconAdmissionReconciler) reconcileService(ctx context.Context, req ct if !reflect.DeepEqual(service.Spec.Ports, existingService.Spec.Ports) { existingService.Spec.Ports = service.Spec.Ports + // Set GVK on existingService since it's not populated when retrieved from the API server + existingService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) if err := k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingService); err != nil { return false, err } @@ -523,6 +527,8 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionValidatingWebHook(ctx cont if updated { existingWebhook.Webhooks = webhook.Webhooks + // Set GVK on existingWebhook since it's not populated when retrieved from the API server + existingWebhook.SetGroupVersionKind(arv1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration")) if err := k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingWebhook); err != nil { return false, err } @@ -716,6 +722,29 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con } } + mergedTolerations := dep.Spec.Template.Spec.Tolerations + for _, existingTol := range existingDeployment.Spec.Template.Spec.Tolerations { + found := false + for _, specTol := range dep.Spec.Template.Spec.Tolerations { + if reflect.DeepEqual(existingTol, specTol) { + found = true + break + } + } + + if !found { + mergedTolerations = append(mergedTolerations, existingTol) + } + } + + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { + log.V(1).Info("Updating FalconAdmission Deployment: Tolerations changed", + "old", existingDeployment.Spec.Template.Spec.Tolerations, + "new", mergedTolerations) + existingDeployment.Spec.Template.Spec.Tolerations = mergedTolerations + updated = true + } + if updated { existingDeployment.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) if err := k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingDeployment); err != nil { @@ -763,6 +792,8 @@ func (r *FalconAdmissionReconciler) reconcileRegistrySecret(ctx context.Context, } if !reflect.DeepEqual(secret.Data, existingSecret.Data) { + // Set GVK on existingSecret since it's not populated when retrieved from the API server + existingSecret.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingSecret) if err != nil { return err @@ -793,6 +824,8 @@ func (r *FalconAdmissionReconciler) reconcileImageStream(ctx context.Context, re if !reflect.DeepEqual(imageStream.Spec, existingImageStream.Spec) { existingImageStream.Spec = imageStream.Spec + // Set GVK on existingImageStream since it's not populated when retrieved from the API server + existingImageStream.SetGroupVersionKind(imagev1.SchemeGroupVersion.WithKind("ImageStream")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingImageStream) if err != nil { return existingImageStream, err @@ -847,6 +880,8 @@ func (r *FalconAdmissionReconciler) admissionDeploymentUpdate(ctx context.Contex } log.Info("Rolling FalconAdmission Deployment due to non-deployment configuration change") + // Set GVK on existingDeployment since it's not populated when retrieved from the API server + existingDeployment.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) if err := k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingDeployment); err != nil { return err } diff --git a/internal/controller/admission/falconadmission_controller_test.go b/internal/controller/admission/falconadmission_controller_test.go index 6dae8e3d..84084160 100644 --- a/internal/controller/admission/falconadmission_controller_test.go +++ b/internal/controller/admission/falconadmission_controller_test.go @@ -1225,5 +1225,270 @@ var _ = Describe("FalconAdmission controller", func() { Expect(configMap.Data["__CS_SNAPSHOTS_ENABLED"]).To(Equal("false"), "SnapshotsEnabled=false should be respected independently") Expect(configMap.Data["__CS_VISIBILITY_CONFIGMAPS_ENABLED"]).To(Equal("false"), "ConfigMapWatcherEnabled=false should be respected independently") }) + + It("should apply tolerations from spec to deployment", func() { + ctx := context.Background() + + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerName, + Namespace: namespaceName, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: namespaceName, + Image: "crowdstrike/falcon-kac:test", + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + Tolerations: []corev1.Toleration{ + { + Key: "disk-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, falconAdmission)).To(Succeed()) + + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err := falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + }, 5*time.Second, time.Second).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("disk-pressure")) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Operator).To(Equal(corev1.TolerationOpExists)) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Effect).To(Equal(corev1.TaintEffectNoSchedule)) + }) + + It("should preserve system-added tolerations while applying spec tolerations", func() { + ctx := context.Background() + + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerName, + Namespace: namespaceName, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: namespaceName, + Image: "crowdstrike/falcon-kac:test", + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + Tolerations: []corev1.Toleration{ + { + Key: "custom-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, falconAdmission)).To(Succeed()) + + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err := falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying initial toleration is applied") + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + }, 5*time.Second, time.Second).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("custom-taint")) + + By("Simulating a system like OpenShift adding a toleration") + deployment.Spec.Template.Spec.Tolerations = append(deployment.Spec.Template.Spec.Tolerations, corev1.Toleration{ + Key: "node.kubernetes.io/not-ready", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: func() *int64 { i := int64(300); return &i }(), + }) + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + By("Verifying system toleration was added") + Eventually(func() int { + dep := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, dep) + return len(dep.Spec.Template.Spec.Tolerations) + }, 5*time.Second, time.Second).Should(Equal(2)) + + By("Reconciling again to ensure system toleration is preserved") + _, err = falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both tolerations exist after reconciliation") + deployment = &appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + if err != nil { + return false + } + if len(deployment.Spec.Template.Spec.Tolerations) != 2 { + return false + } + hasCustom := false + hasSystem := false + for _, tol := range deployment.Spec.Template.Spec.Tolerations { + if tol.Key == "custom-taint" { + hasCustom = true + } + if tol.Key == "node.kubernetes.io/not-ready" { + hasSystem = true + } + } + return hasCustom && hasSystem + }, 5*time.Second, time.Second).Should(BeTrue()) + }) + + It("should update tolerations when spec changes", func() { + ctx := context.Background() + + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerName, + Namespace: namespaceName, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: namespaceName, + Image: "crowdstrike/falcon-kac:test", + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + Tolerations: []corev1.Toleration{ + { + Key: "initial-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, falconAdmission)).To(Succeed()) + + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err := falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying initial toleration is applied") + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + }, 5*time.Second, time.Second).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("initial-taint")) + + By("Waiting for deployment to be older than 5 seconds to allow reconcile updates") + time.Sleep(6 * time.Second) + + By("Updating FalconAdmission spec with new toleration") + Eventually(func() error { + if err := k8sClient.Get(ctx, admissionNamespacedName, falconAdmission); err != nil { + return err + } + falconAdmission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "updated-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + } + return k8sClient.Update(ctx, falconAdmission) + }, 5*time.Second, time.Second).Should(Succeed()) + + By("Reconciling with updated spec") + _, err = falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both old and new tolerations exist (merge preserves old as potential system toleration)") + deployment = &appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + if err != nil { + return false + } + + if len(deployment.Spec.Template.Spec.Tolerations) < 1 { + return false + } + + hasUpdated := false + for _, tol := range deployment.Spec.Template.Spec.Tolerations { + if tol.Key == "updated-taint" { + hasUpdated = true + } + } + return hasUpdated + }, 5*time.Second, time.Second).Should(BeTrue()) + }) }) }) diff --git a/internal/controller/admission/rbac.go b/internal/controller/admission/rbac.go index e7246714..590e125f 100644 --- a/internal/controller/admission/rbac.go +++ b/internal/controller/admission/rbac.go @@ -57,7 +57,6 @@ func (r *FalconAdmissionReconciler) reconcileServiceAccount(ctx context.Context, return serviceAccountUpdated, err } - // Set GVK on existingServiceAccount since it's not populated when retrieved from the API server existingServiceAccount.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) // Check if any annotations from serviceAccount need to be added to existingServiceAccount @@ -159,6 +158,8 @@ func (r *FalconAdmissionReconciler) reconcileClusterRoleBinding(ctx context.Cont // If RoleRef is the same but Subjects have changed, update the object and post to k8s api } else if !reflect.DeepEqual(clusterRoleBinding.Subjects, existingClusterRoleBinding.Subjects) { existingClusterRoleBinding.Subjects = clusterRoleBinding.Subjects + // Set GVK on existingClusterRoleBinding since it's not populated when retrieved from the API server + existingClusterRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingClusterRoleBinding) if err != nil { return err @@ -187,6 +188,8 @@ func (r *FalconAdmissionReconciler) reconcileRole(ctx context.Context, req ctrl. if !reflect.DeepEqual(role.Rules, existingRole.Rules) { existingRole.Rules = role.Rules + // Set GVK on existingRole since it's not populated when retrieved from the API server + existingRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingRole) if err != nil { return err @@ -229,6 +232,8 @@ func (r *FalconAdmissionReconciler) reconcileRoleBinding(ctx context.Context, re // If RoleRef is the same but Subjects have changed, update the object and post to k8s api } else if !reflect.DeepEqual(roleBinding.Subjects, existingRoleBinding.Subjects) { existingRoleBinding.Subjects = roleBinding.Subjects + // Set GVK on existingRoleBinding since it's not populated when retrieved from the API server + existingRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) err = k8sutils.Update(r.Client, ctx, req, log, falconAdmission, &falconAdmission.Status, existingRoleBinding) if err != nil { return err diff --git a/internal/controller/assets/deployment.go b/internal/controller/assets/deployment.go index a484b353..b164f51d 100644 --- a/internal/controller/assets/deployment.go +++ b/internal/controller/assets/deployment.go @@ -388,6 +388,7 @@ func ImageAnalyzerDeployment(name string, namespace string, component string, im NodeSelector: common.NodeSelector, Volumes: volumes, PriorityClassName: falconImageAnalyzer.Spec.ImageAnalyzerConfig.PriorityClass.Name, + Tolerations: falconImageAnalyzer.Spec.Tolerations, }, }, }, @@ -769,6 +770,7 @@ func AdmissionDeployment(name string, namespace string, component string, imageU PriorityClassName: common.FalconPriorityClassName, Containers: *kacContainers, Volumes: volumes, + Tolerations: falconAdmission.Spec.AdmissionConfig.Tolerations, }, }, }, diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index e20d452a..6a242ea4 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -300,58 +300,109 @@ func (r *FalconImageAnalyzerReconciler) reconcileImageAnalyzerDeployment(ctx con return err } - if len(proxy.ReadProxyVarsFromEnv()) > 0 { - for i, container := range existingDeployment.Spec.Template.Spec.Containers { - newContainerEnv := common.AppendUniqueEnvVars(container.Env, proxy.ReadProxyVarsFromEnv()) - updatedContainerEnv := common.UpdateEnvVars(container.Env, proxy.ReadProxyVarsFromEnv()) - if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[i].Env, newContainerEnv) { - existingDeployment.Spec.Template.Spec.Containers[i].Env = newContainerEnv - updated = true - } - if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[i].Env, updatedContainerEnv) { - existingDeployment.Spec.Template.Spec.Containers[i].Env = updatedContainerEnv - updated = true - } - if updated { - log.Info("Updating FalconNodeSensor Deployment Proxy Settings") + existingDeployment.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) + + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + if len(proxy.ReadProxyVarsFromEnv()) > 0 { + for i, container := range existingDeployment.Spec.Template.Spec.Containers { + newContainerEnv := common.AppendUniqueEnvVars(container.Env, proxy.ReadProxyVarsFromEnv()) + updatedContainerEnv := common.UpdateEnvVars(container.Env, proxy.ReadProxyVarsFromEnv()) + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[i].Env, newContainerEnv) { + existingDeployment.Spec.Template.Spec.Containers[i].Env = newContainerEnv + updated = true + } + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[i].Env, updatedContainerEnv) { + existingDeployment.Spec.Template.Spec.Containers[i].Env = updatedContainerEnv + updated = true + } + if updated { + log.Info("Updating FalconNodeSensor Deployment Proxy Settings") + } } } - } - if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].Image, existingDeployment.Spec.Template.Spec.Containers[0].Image) { - existingDeployment.Spec.Template.Spec.Containers[0].Image = dep.Spec.Template.Spec.Containers[0].Image - updated = true - } + if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].Image, existingDeployment.Spec.Template.Spec.Containers[0].Image) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: Container Image changed", + "old", existingDeployment.Spec.Template.Spec.Containers[0].Image, + "new", dep.Spec.Template.Spec.Containers[0].Image) + existingDeployment.Spec.Template.Spec.Containers[0].Image = dep.Spec.Template.Spec.Containers[0].Image + updated = true + } - if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].ImagePullPolicy, existingDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy) { - existingDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = dep.Spec.Template.Spec.Containers[0].ImagePullPolicy - updated = true - } + if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].ImagePullPolicy, existingDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: Container ImagePullPolicy changed", + "old", existingDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy, + "new", dep.Spec.Template.Spec.Containers[0].ImagePullPolicy) + existingDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = dep.Spec.Template.Spec.Containers[0].ImagePullPolicy + updated = true + } - if !reflect.DeepEqual(dep.Spec.Template.Spec.ImagePullSecrets, existingDeployment.Spec.Template.Spec.ImagePullSecrets) { - existingDeployment.Spec.Template.Spec.ImagePullSecrets = dep.Spec.Template.Spec.ImagePullSecrets - updated = true - } + if !reflect.DeepEqual(dep.Spec.Template.Spec.ImagePullSecrets, existingDeployment.Spec.Template.Spec.ImagePullSecrets) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: ImagePullSecrets changed", + "old", existingDeployment.Spec.Template.Spec.ImagePullSecrets, + "new", dep.Spec.Template.Spec.ImagePullSecrets) + existingDeployment.Spec.Template.Spec.ImagePullSecrets = dep.Spec.Template.Spec.ImagePullSecrets + updated = true + } - if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].Ports, existingDeployment.Spec.Template.Spec.Containers[0].Ports) { - existingDeployment.Spec.Template.Spec.Containers[0].Ports = dep.Spec.Template.Spec.Containers[0].Ports - updated = true - } + if !reflect.DeepEqual(dep.Spec.Template.Spec.Containers[0].Ports, existingDeployment.Spec.Template.Spec.Containers[0].Ports) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: Container Ports changed", + "old", existingDeployment.Spec.Template.Spec.Containers[0].Ports, + "new", dep.Spec.Template.Spec.Containers[0].Ports) + existingDeployment.Spec.Template.Spec.Containers[0].Ports = dep.Spec.Template.Spec.Containers[0].Ports + updated = true + } - if !reflect.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { - existingDeployment.Spec.Strategy.RollingUpdate = dep.Spec.Strategy.RollingUpdate - updated = true - } + if !reflect.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: RollingUpdate strategy changed", + "old", existingDeployment.Spec.Strategy.RollingUpdate, + "new", dep.Spec.Strategy.RollingUpdate) + existingDeployment.Spec.Strategy.RollingUpdate = dep.Spec.Strategy.RollingUpdate + updated = true + } - if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Affinity.NodeAffinity, dep.Spec.Template.Spec.Affinity.NodeAffinity) { - existingDeployment.Spec.Template.Spec.Affinity.NodeAffinity = dep.Spec.Template.Spec.Affinity.NodeAffinity - updated = true - } + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Affinity.NodeAffinity, dep.Spec.Template.Spec.Affinity.NodeAffinity) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: NodeAffinity changed", + "old", existingDeployment.Spec.Template.Spec.Affinity.NodeAffinity, + "new", dep.Spec.Template.Spec.Affinity.NodeAffinity) + existingDeployment.Spec.Template.Spec.Affinity.NodeAffinity = dep.Spec.Template.Spec.Affinity.NodeAffinity + updated = true + } - if updated { - if err := k8sutils.Update(r.Client, ctx, req, log, falconImageAnalyzer, &falconImageAnalyzer.Status, existingDeployment); err != nil { - return err + mergedTolerations := dep.Spec.Template.Spec.Tolerations + for _, existingTol := range existingDeployment.Spec.Template.Spec.Tolerations { + found := false + for _, specTol := range dep.Spec.Template.Spec.Tolerations { + if reflect.DeepEqual(existingTol, specTol) { + found = true + break + } + } + + if !found { + mergedTolerations = append(mergedTolerations, existingTol) + } + } + + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { + log.V(1).Info("Updating FalconImageAnalyzer Deployment: Tolerations changed", + "old", existingDeployment.Spec.Template.Spec.Tolerations, + "new", mergedTolerations) + existingDeployment.Spec.Template.Spec.Tolerations = mergedTolerations + updated = true + } + + if updated { + if err := k8sutils.Update(r.Client, ctx, req, log, falconImageAnalyzer, &falconImageAnalyzer.Status, existingDeployment); err != nil { + return err + } } + + return nil + }) + if err != nil { + log.Error(err, "Failed to update FalconImageAnalyzer Deployment after retries") + return err } return nil @@ -520,6 +571,9 @@ func (r *FalconImageAnalyzerReconciler) reconcileIARAgentService(ctx context.Con return err } + // Set GVK on existingService since it's not populated when retrieved from the API server + existingService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) + if !reflect.DeepEqual(service.Spec, existingService.Spec) { existingService.Spec = service.Spec err = k8sutils.Update(r.Client, ctx, req, log, falconImageAnalyzer, &falconImageAnalyzer.Status, existingService) diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller_test.go b/internal/controller/falcon_image_analyzer/falconimage_controller_test.go index 248c31c0..d7c45b13 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller_test.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller_test.go @@ -773,5 +773,256 @@ var _ = Describe("FalconImageAnalyzer controller", func() { Expect(serviceAccount.Annotations).To(HaveKeyWithValue("openshift.io/sa.scc.uid-range", "1000710000/10000")) Expect(serviceAccount.Annotations).To(HaveKeyWithValue("operator-managed-annotation", "value1")) }) + + It("should apply tolerations from spec to deployment", func() { + By("Creating FalconImageAnalyzer with tolerations") + falconImageAnalyzer := &falconv1alpha1.FalconImageAnalyzer{ + ObjectMeta: metav1.ObjectMeta{ + Name: ImageAnalyzerName, + Namespace: testNamespace.Name, + }, + Spec: falconv1alpha1.FalconImageAnalyzerSpec{ + InstallNamespace: imageAnalyzerNamespacedName.Namespace, + FalconAPI: &falconv1alpha1.FalconAPI{ + CID: &falconCID, + CloudRegion: "autodiscover", + }, + Image: imageAnalyzerImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + Tolerations: []corev1.Toleration{ + { + Key: "node.kubernetes.io/disk-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + } + + err := k8sClient.Create(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling the custom resource") + falconImageAnalyzerReconciler := &FalconImageAnalyzerReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying tolerations were applied to deployment") + Eventually(func() []corev1.Toleration { + deployment := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + return deployment.Spec.Template.Spec.Tolerations + }, 10*time.Second, time.Second).Should(ContainElement(corev1.Toleration{ + Key: "node.kubernetes.io/disk-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + })) + }) + + It("should preserve system-added tolerations while applying spec tolerations", func() { + By("Creating FalconImageAnalyzer with tolerations") + falconImageAnalyzer := &falconv1alpha1.FalconImageAnalyzer{ + ObjectMeta: metav1.ObjectMeta{ + Name: ImageAnalyzerName, + Namespace: testNamespace.Name, + }, + Spec: falconv1alpha1.FalconImageAnalyzerSpec{ + InstallNamespace: imageAnalyzerNamespacedName.Namespace, + FalconAPI: &falconv1alpha1.FalconAPI{ + CID: &falconCID, + CloudRegion: "autodiscover", + }, + Image: imageAnalyzerImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + Tolerations: []corev1.Toleration{ + { + Key: "custom-taint", + Operator: corev1.TolerationOpEqual, + Value: "custom-value", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + } + + err := k8sClient.Create(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to create initial deployment") + falconImageAnalyzerReconciler := &FalconImageAnalyzerReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Simulating system adding tolerations (e.g., OpenShift)") + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + }, 10*time.Second, time.Second).Should(Succeed()) + + // Add system toleration + systemToleration := corev1.Toleration{ + Key: "node.kubernetes.io/not-ready", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: func() *int64 { + seconds := int64(300) + return &seconds + }(), + } + deployment.Spec.Template.Spec.Tolerations = append( + deployment.Spec.Template.Spec.Tolerations, + systemToleration, + ) + err = k8sClient.Update(ctx, deployment) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling again to verify system tolerations are preserved") + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both spec and system tolerations exist") + Eventually(func() []corev1.Toleration { + deployment := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + return deployment.Spec.Template.Spec.Tolerations + }, 10*time.Second, time.Second).Should(And( + ContainElement(corev1.Toleration{ + Key: "custom-taint", + Operator: corev1.TolerationOpEqual, + Value: "custom-value", + Effect: corev1.TaintEffectNoSchedule, + }), + ContainElement(systemToleration), + )) + }) + + It("should update tolerations when spec changes", func() { + By("Creating FalconImageAnalyzer with initial tolerations") + falconImageAnalyzer := &falconv1alpha1.FalconImageAnalyzer{ + ObjectMeta: metav1.ObjectMeta{ + Name: ImageAnalyzerName, + Namespace: testNamespace.Name, + }, + Spec: falconv1alpha1.FalconImageAnalyzerSpec{ + InstallNamespace: imageAnalyzerNamespacedName.Namespace, + FalconAPI: &falconv1alpha1.FalconAPI{ + CID: &falconCID, + CloudRegion: "autodiscover", + }, + Image: imageAnalyzerImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + Tolerations: []corev1.Toleration{ + { + Key: "initial-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + } + + err := k8sClient.Create(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to create initial deployment") + falconImageAnalyzerReconciler := &FalconImageAnalyzerReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying initial toleration") + Eventually(func() []corev1.Toleration { + deployment := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + return deployment.Spec.Template.Spec.Tolerations + }, 10*time.Second, time.Second).Should(ContainElement(corev1.Toleration{ + Key: "initial-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + })) + + By("Updating tolerations in spec") + err = k8sClient.Get(ctx, imageAnalyzerNamespacedName, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + falconImageAnalyzer.Spec.Tolerations = []corev1.Toleration{ + { + Key: "updated-taint", + Operator: corev1.TolerationOpEqual, + Value: "updated-value", + Effect: corev1.TaintEffectNoExecute, + }, + } + err = k8sClient.Update(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to apply updated tolerations") + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both tolerations are present (old toleration preserved as it looks like system-added)") + Eventually(func() []corev1.Toleration { + deployment := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + return deployment.Spec.Template.Spec.Tolerations + }, 10*time.Second, time.Second).Should(And( + ContainElement(corev1.Toleration{ + Key: "updated-taint", + Operator: corev1.TolerationOpEqual, + Value: "updated-value", + Effect: corev1.TaintEffectNoExecute, + }), + ContainElement(corev1.Toleration{ + Key: "initial-taint", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }), + )) + }) }) }) diff --git a/test/e2e/cr_config_test.go b/test/e2e/cr_config_test.go index eb3e2ad0..af7f076f 100644 --- a/test/e2e/cr_config_test.go +++ b/test/e2e/cr_config_test.go @@ -41,7 +41,7 @@ var ( kind: "FalconImageAnalyzer", namespace: "falcon-iar", metadataName: "falcon-image-analyzer", - componentName: "admission_controller", + componentName: "falcon-imageanalyzer", } nodeConfig = crConfig{ kind: "FalconNodeSensor", diff --git a/test/e2e/e2e_common_test.go b/test/e2e/e2e_common_test.go index 3a315230..8da55f00 100644 --- a/test/e2e/e2e_common_test.go +++ b/test/e2e/e2e_common_test.go @@ -6,8 +6,11 @@ import ( "os" "os/exec" "path/filepath" + "strings" + falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" "github.com/crowdstrike/falcon-operator/test/utils" + //nolint:golint //nolint:revive . "github.com/onsi/ginkgo/v2" @@ -15,6 +18,7 @@ import ( //nolint:golint //nolint:revive . "github.com/onsi/gomega" + "sigs.k8s.io/yaml" ) type tokenRequest struct { @@ -174,3 +178,56 @@ func (c crConfig) validateInitContainerReadOnlyRootFilesystem() { ExpectWithOffset(1, err).NotTo(HaveOccurred()) ExpectWithOffset(1, string(output)).To(Equal("true")) } + +// loadManifest reads a manifest file, unmarshals it into the provided object, and updates credentials +func loadManifest(manifest string, obj interface{}) error { + manifestPath := filepath.Join(projectDir, manifest) + data, err := os.ReadFile(manifestPath) + if err != nil { + return err + } + + if err := yaml.Unmarshal(data, obj); err != nil { + return err + } + + falconClientID, falconClientSecret := getCredentials() + if falconClientID == "" || falconClientSecret == "" { + return nil + } + + switch v := obj.(type) { + case *falconv1alpha1.FalconImageAnalyzer: + v.Spec.FalconAPI.ClientId = falconClientID + v.Spec.FalconAPI.ClientSecret = falconClientSecret + case *falconv1alpha1.FalconAdmission: + v.Spec.FalconAPI.ClientId = falconClientID + v.Spec.FalconAPI.ClientSecret = falconClientSecret + case *falconv1alpha1.FalconNodeSensor: + v.Spec.FalconAPI.ClientId = falconClientID + v.Spec.FalconAPI.ClientSecret = falconClientSecret + case *falconv1alpha1.FalconContainer: + v.Spec.FalconAPI.ClientId = falconClientID + v.Spec.FalconAPI.ClientSecret = falconClientSecret + case *falconv1alpha1.FalconDeployment: + v.Spec.FalconAPI.ClientId = falconClientID + v.Spec.FalconAPI.ClientSecret = falconClientSecret + } + + return nil +} + +// applyManifest marshals the object to YAML and applies it via kubectl +func applyManifest(obj interface{}, namespace string) error { + // Marshal to YAML + data, err := yaml.Marshal(obj) + if err != nil { + return err + } + + // Apply via kubectl + cmd := exec.Command("kubectl", "apply", "-f", "-", "-n", namespace) + cmd.Stdin = strings.NewReader(string(data)) + _, err = utils.Run(cmd) + return err +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 524e04d6..cd0e79d0 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,6 +10,10 @@ import ( "sync" "time" + falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + "github.com/crowdstrike/falcon-operator/test/utils" + corev1 "k8s.io/api/core/v1" + //nolint:golint //nolint:revive . "github.com/onsi/ginkgo/v2" @@ -17,8 +21,6 @@ import ( //nolint:golint //nolint:revive . "github.com/onsi/gomega" - - "github.com/crowdstrike/falcon-operator/test/utils" ) // Environment Variables: @@ -697,4 +699,189 @@ var _ = Describe("falcon", Ordered, func() { secretConfig.waitForNamespaceDeletion() }) }) + + Context("Falcon Image Analyzer Tolerations", func() { + manifest := "./config/samples/falcon_v1alpha1_falconimageanalyzer.yaml" + It("should deploy with tolerations successfully", func() { + By("loading and modifying the FalconImageAnalyzer manifest") + var iar falconv1alpha1.FalconImageAnalyzer + err := loadManifest(manifest, &iar) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Add tolerations at the spec level + iar.Spec.Tolerations = []corev1.Toleration{ + { + Key: "node.kubernetes.io/not-ready", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: func(i int64) *int64 { return &i }(300), + }, + } + + By("applying the modified manifest") + err = applyManifest(&iar, iarConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + iarConfig.validateRunningStatus(shouldBeRunning) + iarConfig.validateCrStatus() + + By("validating the deployment has the expected tolerations") + validateTolerationsInDeployment := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node.kubernetes.io/not-ready')].effect}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoExecute") { + return fmt.Errorf("expected toleration not found in deployment: %s", output) + } + return nil + } + EventuallyWithOffset(1, validateTolerationsInDeployment, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should update tolerations when spec changes", func() { + By("loading and modifying the FalconImageAnalyzer manifest with updated tolerations") + var iar falconv1alpha1.FalconImageAnalyzer + err := loadManifest(manifest, &iar) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Update tolerations + iar.Spec.Tolerations = []corev1.Toleration{ + { + Key: "node.kubernetes.io/disk-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + } + + By("applying the updated manifest") + err = applyManifest(&iar, iarConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating the deployment tolerations are updated") + validateUpdatedTolerations := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node.kubernetes.io/disk-pressure')].effect}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoSchedule") { + return fmt.Errorf("updated toleration not found in deployment: %s", output) + } + return nil + } + EventuallyWithOffset(1, validateUpdatedTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should cleanup successfully", func() { + iarConfig.manageCrdInstance(crDelete, manifest) + iarConfig.validateRunningStatus(shouldBeTerminated) + iarConfig.waitForNamespaceDeletion() + }) + }) + + Context("Falcon Admission Controller Tolerations", func() { + manifest := "./config/samples/falcon_v1alpha1_falconadmission.yaml" + It("should deploy with tolerations successfully", func() { + By("loading and modifying the FalconAdmission manifest") + var admission falconv1alpha1.FalconAdmission + err := loadManifest(manifest, &admission) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Add tolerations to AdmissionConfig + admission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "node.kubernetes.io/memory-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + } + + By("applying the modified manifest") + err = applyManifest(&admission, kacConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + kacConfig.validateRunningStatus(shouldBeRunning) + kacConfig.validateCrStatus() + + By("validating the deployment has the expected tolerations") + validateTolerationsInDeployment := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node.kubernetes.io/memory-pressure')].effect}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoSchedule") { + return fmt.Errorf("expected toleration not found in deployment: %s", output) + } + return nil + } + EventuallyWithOffset(1, validateTolerationsInDeployment, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should preserve system-added tolerations", func() { + By("getting current tolerations") + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations}") + currentTolerations, err := utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("loading and modifying the FalconAdmission manifest with additional tolerations") + var admission falconv1alpha1.FalconAdmission + err = loadManifest(manifest, &admission) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Add multiple tolerations to AdmissionConfig + admission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "node.kubernetes.io/memory-pressure", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "custom-taint", + Operator: corev1.TolerationOpEqual, + Value: "true", + Effect: corev1.TaintEffectNoExecute, + }, + } + + By("applying the updated manifest") + err = applyManifest(&admission, kacConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating both old and new tolerations are present") + validateBothTolerations := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations}") + updatedTolerations, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + + tolerationsStr := string(updatedTolerations) + if !strings.Contains(tolerationsStr, "node.kubernetes.io/memory-pressure") { + return fmt.Errorf("previous toleration not preserved: %s", tolerationsStr) + } + if !strings.Contains(tolerationsStr, "custom-taint") { + return fmt.Errorf("new toleration not found: %s", tolerationsStr) + } + + // Verify we have at least the tolerations we specified + // (may have additional system-added ones) + if len(currentTolerations) > 0 && len(updatedTolerations) < len(currentTolerations) { + return fmt.Errorf("tolerations count decreased unexpectedly") + } + return nil + } + EventuallyWithOffset(1, validateBothTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should cleanup successfully", func() { + kacConfig.manageCrdInstance(crDelete, manifest) + kacConfig.validateRunningStatus(shouldBeTerminated) + kacConfig.waitForNamespaceDeletion() + }) + }) }) From 45a4a157f6ebfa18c6f376e048babca43322ff0b Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:00:48 -0500 Subject: [PATCH 02/10] Testing --- api/falcon/v1alpha1/falconadmission_types.go | 2 +- .../v1alpha1/falconimageanalyzer_types.go | 4 +- ...falcon-operator.clusterserviceversion.yaml | 22 +- ...lcon.crowdstrike.com_falconadmissions.yaml | 42 +++ ...con.crowdstrike.com_falcondeployments.yaml | 87 +++++- ....crowdstrike.com_falconimageanalyzers.yaml | 45 ++- ...con.crowdstrike.com_falcondeployments.yaml | 6 +- ....crowdstrike.com_falconimageanalyzers.yaml | 6 +- ...falcon-operator.clusterserviceversion.yaml | 20 +- deploy/falcon-operator.yaml | 12 +- .../admission/falconadmission_controller.go | 2 +- .../falconadmission_controller_test.go | 188 +++++++++++++ .../falconimage_controller.go | 2 +- .../falconimage_controller_test.go | 194 +++++++++++++ test/e2e/e2e_test.go | 262 +++++++++++++++++- 15 files changed, 865 insertions(+), 29 deletions(-) diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index 70c3fd3a..181278d6 100644 --- a/api/falcon/v1alpha1/falconadmission_types.go +++ b/api/falcon/v1alpha1/falconadmission_types.go @@ -208,7 +208,7 @@ type FalconAdmissionConfigSpec struct { // Specifies tolerations for scheduling the Admission Controller. // +kubebuilder:default:={} - // +operator-sdk:csv:customresourcedefinitions:type=spec,order=19 + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=20 Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } diff --git a/api/falcon/v1alpha1/falconimageanalyzer_types.go b/api/falcon/v1alpha1/falconimageanalyzer_types.go index d259fef5..f61f7ad0 100644 --- a/api/falcon/v1alpha1/falconimageanalyzer_types.go +++ b/api/falcon/v1alpha1/falconimageanalyzer_types.go @@ -55,11 +55,11 @@ type FalconImageAnalyzerSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Image Analyzer Version",order=7 Version *string `json:"version,omitempty"` - // Specifies node affinity for scheduling the Sensor. + // Specifies node affinity for scheduling the Falcon Image Analyzer Sensor. // +operator-sdk:csv:customresourcedefinitions:type=spec,order=8 NodeAffinity *corev1.NodeAffinity `json:"nodeAffinity,omitempty"` - // Specifies tolerations for scheduling the Sensor. + // Specifies tolerations for scheduling the Falcon Image Analyzer Sensor. // +kubebuilder:default:={} // +operator-sdk:csv:customresourcedefinitions:type=spec,order=9 Tolerations []corev1.Toleration `json:"tolerations,omitempty"` diff --git a/bundle/manifests/falcon-operator.clusterserviceversion.yaml b/bundle/manifests/falcon-operator.clusterserviceversion.yaml index 2496a0df..267fdaef 100644 --- a/bundle/manifests/falcon-operator.clusterserviceversion.yaml +++ b/bundle/manifests/falcon-operator.clusterserviceversion.yaml @@ -148,7 +148,7 @@ metadata: capabilities: Seamless Upgrades categories: Security,Monitoring containerImage: quay.io/crowdstrike/falcon-operator - createdAt: "2025-11-05T18:34:58Z" + createdAt: "2026-05-28T19:37:16Z" description: Falcon Operator installs CrowdStrike Falcon Sensors on the cluster features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" @@ -431,6 +431,9 @@ spec: to know this to discover and communicate with IAR. displayName: Falcon Image Analyzer Namespace path: admissionConfig.falconImageAnalyzerNamespace + - description: Specifies tolerations for scheduling the Admission Controller. + displayName: Tolerations + path: admissionConfig.tolerations version: v1alpha1 - description: FalconContainer is the Schema for the falconcontainers API displayName: Falcon Container @@ -1429,7 +1432,8 @@ spec: - description: Set the falcon image analyzer volume size limit. displayName: Falcon Image Analyzer Volume Size Limit path: falconImageAnalyzer.imageAnalyzerConfig.sizeLimit - - description: Specifies node affinity for scheduling the Sensor. + - description: Specifies node affinity for scheduling the Falcon Image Analyzer + Sensor. displayName: Node Affinity path: falconImageAnalyzer.nodeAffinity - description: Utilize default or Pay-As-You-Go billing. @@ -1458,6 +1462,10 @@ spec: - description: Set the falcon image analyzer volume mount path. displayName: Falcon Image Analyzer Volume Mount Path path: falconImageAnalyzer.imageAnalyzerConfig.mountPath + - description: Specifies tolerations for scheduling the Falcon Image Analyzer + Sensor. + displayName: Tolerations + path: falconImageAnalyzer.tolerations - description: Falcon Node Sensor Controller Configuration displayName: Falcon Node Sensor Configuration path: falconNodeSensor @@ -1559,6 +1567,9 @@ spec: to know this to discover and communicate with IAR. displayName: Falcon Image Analyzer Namespace path: falconAdmission.admissionConfig.falconImageAnalyzerNamespace + - description: Specifies tolerations for scheduling the Admission Controller. + displayName: Tolerations + path: falconAdmission.admissionConfig.tolerations - description: |- Advanced configures various options that go against industry practices or are otherwise not recommended for use. Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. @@ -1802,12 +1813,17 @@ spec: - description: Set the falcon image analyzer volume size limit. displayName: Falcon Image Analyzer Volume Size Limit path: imageAnalyzerConfig.sizeLimit - - description: Specifies node affinity for scheduling the Sensor. + - description: Specifies node affinity for scheduling the Falcon Image Analyzer + Sensor. displayName: Node Affinity path: nodeAffinity - description: Set the falcon image analyzer volume mount path. displayName: Falcon Image Analyzer Volume Mount Path path: imageAnalyzerConfig.mountPath + - description: Specifies tolerations for scheduling the Falcon Image Analyzer + Sensor. + displayName: Tolerations + path: tolerations - description: Name of the Kubernetes Cluster. displayName: Falcon Image Analyzer Cluster Name path: imageAnalyzerConfig.clusterName diff --git a/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml index 745dcc51..14fc385f 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml @@ -643,6 +643,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: diff --git a/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml b/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml index 8f9d943c..8a1f42f0 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml @@ -699,6 +699,48 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Admission + Controller. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array updateStrategy: default: rollingUpdate: @@ -3784,7 +3826,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon + Image Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -4020,6 +4063,48 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: diff --git a/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml b/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml index ef9c5536..1dbfed2f 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -375,7 +375,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon Image + Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -611,6 +612,48 @@ spec: required: - type type: object + tolerations: + default: [] + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array version: description: 'Falcon Image Analyzer Version. The latest version will be selected when version specifier is missing. Example: 6.31, 6.31.0, diff --git a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml index 38290ffc..88b09d24 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -3834,7 +3834,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon + Image Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -4072,7 +4073,8 @@ spec: type: object tolerations: default: [] - description: Specifies tolerations for scheduling the Sensor. + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. items: description: |- The pod this Toleration is attached to tolerates any taint that matches diff --git a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml index 7f4eb1d0..4ff50ede 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -375,7 +375,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon Image + Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -613,7 +614,8 @@ spec: type: object tolerations: default: [] - description: Specifies tolerations for scheduling the Sensor. + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. items: description: |- The pod this Toleration is attached to tolerates any taint that matches diff --git a/config/manifests/bases/falcon-operator.clusterserviceversion.yaml b/config/manifests/bases/falcon-operator.clusterserviceversion.yaml index fedca6b8..ab76a290 100644 --- a/config/manifests/bases/falcon-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/falcon-operator.clusterserviceversion.yaml @@ -287,6 +287,9 @@ spec: to know this to discover and communicate with IAR. displayName: Falcon Image Analyzer Namespace path: admissionConfig.falconImageAnalyzerNamespace + - description: Specifies tolerations for scheduling the Admission Controller. + displayName: Tolerations + path: admissionConfig.tolerations version: v1alpha1 - description: FalconContainer is the Schema for the falconcontainers API displayName: Falcon Container @@ -1285,7 +1288,8 @@ spec: - description: Set the falcon image analyzer volume size limit. displayName: Falcon Image Analyzer Volume Size Limit path: falconImageAnalyzer.imageAnalyzerConfig.sizeLimit - - description: Specifies node affinity for scheduling the Sensor. + - description: Specifies node affinity for scheduling the Falcon Image Analyzer + Sensor. displayName: Node Affinity path: falconImageAnalyzer.nodeAffinity - description: Utilize default or Pay-As-You-Go billing. @@ -1314,6 +1318,10 @@ spec: - description: Set the falcon image analyzer volume mount path. displayName: Falcon Image Analyzer Volume Mount Path path: falconImageAnalyzer.imageAnalyzerConfig.mountPath + - description: Specifies tolerations for scheduling the Falcon Image Analyzer + Sensor. + displayName: Tolerations + path: falconImageAnalyzer.tolerations - description: Falcon Node Sensor Controller Configuration displayName: Falcon Node Sensor Configuration path: falconNodeSensor @@ -1415,6 +1423,9 @@ spec: to know this to discover and communicate with IAR. displayName: Falcon Image Analyzer Namespace path: falconAdmission.admissionConfig.falconImageAnalyzerNamespace + - description: Specifies tolerations for scheduling the Admission Controller. + displayName: Tolerations + path: falconAdmission.admissionConfig.tolerations - description: |- Advanced configures various options that go against industry practices or are otherwise not recommended for use. Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. @@ -1658,12 +1669,17 @@ spec: - description: Set the falcon image analyzer volume size limit. displayName: Falcon Image Analyzer Volume Size Limit path: imageAnalyzerConfig.sizeLimit - - description: Specifies node affinity for scheduling the Sensor. + - description: Specifies node affinity for scheduling the Falcon Image Analyzer + Sensor. displayName: Node Affinity path: nodeAffinity - description: Set the falcon image analyzer volume mount path. displayName: Falcon Image Analyzer Volume Mount Path path: imageAnalyzerConfig.mountPath + - description: Specifies tolerations for scheduling the Falcon Image Analyzer + Sensor. + displayName: Tolerations + path: tolerations - description: Name of the Kubernetes Cluster. displayName: Falcon Image Analyzer Cluster Name path: imageAnalyzerConfig.clusterName diff --git a/deploy/falcon-operator.yaml b/deploy/falcon-operator.yaml index e5117ac7..7f97cf4c 100644 --- a/deploy/falcon-operator.yaml +++ b/deploy/falcon-operator.yaml @@ -7455,7 +7455,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon + Image Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -7693,7 +7694,8 @@ spec: type: object tolerations: default: [] - description: Specifies tolerations for scheduling the Sensor. + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. items: description: |- The pod this Toleration is attached to tolerates any taint that matches @@ -8884,7 +8886,8 @@ spec: It also should not be the same namespace where the Falcon Operator or the Falcon Sensor is installed. type: string nodeAffinity: - description: Specifies node affinity for scheduling the Sensor. + description: Specifies node affinity for scheduling the Falcon Image + Analyzer Sensor. properties: preferredDuringSchedulingIgnoredDuringExecution: description: |- @@ -9122,7 +9125,8 @@ spec: type: object tolerations: default: [] - description: Specifies tolerations for scheduling the Sensor. + description: Specifies tolerations for scheduling the Falcon Image + Analyzer Sensor. items: description: |- The pod this Toleration is attached to tolerates any taint that matches diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index ec78380b..87f434ab 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -726,7 +726,7 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con for _, existingTol := range existingDeployment.Spec.Template.Spec.Tolerations { found := false for _, specTol := range dep.Spec.Template.Spec.Tolerations { - if reflect.DeepEqual(existingTol, specTol) { + if existingTol.Key == specTol.Key && existingTol.Effect == specTol.Effect { found = true break } diff --git a/internal/controller/admission/falconadmission_controller_test.go b/internal/controller/admission/falconadmission_controller_test.go index 84084160..a3549e14 100644 --- a/internal/controller/admission/falconadmission_controller_test.go +++ b/internal/controller/admission/falconadmission_controller_test.go @@ -1490,5 +1490,193 @@ var _ = Describe("FalconAdmission controller", func() { return hasUpdated }, 5*time.Second, time.Second).Should(BeTrue()) }) + + It("should replace toleration when spec has same Key+Effect but different Value/Operator", func() { + ctx := context.Background() + + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerName, + Namespace: namespaceName, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: namespaceName, + Image: "crowdstrike/falcon-kac:test", + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + Tolerations: []corev1.Toleration{ + { + Key: "app", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, falconAdmission)).To(Succeed()) + + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err := falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying initial toleration with Exists operator") + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + }, 5*time.Second, time.Second).Should(Succeed()) + + Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("app")) + Expect(deployment.Spec.Template.Spec.Tolerations[0].Operator).To(Equal(corev1.TolerationOpExists)) + + By("Manually updating deployment to simulate existing toleration with different value") + deployment.Spec.Template.Spec.Tolerations[0] = corev1.Toleration{ + Key: "app", + Operator: corev1.TolerationOpEqual, + Value: "old-value", + Effect: corev1.TaintEffectNoSchedule, + } + Expect(k8sClient.Update(ctx, deployment)).To(Succeed()) + + By("Waiting for deployment to be older than 5 seconds to allow reconcile updates") + time.Sleep(6 * time.Second) + + By("Updating spec with new value for same Key+Effect") + Eventually(func() error { + if err := k8sClient.Get(ctx, admissionNamespacedName, falconAdmission); err != nil { + return err + } + falconAdmission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "app", + Operator: corev1.TolerationOpEqual, + Value: "new-value", + Effect: corev1.TaintEffectNoSchedule, + }, + } + return k8sClient.Update(ctx, falconAdmission) + }, 5*time.Second, time.Second).Should(Succeed()) + + By("Reconciling to apply updated toleration") + _, err = falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying spec toleration replaces existing one (only one toleration with new value)") + deployment = &appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + if err != nil { + return false + } + + if len(deployment.Spec.Template.Spec.Tolerations) != 1 { + return false + } + + tol := deployment.Spec.Template.Spec.Tolerations[0] + return tol.Key == "app" && + tol.Effect == corev1.TaintEffectNoSchedule && + tol.Operator == corev1.TolerationOpEqual && + tol.Value == "new-value" + }, 5*time.Second, time.Second).Should(BeTrue()) + }) + + It("should allow multiple tolerations with same Key but different Effect", func() { + ctx := context.Background() + + falconAdmission := &falconv1alpha1.FalconAdmission{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerName, + Namespace: namespaceName, + }, + Spec: falconv1alpha1.FalconAdmissionSpec{ + Falcon: falconv1alpha1.FalconSensor{ + CID: &falconCID, + }, + InstallNamespace: namespaceName, + Image: "crowdstrike/falcon-kac:test", + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + AdmissionConfig: falconv1alpha1.FalconAdmissionConfigSpec{ + Tolerations: []corev1.Toleration{ + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, falconAdmission)).To(Succeed()) + + falconAdmissionReconciler := &FalconAdmissionReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err := falconAdmissionReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: admissionNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both tolerations with same Key but different Effect exist") + deployment := &appsv1.Deployment{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: falconAdmission.Name, + Namespace: namespaceName, + }, deployment) + if err != nil { + return false + } + + if len(deployment.Spec.Template.Spec.Tolerations) != 2 { + return false + } + + hasNoSchedule := false + hasNoExecute := false + for _, tol := range deployment.Spec.Template.Spec.Tolerations { + if tol.Key == "node-role" && tol.Effect == corev1.TaintEffectNoSchedule { + hasNoSchedule = true + } + if tol.Key == "node-role" && tol.Effect == corev1.TaintEffectNoExecute { + hasNoExecute = true + } + } + return hasNoSchedule && hasNoExecute + }, 5*time.Second, time.Second).Should(BeTrue()) + }) }) }) diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index 6a242ea4..c22d0dd9 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -373,7 +373,7 @@ func (r *FalconImageAnalyzerReconciler) reconcileImageAnalyzerDeployment(ctx con for _, existingTol := range existingDeployment.Spec.Template.Spec.Tolerations { found := false for _, specTol := range dep.Spec.Template.Spec.Tolerations { - if reflect.DeepEqual(existingTol, specTol) { + if existingTol.Key == specTol.Key && existingTol.Effect == specTol.Effect { found = true break } diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller_test.go b/internal/controller/falcon_image_analyzer/falconimage_controller_test.go index d7c45b13..7e8c287a 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller_test.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller_test.go @@ -1024,5 +1024,199 @@ var _ = Describe("FalconImageAnalyzer controller", func() { }), )) }) + + It("should replace toleration when spec has same Key+Effect but different Value/Operator", func() { + By("Creating FalconImageAnalyzer with Exists toleration") + falconImageAnalyzer := &falconv1alpha1.FalconImageAnalyzer{ + ObjectMeta: metav1.ObjectMeta{ + Name: ImageAnalyzerName, + Namespace: testNamespace.Name, + }, + Spec: falconv1alpha1.FalconImageAnalyzerSpec{ + InstallNamespace: imageAnalyzerNamespacedName.Namespace, + FalconAPI: &falconv1alpha1.FalconAPI{ + CID: &falconCID, + CloudRegion: "autodiscover", + }, + Image: imageAnalyzerImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + Tolerations: []corev1.Toleration{ + { + Key: "app", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + } + + err := k8sClient.Create(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to create initial deployment") + falconImageAnalyzerReconciler := &FalconImageAnalyzerReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying initial toleration with Exists operator") + Eventually(func() []corev1.Toleration { + deployment := &appsv1.Deployment{} + _ = k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + return deployment.Spec.Template.Spec.Tolerations + }, 10*time.Second, time.Second).Should(ContainElement(corev1.Toleration{ + Key: "app", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + })) + + By("Manually updating deployment to simulate existing toleration with different operator/value") + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + }, 10*time.Second, time.Second).Should(Succeed()) + + deployment.Spec.Template.Spec.Tolerations[0] = corev1.Toleration{ + Key: "app", + Operator: corev1.TolerationOpEqual, + Value: "old-value", + Effect: corev1.TaintEffectNoSchedule, + } + err = k8sClient.Update(ctx, deployment) + Expect(err).To(Not(HaveOccurred())) + + By("Updating spec with new value for same Key+Effect") + err = k8sClient.Get(ctx, imageAnalyzerNamespacedName, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + falconImageAnalyzer.Spec.Tolerations = []corev1.Toleration{ + { + Key: "app", + Operator: corev1.TolerationOpEqual, + Value: "new-value", + Effect: corev1.TaintEffectNoSchedule, + }, + } + err = k8sClient.Update(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to apply updated toleration") + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying spec toleration replaces existing one (only one toleration with new value)") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + if err != nil { + return false + } + + if len(deployment.Spec.Template.Spec.Tolerations) != 1 { + return false + } + + tol := deployment.Spec.Template.Spec.Tolerations[0] + return tol.Key == "app" && + tol.Effect == corev1.TaintEffectNoSchedule && + tol.Operator == corev1.TolerationOpEqual && + tol.Value == "new-value" + }, 10*time.Second, time.Second).Should(BeTrue()) + }) + + It("should allow multiple tolerations with same Key but different Effect", func() { + By("Creating FalconImageAnalyzer with multiple tolerations for same key") + falconImageAnalyzer := &falconv1alpha1.FalconImageAnalyzer{ + ObjectMeta: metav1.ObjectMeta{ + Name: ImageAnalyzerName, + Namespace: testNamespace.Name, + }, + Spec: falconv1alpha1.FalconImageAnalyzerSpec{ + InstallNamespace: imageAnalyzerNamespacedName.Namespace, + FalconAPI: &falconv1alpha1.FalconAPI{ + CID: &falconCID, + CloudRegion: "autodiscover", + }, + Image: imageAnalyzerImage, + Registry: falconv1alpha1.RegistrySpec{ + Type: "crowdstrike", + }, + Tolerations: []corev1.Toleration{ + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + } + + err := k8sClient.Create(ctx, falconImageAnalyzer) + Expect(err).To(Not(HaveOccurred())) + + By("Reconciling to create deployment") + falconImageAnalyzerReconciler := &FalconImageAnalyzerReconciler{ + Client: k8sClient, + Reader: k8sReader, + Scheme: k8sClient.Scheme(), + } + + _, err = falconImageAnalyzerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: imageAnalyzerNamespacedName, + }) + Expect(err).To(Not(HaveOccurred())) + + By("Verifying both tolerations with same Key but different Effect exist") + Eventually(func() bool { + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: ImageAnalyzerName, + Namespace: imageAnalyzerNamespacedName.Namespace, + }, deployment) + if err != nil { + return false + } + + if len(deployment.Spec.Template.Spec.Tolerations) != 2 { + return false + } + + hasNoSchedule := false + hasNoExecute := false + for _, tol := range deployment.Spec.Template.Spec.Tolerations { + if tol.Key == "node-role" && tol.Effect == corev1.TaintEffectNoSchedule { + hasNoSchedule = true + } + if tol.Key == "node-role" && tol.Effect == corev1.TaintEffectNoExecute { + hasNoExecute = true + } + } + return hasNoSchedule && hasNoExecute + }, 10*time.Second, time.Second).Should(BeTrue()) + }) }) }) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index cd0e79d0..a3cd6a52 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -740,38 +740,143 @@ var _ = Describe("falcon", Ordered, func() { EventuallyWithOffset(1, validateTolerationsInDeployment, defaultTimeout, defaultPollPeriod).Should(Succeed()) }) - It("should update tolerations when spec changes", func() { - By("loading and modifying the FalconImageAnalyzer manifest with updated tolerations") + It("should replace toleration when Key+Effect match but Value/Operator differ", func() { + By("loading and modifying the FalconImageAnalyzer manifest with initial toleration") var iar falconv1alpha1.FalconImageAnalyzer err := loadManifest(manifest, &iar) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - // Update tolerations + // Set initial toleration with Exists operator iar.Spec.Tolerations = []corev1.Toleration{ { - Key: "node.kubernetes.io/disk-pressure", + Key: "app", Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoSchedule, }, } + By("applying the manifest with initial toleration") + err = applyManifest(&iar, iarConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating initial toleration with Exists operator") + validateInitialToleration := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='app')].operator}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "Exists") { + return fmt.Errorf("expected Exists operator not found: %s", output) + } + return nil + } + EventuallyWithOffset(1, validateInitialToleration, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + By("updating toleration with same Key+Effect but different Operator and Value") + err = loadManifest(manifest, &iar) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Update to Equal operator with value for same Key+Effect + iar.Spec.Tolerations = []corev1.Toleration{ + { + Key: "app", + Operator: corev1.TolerationOpEqual, + Value: "falcon", + Effect: corev1.TaintEffectNoSchedule, + }, + } + By("applying the updated manifest") err = applyManifest(&iar, iarConfig.namespace) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("validating the deployment tolerations are updated") - validateUpdatedTolerations := func() error { + By("validating toleration was replaced with new Value and Operator") + validateReplacedToleration := func() error { + // Check for new operator cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", "-n", iarConfig.namespace, - "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node.kubernetes.io/disk-pressure')].effect}") + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='app')].operator}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "Equal") { + return fmt.Errorf("expected Equal operator not found: %s", output) + } + + // Check for new value + cmd = exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='app')].value}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "falcon") { + return fmt.Errorf("expected value 'falcon' not found: %s", output) + } + + // Verify only one toleration with key 'app' exists + cmd = exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='app')]}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + // Should only have one match (not duplicate) + matches := strings.Count(string(output), `"key":"app"`) + if matches > 1 { + return fmt.Errorf("found %d tolerations with key 'app', expected 1: %s", matches, output) + } + return nil + } + EventuallyWithOffset(1, validateReplacedToleration, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should allow multiple tolerations with same Key but different Effect", func() { + By("loading and modifying the FalconImageAnalyzer manifest with multiple tolerations") + var iar falconv1alpha1.FalconImageAnalyzer + err := loadManifest(manifest, &iar) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Set multiple tolerations with same Key but different Effects + iar.Spec.Tolerations = []corev1.Toleration{ + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "node-role", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + } + + By("applying the manifest with multiple tolerations") + err = applyManifest(&iar, iarConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating both tolerations with same Key but different Effect exist") + validateBothEffects := func() error { + // Check for NoSchedule effect + cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-role')][?(@.effect=='NoSchedule')]}") output, err := utils.Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) if !strings.Contains(string(output), "NoSchedule") { - return fmt.Errorf("updated toleration not found in deployment: %s", output) + return fmt.Errorf("NoSchedule effect not found for key 'node-role': %s", output) + } + + // Check for NoExecute effect + cmd = exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", + "-n", iarConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-role')][?(@.effect=='NoExecute')]}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoExecute") { + return fmt.Errorf("NoExecute effect not found for key 'node-role': %s", output) } return nil } - EventuallyWithOffset(1, validateUpdatedTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) + EventuallyWithOffset(1, validateBothEffects, defaultTimeout, defaultPollPeriod).Should(Succeed()) }) It("should cleanup successfully", func() { @@ -878,6 +983,145 @@ var _ = Describe("falcon", Ordered, func() { EventuallyWithOffset(1, validateBothTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) }) + It("should replace toleration when Key+Effect match but Value/Operator differ", func() { + By("loading and modifying the FalconAdmission manifest with initial toleration") + var admission falconv1alpha1.FalconAdmission + err := loadManifest(manifest, &admission) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Set initial toleration with Exists operator + admission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "environment", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + } + + By("applying the manifest with initial toleration") + err = applyManifest(&admission, kacConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating initial toleration with Exists operator") + validateInitialToleration := func() error { + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='environment')].operator}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "Exists") { + return fmt.Errorf("expected Exists operator not found: %s", output) + } + return nil + } + EventuallyWithOffset(1, validateInitialToleration, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + By("updating toleration with same Key+Effect but different Operator and Value") + err = loadManifest(manifest, &admission) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Update to Equal operator with value for same Key+Effect + admission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "environment", + Operator: corev1.TolerationOpEqual, + Value: "production", + Effect: corev1.TaintEffectNoSchedule, + }, + } + + By("applying the updated manifest") + err = applyManifest(&admission, kacConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating toleration was replaced with new Value and Operator") + validateReplacedToleration := func() error { + // Check for new operator + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='environment')].operator}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "Equal") { + return fmt.Errorf("expected Equal operator not found: %s", output) + } + + // Check for new value + cmd = exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='environment')].value}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "production") { + return fmt.Errorf("expected value 'production' not found: %s", output) + } + + // Verify only one toleration with key 'environment' exists + cmd = exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='environment')]}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + // Should only have one match (not duplicate) + matches := strings.Count(string(output), `"key":"environment"`) + if matches > 1 { + return fmt.Errorf("found %d tolerations with key 'environment', expected 1: %s", matches, output) + } + return nil + } + EventuallyWithOffset(1, validateReplacedToleration, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should allow multiple tolerations with same Key but different Effect", func() { + By("loading and modifying the FalconAdmission manifest with multiple tolerations") + var admission falconv1alpha1.FalconAdmission + err := loadManifest(manifest, &admission) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Set multiple tolerations with same Key but different Effects + admission.Spec.AdmissionConfig.Tolerations = []corev1.Toleration{ + { + Key: "node-type", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "node-type", + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + } + + By("applying the manifest with multiple tolerations") + err = applyManifest(&admission, kacConfig.namespace) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating both tolerations with same Key but different Effect exist") + validateBothEffects := func() error { + // Check for NoSchedule effect + cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-type')][?(@.effect=='NoSchedule')]}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoSchedule") { + return fmt.Errorf("NoSchedule effect not found for key 'node-type': %s", output) + } + + // Check for NoExecute effect + cmd = exec.Command("kubectl", "get", "deployment", "falcon-kac", + "-n", kacConfig.namespace, + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-type')][?(@.effect=='NoExecute')]}") + output, err = utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if !strings.Contains(string(output), "NoExecute") { + return fmt.Errorf("NoExecute effect not found for key 'node-type': %s", output) + } + return nil + } + EventuallyWithOffset(1, validateBothEffects, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + It("should cleanup successfully", func() { kacConfig.manageCrdInstance(crDelete, manifest) kacConfig.validateRunningStatus(shouldBeTerminated) From 57e16c21b26a89536667b146fcb693a19aa4b792 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:41:53 -0500 Subject: [PATCH 03/10] testing --- test/e2e/e2e_test.go | 56 ++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index a3cd6a52..76af901c 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -700,7 +700,7 @@ var _ = Describe("falcon", Ordered, func() { }) }) - Context("Falcon Image Analyzer Tolerations", func() { + Context("Falcon Image Analyzer Tolerations", Label("FalconImageAnalyzer"), func() { manifest := "./config/samples/falcon_v1alpha1_falconimageanalyzer.yaml" It("should deploy with tolerations successfully", func() { By("loading and modifying the FalconImageAnalyzer manifest") @@ -855,24 +855,26 @@ var _ = Describe("falcon", Ordered, func() { By("validating both tolerations with same Key but different Effect exist") validateBothEffects := func() error { - // Check for NoSchedule effect + // Get all tolerations with key 'node-role' cmd := exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", "-n", iarConfig.namespace, - "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-role')][?(@.effect=='NoSchedule')]}") + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-role')]}") output, err := utils.Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) - if !strings.Contains(string(output), "NoSchedule") { - return fmt.Errorf("NoSchedule effect not found for key 'node-role': %s", output) + + // Verify both effects are present + outputStr := string(output) + if !strings.Contains(outputStr, "NoSchedule") { + return fmt.Errorf("NoSchedule effect not found for key 'node-role': %s", outputStr) + } + if !strings.Contains(outputStr, "NoExecute") { + return fmt.Errorf("NoExecute effect not found for key 'node-role': %s", outputStr) } - // Check for NoExecute effect - cmd = exec.Command("kubectl", "get", "deployment", "falcon-image-analyzer", - "-n", iarConfig.namespace, - "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-role')][?(@.effect=='NoExecute')]}") - output, err = utils.Run(cmd) - ExpectWithOffset(2, err).NotTo(HaveOccurred()) - if !strings.Contains(string(output), "NoExecute") { - return fmt.Errorf("NoExecute effect not found for key 'node-role': %s", output) + // Verify we have exactly 2 tolerations with this key (one for each effect) + effectCount := strings.Count(outputStr, `"effect":`) + if effectCount != 2 { + return fmt.Errorf("expected 2 tolerations with key 'node-role', found %d: %s", effectCount, outputStr) } return nil } @@ -886,7 +888,7 @@ var _ = Describe("falcon", Ordered, func() { }) }) - Context("Falcon Admission Controller Tolerations", func() { + Context("Falcon Admission Controller Tolerations", Label("FalconAdmission"), func() { manifest := "./config/samples/falcon_v1alpha1_falconadmission.yaml" It("should deploy with tolerations successfully", func() { By("loading and modifying the FalconAdmission manifest") @@ -1098,24 +1100,26 @@ var _ = Describe("falcon", Ordered, func() { By("validating both tolerations with same Key but different Effect exist") validateBothEffects := func() error { - // Check for NoSchedule effect + // Get all tolerations with key 'node-type' cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", "-n", kacConfig.namespace, - "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-type')][?(@.effect=='NoSchedule')]}") + "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-type')]}") output, err := utils.Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) - if !strings.Contains(string(output), "NoSchedule") { - return fmt.Errorf("NoSchedule effect not found for key 'node-type': %s", output) + + // Verify both effects are present + outputStr := string(output) + if !strings.Contains(outputStr, "NoSchedule") { + return fmt.Errorf("NoSchedule effect not found for key 'node-type': %s", outputStr) + } + if !strings.Contains(outputStr, "NoExecute") { + return fmt.Errorf("NoExecute effect not found for key 'node-type': %s", outputStr) } - // Check for NoExecute effect - cmd = exec.Command("kubectl", "get", "deployment", "falcon-kac", - "-n", kacConfig.namespace, - "-o", "jsonpath={.spec.template.spec.tolerations[?(@.key=='node-type')][?(@.effect=='NoExecute')]}") - output, err = utils.Run(cmd) - ExpectWithOffset(2, err).NotTo(HaveOccurred()) - if !strings.Contains(string(output), "NoExecute") { - return fmt.Errorf("NoExecute effect not found for key 'node-type': %s", output) + // Verify we have exactly 2 tolerations with this key (one for each effect) + effectCount := strings.Count(outputStr, `"effect":`) + if effectCount != 2 { + return fmt.Errorf("expected 2 tolerations with key 'node-type', found %d: %s", effectCount, outputStr) } return nil } From 07168ce2804b29ee65f1b1eddec4eb67b1804906 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:55:52 -0500 Subject: [PATCH 04/10] testing --- bundle/manifests/falcon-operator.clusterserviceversion.yaml | 4 ++-- config/manager/kustomization.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bundle/manifests/falcon-operator.clusterserviceversion.yaml b/bundle/manifests/falcon-operator.clusterserviceversion.yaml index 267fdaef..14464da6 100644 --- a/bundle/manifests/falcon-operator.clusterserviceversion.yaml +++ b/bundle/manifests/falcon-operator.clusterserviceversion.yaml @@ -148,7 +148,7 @@ metadata: capabilities: Seamless Upgrades categories: Security,Monitoring containerImage: quay.io/crowdstrike/falcon-operator - createdAt: "2026-05-28T19:37:16Z" + createdAt: "2026-06-01T20:55:16Z" description: Falcon Operator installs CrowdStrike Falcon Sensors on the cluster features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" @@ -2414,7 +2414,7 @@ spec: fieldPath: metadata.annotations['olm.targetNamespaces'] - name: OPERATOR_NAME value: falcon-operator - image: quay.io/crowdstrike/falcon-operator:1.6.0 + image: quay.io/crowdstrike/falcon-operator:iar-kac-tolerations livenessProbe: httpGet: path: /healthz diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 598fb81d..f0317042 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -15,4 +15,4 @@ kind: Kustomization images: - name: controller newName: quay.io/crowdstrike/falcon-operator - newTag: 1.6.0 + newTag: iar-kac-tolerations From df251529d88df08f6c29d5e3d9732e6ec7b493af Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:12:29 -0500 Subject: [PATCH 05/10] Testing --- .../admission/falconadmission_controller.go | 2 +- test/e2e/e2e_test.go | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index 87f434ab..4a3e5b90 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -691,7 +691,7 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con // Merge existing proxy env vars with spec container env to preserve existing proxy envs specContainerEnvWithExistingProxy := common.MergeEnvVars(container.Env, existingContainer.Env, common.ProxyEnvNamesWithLowerCase()) - if !reflect.DeepEqual(specContainerEnvWithExistingProxy, existingContainer.Env) { + if !equality.Semantic.DeepEqual(specContainerEnvWithExistingProxy, existingContainer.Env) { log.V(1).Info( "Updating FalconAdmission Deployment: Environment variables changed", "container", container.Name, diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 76af901c..8713aada 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -890,7 +890,7 @@ var _ = Describe("falcon", Ordered, func() { Context("Falcon Admission Controller Tolerations", Label("FalconAdmission"), func() { manifest := "./config/samples/falcon_v1alpha1_falconadmission.yaml" - It("should deploy with tolerations successfully", func() { + It("should deploy successfully", func() { By("loading and modifying the FalconAdmission manifest") var admission falconv1alpha1.FalconAdmission err := loadManifest(manifest, &admission) @@ -983,6 +983,11 @@ var _ = Describe("falcon", Ordered, func() { return nil } EventuallyWithOffset(1, validateBothTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + if reconcileLoopCheck { + By("validating no reconcile loop after toleration changes") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + } }) It("should replace toleration when Key+Effect match but Value/Operator differ", func() { @@ -1004,6 +1009,11 @@ var _ = Describe("falcon", Ordered, func() { err = applyManifest(&admission, kacConfig.namespace) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + if reconcileLoopCheck { + By("validating no reconcile loop after initial tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + } + By("validating initial toleration with Exists operator") validateInitialToleration := func() error { cmd := exec.Command("kubectl", "get", "deployment", "falcon-kac", @@ -1036,6 +1046,11 @@ var _ = Describe("falcon", Ordered, func() { err = applyManifest(&admission, kacConfig.namespace) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + if reconcileLoopCheck { + By("validating no reconcile loop after updating tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + } + By("validating toleration was replaced with new Value and Operator") validateReplacedToleration := func() error { // Check for new operator @@ -1098,6 +1113,11 @@ var _ = Describe("falcon", Ordered, func() { err = applyManifest(&admission, kacConfig.namespace) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + if reconcileLoopCheck { + By("validating no reconcile loop after adding multiple tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + } + By("validating both tolerations with same Key but different Effect exist") validateBothEffects := func() error { // Get all tolerations with key 'node-type' From bfda6dd4b9e83798399667aeffa67174e5b60ccd Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:21:40 -0500 Subject: [PATCH 06/10] fix rolling deployment loops --- api/falcon/v1alpha1/falconadmission_types.go | 3 ++- .../bases/falcon.crowdstrike.com_falconadmissions.yaml | 8 ++++---- .../bases/falcon.crowdstrike.com_falcondeployments.yaml | 8 ++++---- .../controller/admission/falconadmission_controller.go | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index 181278d6..e6526494 100644 --- a/api/falcon/v1alpha1/falconadmission_types.go +++ b/api/falcon/v1alpha1/falconadmission_types.go @@ -198,7 +198,7 @@ type FalconAdmissionConfigSpec struct { ResourcesAC *corev1.ResourceRequirements `json:"resources,omitempty"` // Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. - // +kubebuilder:default:={"rollingUpdate":{"maxUnavailable":0,"maxSurge":1}} + // +kubebuilder:default:={} // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Deployment Update Strategy",order=11 DepUpdateStrategy FalconAdmissionUpdateStrategy `json:"updateStrategy,omitempty"` @@ -220,6 +220,7 @@ type FalconAdmissionServiceAccount struct { type FalconAdmissionUpdateStrategy struct { // RollingUpdate is used to specify the strategy used to roll out a deployment + // +kubebuilder:default:={"maxUnavailable":0,"maxSurge":1} // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller deployment update configuration",order=1,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:updateStrategy"} RollingUpdate appsv1.RollingUpdateDeployment `json:"rollingUpdate,omitempty"` } diff --git a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml index 594d4290..3bf82426 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml @@ -686,14 +686,14 @@ spec: type: object type: array updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml index 88b09d24..84abc72d 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -742,14 +742,14 @@ spec: type: object type: array updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index 4a3e5b90..1cc3db84 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -598,7 +598,7 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con updated = true } - if !reflect.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { log.V(1).Info("Updating FalconAdmission Deployment: RollingUpdate strategy changed", "old", existingDeployment.Spec.Strategy.RollingUpdate, "new", dep.Spec.Strategy.RollingUpdate) From 4a7d1eb85ee9e99cd33aa869826640ee2b987b37 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:10:14 -0500 Subject: [PATCH 07/10] updating deep equal and thresholds --- .../admission/falconadmission_controller.go | 2 +- .../falconimage_controller.go | 2 +- test/e2e/e2e_helpers_test.go | 10 +++---- test/e2e/e2e_test.go | 30 ++++++++++++++----- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index 1cc3db84..e514ec20 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -737,7 +737,7 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con } } - if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { log.V(1).Info("Updating FalconAdmission Deployment: Tolerations changed", "old", existingDeployment.Spec.Template.Spec.Tolerations, "new", mergedTolerations) diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index c22d0dd9..421913f4 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -384,7 +384,7 @@ func (r *FalconImageAnalyzerReconciler) reconcileImageAnalyzerDeployment(ctx con } } - if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, mergedTolerations) { log.V(1).Info("Updating FalconImageAnalyzer Deployment: Tolerations changed", "old", existingDeployment.Spec.Template.Spec.Tolerations, "new", mergedTolerations) diff --git a/test/e2e/e2e_helpers_test.go b/test/e2e/e2e_helpers_test.go index f23c1b82..dd8848ef 100644 --- a/test/e2e/e2e_helpers_test.go +++ b/test/e2e/e2e_helpers_test.go @@ -59,8 +59,8 @@ func isOpenShift() bool { // validateNoReconcileLoop checks that the controller is not stuck in an infinite reconcile loop // kind parameter specifies the CRD kind to filter logs (e.g., "FalconNodeSensor", "FalconAdmission") -func validateNoReconcileLoop(controllerPodName, namespace, kind string, duration time.Duration) { - By(fmt.Sprintf("validating no infinite reconcile loop for %s over %v", kind, duration)) +func validateNoReconcileLoop(controllerPodName, namespace, kind string, duration time.Duration, threshold int) { + By(fmt.Sprintf("validating no infinite reconcile loop for %s over %v (threshold: %d)", kind, duration, threshold)) // Sleep for duration + 5 seconds buffer to ensure any in-progress reconciles complete bufferDuration := duration + (5 * time.Second) @@ -89,8 +89,8 @@ func validateNoReconcileLoop(controllerPodName, namespace, kind string, duration reconcileCount := len(reconcileIDs) By(fmt.Sprintf("detected %d unique reconcile operations for %s in the last %v", reconcileCount, kind, duration)) - if reconcileCount > 0 { - Fail(fmt.Sprintf("Infinite reconcile loop detected for %s: %d unique reconcile operations in %v (expected: 0)", - kind, reconcileCount, duration)) + if reconcileCount > threshold { + Fail(fmt.Sprintf("Infinite reconcile loop detected for %s: %d unique reconcile operations in %v (expected: <= %d)", + kind, reconcileCount, duration, threshold)) } } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 8713aada..7eb62ac5 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -54,6 +54,7 @@ const ( defaultTimeout = 3 * time.Minute defaultPollPeriod = 5 * time.Second reconcileLoopValidationDuration = 30 * time.Second + reconcileLoopThreshold = 0 metricsServiceName = "falcon-operator-controller-manager-metrics-service" metricsRoleBindingName = "falcon-operator-metrics-binding" serviceAccountName = "falcon-operator-controller-manager" @@ -152,7 +153,7 @@ var _ = Describe("falcon", Ordered, func() { go func(k string) { defer wg.Done() defer GinkgoRecover() - validateNoReconcileLoop(controllerPodName, namespace, k, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, k, reconcileLoopValidationDuration, reconcileLoopThreshold) }(kind) } wg.Wait() @@ -170,7 +171,7 @@ var _ = Describe("falcon", Ordered, func() { } if kind != "" { - validateNoReconcileLoop(controllerPodName, namespace, kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kind, reconcileLoopValidationDuration, reconcileLoopThreshold) } }) @@ -266,7 +267,7 @@ var _ = Describe("falcon", Ordered, func() { } }) - Context("Falcon Operator", Label("FalconNodeSensor", "FalconAdmission", "FalconContainer", "FalconDeployment"), func() { + Context("Falcon Operator", Label("FalconNodeSensor", "FalconAdmission", "FalconImageAnalyzer", "FalconContainer", "FalconDeployment"), func() { It("should run successfully", func() { var err error @@ -738,6 +739,11 @@ var _ = Describe("falcon", Ordered, func() { return nil } EventuallyWithOffset(1, validateTolerationsInDeployment, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + if reconcileLoopCheck { + By("validating no reconcile loop after adding tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, reconcileLoopThreshold) + } }) It("should replace toleration when Key+Effect match but Value/Operator differ", func() { @@ -827,6 +833,11 @@ var _ = Describe("falcon", Ordered, func() { return nil } EventuallyWithOffset(1, validateReplacedToleration, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + if reconcileLoopCheck { + By("validating no reconcile loop after updating tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, reconcileLoopThreshold) + } }) It("should allow multiple tolerations with same Key but different Effect", func() { @@ -879,6 +890,11 @@ var _ = Describe("falcon", Ordered, func() { return nil } EventuallyWithOffset(1, validateBothEffects, defaultTimeout, defaultPollPeriod).Should(Succeed()) + + if reconcileLoopCheck { + By("validating no reconcile loop after adding same Key but different Effect tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, reconcileLoopThreshold) + } }) It("should cleanup successfully", func() { @@ -986,7 +1002,7 @@ var _ = Describe("falcon", Ordered, func() { if reconcileLoopCheck { By("validating no reconcile loop after toleration changes") - validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) } }) @@ -1011,7 +1027,7 @@ var _ = Describe("falcon", Ordered, func() { if reconcileLoopCheck { By("validating no reconcile loop after initial tolerations") - validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) } By("validating initial toleration with Exists operator") @@ -1048,7 +1064,7 @@ var _ = Describe("falcon", Ordered, func() { if reconcileLoopCheck { By("validating no reconcile loop after updating tolerations") - validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) } By("validating toleration was replaced with new Value and Operator") @@ -1115,7 +1131,7 @@ var _ = Describe("falcon", Ordered, func() { if reconcileLoopCheck { By("validating no reconcile loop after adding multiple tolerations") - validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) } By("validating both tolerations with same Key but different Effect exist") From ad56bd0a6c5bd0c27abbd336e2d66272f4e800e8 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:59:35 -0500 Subject: [PATCH 08/10] fix rollingupdate in iar --- api/falcon/v1alpha1/falconimageanalyzer_types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/falcon/v1alpha1/falconimageanalyzer_types.go b/api/falcon/v1alpha1/falconimageanalyzer_types.go index f61f7ad0..89df5970 100644 --- a/api/falcon/v1alpha1/falconimageanalyzer_types.go +++ b/api/falcon/v1alpha1/falconimageanalyzer_types.go @@ -90,7 +90,7 @@ type FalconImageAnalyzerConfigSpec struct { PriorityClass FalconImageAnalyzerPriorityClass `json:"priorityClass,omitempty"` // Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. - // +kubebuilder:default:={"rollingUpdate":{"maxUnavailable":0,"maxSurge":1}} + // +kubebuilder:default:={} // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Deployment Update Strategy",order=7 DepUpdateStrategy FalconImageAnalyzerUpdateStrategy `json:"updateStrategy,omitempty"` @@ -147,6 +147,7 @@ type FalconImageAnalyzerServiceAccount struct { type FalconImageAnalyzerUpdateStrategy struct { // RollingUpdate is used to specify the strategy used to roll out a deployment + // +kubebuilder:default:={"maxUnavailable":0,"maxSurge":1} // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Image Analyzer deployment update configuration",order=1,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:updateStrategy"} RollingUpdate appsv1.RollingUpdateDeployment `json:"rollingUpdate,omitempty"` } From 5b2d92457381e91690a2b2952adfa653e8f4663b Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:20:06 -0500 Subject: [PATCH 09/10] fix iar rolling update --- .../falcon-operator.clusterserviceversion.yaml | 2 +- .../falcon.crowdstrike.com_falconadmissions.yaml | 8 ++++---- ...falcon.crowdstrike.com_falcondeployments.yaml | 16 ++++++++-------- ...con.crowdstrike.com_falconimageanalyzers.yaml | 8 ++++---- ...falcon.crowdstrike.com_falcondeployments.yaml | 8 ++++---- ...con.crowdstrike.com_falconimageanalyzers.yaml | 8 ++++---- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/bundle/manifests/falcon-operator.clusterserviceversion.yaml b/bundle/manifests/falcon-operator.clusterserviceversion.yaml index 14464da6..191abed2 100644 --- a/bundle/manifests/falcon-operator.clusterserviceversion.yaml +++ b/bundle/manifests/falcon-operator.clusterserviceversion.yaml @@ -148,7 +148,7 @@ metadata: capabilities: Seamless Upgrades categories: Security,Monitoring containerImage: quay.io/crowdstrike/falcon-operator - createdAt: "2026-06-01T20:55:16Z" + createdAt: "2026-06-02T21:19:50Z" description: Falcon Operator installs CrowdStrike Falcon Sensors on the cluster features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" diff --git a/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml index 14fc385f..46b30aa4 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml @@ -686,14 +686,14 @@ spec: type: object type: array updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml b/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml index 8a1f42f0..0b338175 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml @@ -742,14 +742,14 @@ spec: type: object type: array updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: @@ -3771,14 +3771,14 @@ spec: description: Set the falcon image analyzer volume size limit. type: string updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml b/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml index 1dbfed2f..498db4b7 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -320,14 +320,14 @@ spec: description: Set the falcon image analyzer volume size limit. type: string updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml index 84abc72d..efdbdab0 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -3779,14 +3779,14 @@ spec: description: Set the falcon image analyzer volume size limit. type: string updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: diff --git a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml index 4ff50ede..892031eb 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -320,14 +320,14 @@ spec: description: Set the falcon image analyzer volume size limit. type: string updateStrategy: - default: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + default: {} description: Type of Deployment update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. properties: rollingUpdate: + default: + maxSurge: 1 + maxUnavailable: 0 description: RollingUpdate is used to specify the strategy used to roll out a deployment properties: From 05ed5fe300e2b708c571ff9bdfe99908f9908557 Mon Sep 17 00:00:00 2001 From: Greg Pontejos <242696964+gpontejos-cs@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:21:03 -0500 Subject: [PATCH 10/10] fix rolling update --- internal/controller/assets/deployment.go | 18 ++++++++++++ internal/controller/assets/deployment_test.go | 29 +++++++++++++++++++ .../falconimage_controller.go | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/controller/assets/deployment.go b/internal/controller/assets/deployment.go index b164f51d..ae1ddfc7 100644 --- a/internal/controller/assets/deployment.go +++ b/internal/controller/assets/deployment.go @@ -336,6 +336,7 @@ func ImageAnalyzerDeployment(name string, namespace string, component string, im Selector: &metav1.LabelSelector{ MatchLabels: labels, }, + Strategy: imageAnalyzerDepUpdateStrategy(falconImageAnalyzer), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, @@ -817,6 +818,23 @@ func admissionDepVolumeMounts(name string, registryCAConfigMapName string, conta return volumeMounts } +func imageAnalyzerDepUpdateStrategy(imageAnalyzer *falconv1alpha1.FalconImageAnalyzer) appsv1.DeploymentStrategy { + rollingUpdateSettings := appsv1.RollingUpdateDeployment{} + + if imageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxSurge != nil { + rollingUpdateSettings.MaxSurge = imageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxSurge + } + + if imageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxUnavailable != nil { + rollingUpdateSettings.MaxUnavailable = imageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxUnavailable + } + + return appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &rollingUpdateSettings, + } +} + func admissionDepUpdateStrategy(admission *falconv1alpha1.FalconAdmission) appsv1.DeploymentStrategy { rollingUpdateSettings := appsv1.RollingUpdateDeployment{} diff --git a/internal/controller/assets/deployment_test.go b/internal/controller/assets/deployment_test.go index 3a0244a6..bf54900a 100644 --- a/internal/controller/assets/deployment_test.go +++ b/internal/controller/assets/deployment_test.go @@ -94,6 +94,35 @@ func TestAdmissionDepUpdateStrategy(t *testing.T) { } } +// TestImageAnalyzerDepUpdateStrategy tests the Image Analyzer Deployment Update Strategy function +func TestImageAnalyzerDepUpdateStrategy(t *testing.T) { + falconImageAnalyzer := falconv1alpha1.FalconImageAnalyzer{} + + // Test RollingUpdate return value + want := appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + } + + falconImageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxUnavailable = &intstr.IntOrString{Type: intstr.Int, IntVal: 1} + falconImageAnalyzer.Spec.ImageAnalyzerConfig.DepUpdateStrategy.RollingUpdate.MaxSurge = &intstr.IntOrString{Type: intstr.Int, IntVal: 1} + + got := imageAnalyzerDepUpdateStrategy(&falconImageAnalyzer) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("imageAnalyzerDepUpdateStrategy() mismatch (-want +got): %s", diff) + } +} + + // testSideCarDeployment is a helper function to create a Deployment object for testing func testSideCarDeployment(name string, namespace string, component string, imageUri string, falconContainer *falconv1alpha1.FalconContainer) *appsv1.Deployment { replicas := int32(123) diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index 421913f4..6d5c4a73 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -353,7 +353,7 @@ func (r *FalconImageAnalyzerReconciler) reconcileImageAnalyzerDeployment(ctx con updated = true } - if !reflect.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Strategy.RollingUpdate, dep.Spec.Strategy.RollingUpdate) { log.V(1).Info("Updating FalconImageAnalyzer Deployment: RollingUpdate strategy changed", "old", existingDeployment.Spec.Strategy.RollingUpdate, "new", dep.Spec.Strategy.RollingUpdate)