diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index eecc7ff8..1b8f3d7b 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=18 NodeAffinity *corev1.NodeAffinity `json:"nodeAffinity,omitempty"` + + // Specifies tolerations for custom taints on the admission controller pods. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=19 + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` } type FalconAdmissionServiceAccount struct { @@ -346,3 +351,10 @@ func (ac *FalconAdmission) GetFalconSpec() FalconSensor { func (ac *FalconAdmission) SetFalconSpec(falconSpec FalconSensor) { ac.Spec.Falcon = falconSpec } + +func (ac *FalconAdmission) GetTolerations() *[]corev1.Toleration { + if ac.Spec.AdmissionConfig.Tolerations == nil { + return nil + } + return ac.Spec.AdmissionConfig.Tolerations +} diff --git a/api/falcon/v1alpha1/falconimageanalyzer_types.go b/api/falcon/v1alpha1/falconimageanalyzer_types.go index 8d601397..994852bd 100644 --- a/api/falcon/v1alpha1/falconimageanalyzer_types.go +++ b/api/falcon/v1alpha1/falconimageanalyzer_types.go @@ -126,6 +126,11 @@ type FalconImageAnalyzerConfigSpec struct { // +kubebuilder:default:={} // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="KAC Inter-communication Configuration",order=14 KAC FalconImageAnalyzerKACSpec `json:"kac,omitempty"` + + // Specifies tolerations for custom taints on the image analyzer pods. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=14 + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` } type FalconImageAnalyzerPriorityClass struct { @@ -265,3 +270,10 @@ func (fia *FalconImageAnalyzer) GetFalconSpec() FalconSensor { func (fia *FalconImageAnalyzer) SetFalconSpec(FalconSensor) { // noop } + +func (fia *FalconImageAnalyzer) GetTolerations() *[]corev1.Toleration { + if fia.Spec.ImageAnalyzerConfig.Tolerations == nil { + return nil + } + return fia.Spec.ImageAnalyzerConfig.Tolerations +} diff --git a/api/falcon/v1alpha1/zz_generated.deepcopy.go b/api/falcon/v1alpha1/zz_generated.deepcopy.go index d4808454..a93f01d8 100644 --- a/api/falcon/v1alpha1/zz_generated.deepcopy.go +++ b/api/falcon/v1alpha1/zz_generated.deepcopy.go @@ -399,6 +399,17 @@ func (in *FalconAdmissionConfigSpec) DeepCopyInto(out *FalconAdmissionConfigSpec *out = new(corev1.NodeAffinity) (*in).DeepCopyInto(*out) } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]corev1.Toleration) + if **in != nil { + in, out := *in, *out + *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. @@ -1063,6 +1074,18 @@ func (in *FalconImageAnalyzerConfigSpec) DeepCopyInto(out *FalconImageAnalyzerCo in.DepUpdateStrategy.DeepCopyInto(&out.DepUpdateStrategy) in.Exclusions.DeepCopyInto(&out.Exclusions) in.RegistryConfig.DeepCopyInto(&out.RegistryConfig) + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]corev1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + out.IARAgentService = in.IARAgentService out.KAC = in.KAC } diff --git a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml index 70cd6bce..b46e0256 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml @@ -643,6 +643,46 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + description: Specifies tolerations for custom taints on the admission + controller pods. + 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 and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + 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 ac2e2d25..39cd9c88 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -699,6 +699,46 @@ spec: type: integer x-kubernetes-int-or-string: true type: object + tolerations: + description: Specifies tolerations for custom taints on the + admission controller pods. + 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 and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + 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: @@ -3554,6 +3594,46 @@ spec: default: 20Gi description: Set the falcon image analyzer volume size limit. type: string + tolerations: + description: Specifies tolerations for custom taints on the + image analyzer pods. + 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 and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + 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_falconimageanalyzers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml index 212a4ad1..17e8a29c 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml @@ -313,6 +313,46 @@ spec: default: 20Gi description: Set the falcon image analyzer volume size limit. type: string + tolerations: + description: Specifies tolerations for custom taints on the image + analyzer pods. + 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 and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + 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/samples/falcon_v1alpha1_falconadmission_with_tolerations.yaml b/config/samples/falcon_v1alpha1_falconadmission_with_tolerations.yaml new file mode 100644 index 00000000..92e15a83 --- /dev/null +++ b/config/samples/falcon_v1alpha1_falconadmission_with_tolerations.yaml @@ -0,0 +1,39 @@ +apiVersion: falcon.crowdstrike.com/v1alpha1 +kind: FalconAdmission +metadata: + labels: + crowdstrike.com/component: sample + crowdstrike.com/created-by: falcon-operator + crowdstrike.com/instance: falcon-admission + crowdstrike.com/managed-by: kustomize + crowdstrike.com/name: falconadmission + crowdstrike.com/part-of: Falcon + crowdstrike.com/provider: crowdstrike + name: falcon-kac +spec: + falcon_api: + client_id: PLEASE_FILL_IN + client_secret: PLEASE_FILL_IN + cloud_region: autodiscover + registry: + type: crowdstrike + falcon: + trace: none + tags: + - admission_controller + admissionConfig: + # Example tolerations configuration + tolerations: + # Tolerate control-plane nodes + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + # Tolerate master nodes + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + # Custom toleration example + - key: "dedicated" + operator: "Equal" + value: "admission" + effect: "NoSchedule" \ No newline at end of file diff --git a/config/samples/falcon_v1alpha1_falconimageanalyzer_with_tolerations.yaml b/config/samples/falcon_v1alpha1_falconimageanalyzer_with_tolerations.yaml new file mode 100644 index 00000000..a9127684 --- /dev/null +++ b/config/samples/falcon_v1alpha1_falconimageanalyzer_with_tolerations.yaml @@ -0,0 +1,35 @@ +apiVersion: falcon.crowdstrike.com/v1alpha1 +kind: FalconImageAnalyzer +metadata: + labels: + crowdstrike.com/component: sample + crowdstrike.com/created-by: falcon-operator + crowdstrike.com/instance: falcon-image-analyzer + crowdstrike.com/managed-by: kustomize + crowdstrike.com/name: falconimageanalyzer + crowdstrike.com/part-of: Falcon + crowdstrike.com/provider: crowdstrike + name: falcon-image-analyzer +spec: + falcon_api: + client_id: PLEASE_FILL_IN + client_secret: PLEASE_FILL_IN + cloud_region: autodiscover + registry: + type: crowdstrike + imageAnalyzerConfig: + # Example tolerations configuration + tolerations: + # Tolerate control-plane nodes + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + # Tolerate master nodes + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + # Custom toleration example for dedicated image analyzer nodes + - key: "dedicated" + operator: "Equal" + value: "image-analyzer" + effect: "NoSchedule" \ No newline at end of file diff --git a/config/samples/falcon_v1alpha1_falconnodesensor_with_tolerations.yaml b/config/samples/falcon_v1alpha1_falconnodesensor_with_tolerations.yaml new file mode 100644 index 00000000..083c29d9 --- /dev/null +++ b/config/samples/falcon_v1alpha1_falconnodesensor_with_tolerations.yaml @@ -0,0 +1,37 @@ +apiVersion: falcon.crowdstrike.com/v1alpha1 +kind: FalconNodeSensor +metadata: + labels: + crowdstrike.com/component: sample + crowdstrike.com/created-by: falcon-operator + crowdstrike.com/instance: falcon-node-sensor + crowdstrike.com/managed-by: kustomize + crowdstrike.com/name: falconnodesensor + crowdstrike.com/part-of: Falcon + crowdstrike.com/provider: crowdstrike + name: falcon-node-sensor +spec: + falcon_api: + client_id: PLEASE_FILL_IN + client_secret: PLEASE_FILL_IN + cloud_region: autodiscover + node: + # Custom tolerations for the node sensor DaemonSet + tolerations: + # Tolerate control-plane nodes + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + # Tolerate master nodes + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + # Tolerate infra nodes + - key: "node-role.kubernetes.io/infra" + operator: "Exists" + effect: "NoSchedule" + # Custom toleration for dedicated security nodes + - key: "dedicated" + operator: "Equal" + value: "security" + effect: "NoSchedule" \ No newline at end of file diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index b79256dd..04e3e798 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -608,6 +608,11 @@ func (r *FalconAdmissionReconciler) reconcileAdmissionDeployment(ctx context.Con updated = true } + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, dep.Spec.Template.Spec.Tolerations) { + existingDeployment.Spec.Template.Spec.Tolerations = dep.Spec.Template.Spec.Tolerations + updated = true + } + if len(dep.Spec.Template.Spec.Containers) != len(existingDeployment.Spec.Template.Spec.Containers) { existingDeployment.Spec.Template.Spec.Containers = dep.Spec.Template.Spec.Containers updated = true diff --git a/internal/controller/assets/deployment.go b/internal/controller/assets/deployment.go index 49a26c92..83f4a8de 100644 --- a/internal/controller/assets/deployment.go +++ b/internal/controller/assets/deployment.go @@ -387,6 +387,7 @@ func ImageAnalyzerDeployment(name string, namespace string, component string, im NodeSelector: common.NodeSelector, Volumes: volumes, PriorityClassName: falconImageAnalyzer.Spec.ImageAnalyzerConfig.PriorityClass.Name, + Tolerations: getImageAnalyzerTolerations(falconImageAnalyzer), }, }, }, @@ -766,6 +767,7 @@ func AdmissionDeployment(name string, namespace string, component string, imageU PriorityClassName: common.FalconPriorityClassName, Containers: *kacContainers, Volumes: volumes, + Tolerations: getTolerations(falconAdmission), }, }, }, @@ -894,3 +896,17 @@ func getNodeAffinity(nodeAffinity *corev1.NodeAffinity) *corev1.Affinity { }, } } + +func getTolerations(falconAdmission *falconv1alpha1.FalconAdmission) []corev1.Toleration { + if falconAdmission.Spec.AdmissionConfig.Tolerations != nil { + return *falconAdmission.Spec.AdmissionConfig.Tolerations + } + return nil +} + +func getImageAnalyzerTolerations(falconImageAnalyzer *falconv1alpha1.FalconImageAnalyzer) []corev1.Toleration { + if falconImageAnalyzer.Spec.ImageAnalyzerConfig.Tolerations != nil { + return *falconImageAnalyzer.Spec.ImageAnalyzerConfig.Tolerations + } + return nil +} diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index f23b6d09..699effb7 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -345,6 +345,11 @@ func (r *FalconImageAnalyzerReconciler) reconcileImageAnalyzerDeployment(ctx con updated = true } + if !equality.Semantic.DeepEqual(existingDeployment.Spec.Template.Spec.Tolerations, dep.Spec.Template.Spec.Tolerations) { + existingDeployment.Spec.Template.Spec.Tolerations = dep.Spec.Template.Spec.Tolerations + updated = true + } + if updated { if err := k8sutils.Update(r.Client, ctx, req, log, falconImageAnalyzer, &falconImageAnalyzer.Status, existingDeployment); err != nil { return err diff --git a/test/e2e/cr_config_test.go b/test/e2e/cr_config_test.go index 8f3cd796..e92a6687 100644 --- a/test/e2e/cr_config_test.go +++ b/test/e2e/cr_config_test.go @@ -38,6 +38,12 @@ var ( componentName: "admission_controller", } iarConfig = crConfig{ + kind: "FalconImageAnalyzer", + namespace: "falcon-iar", + metadataName: "falcon-image-analyzer", + componentName: "falcon-imageanalyzer", + } + iarDeploymentConfig = crConfig{ kind: "FalconImageAnalyzer", namespace: "falcon-iar", metadataName: "falcon-image-analyzer", diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 3e7952c0..7303e662 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -332,6 +332,30 @@ var _ = Describe("falcon", Ordered, func() { }) }) + Context("Falcon Node Sensor with Tolerations", func() { + manifest := "./config/samples/falcon_v1alpha1_falconnodesensor_with_tolerations.yaml" + It("should deploy successfully with tolerations", func() { + updateManifestApiCreds(manifest) + nodeConfig.manageCrdInstance(crApply, manifest) + nodeConfig.validateRunningStatus(shouldBeRunning) + nodeConfig.validateCrStatus() + + // Validate tolerations on DaemonSet + expectedTolerations := []ExpectedToleration{ + {Key: "node-role.kubernetes.io/control-plane", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "node-role.kubernetes.io/master", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "node-role.kubernetes.io/infra", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "dedicated", Operator: "Equal", Value: "security", Effect: "NoSchedule"}, + } + nodeConfig.validateDaemonSetTolerations("falcon-node-sensor", expectedTolerations) + }) + It("should cleanup successfully", func() { + nodeConfig.manageCrdInstance(crDelete, manifest) + nodeConfig.validateRunningStatus(shouldBeTerminated) + nodeConfig.waitForNamespaceDeletion() + }) + }) + Context("Falcon Admission Controller", func() { manifest := "./config/samples/falcon_v1alpha1_falconadmission.yaml" It("should deploy successfully", func() { @@ -379,6 +403,73 @@ var _ = Describe("falcon", Ordered, func() { }) }) + Context("Falcon Admission Controller with Tolerations", func() { + manifest := "./config/samples/falcon_v1alpha1_falconadmission_with_tolerations.yaml" + It("should deploy successfully with tolerations", func() { + updateManifestApiCreds(manifest) + kacConfig.manageCrdInstance(crApply, manifest) + kacConfig.validateRunningStatus(shouldBeRunning) + kacConfig.validateCrStatus() + + // Get deployment name and validate tolerations + deploymentName, err := kacConfig.getDeploymentName() + Expect(err).NotTo(HaveOccurred()) + + expectedTolerations := []ExpectedToleration{ + {Key: "node-role.kubernetes.io/control-plane", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "node-role.kubernetes.io/master", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "dedicated", Operator: "Equal", Value: "admission", Effect: "NoSchedule"}, + } + kacConfig.validateDeploymentTolerations(deploymentName, expectedTolerations) + }) + It("should cleanup successfully", func() { + kacConfig.manageCrdInstance(crDelete, manifest) + kacConfig.validateRunningStatus(shouldBeTerminated) + kacConfig.waitForNamespaceDeletion() + }) + }) + + Context("Falcon Image Analyzer", func() { + manifest := "./config/samples/falcon_v1alpha1_falconimageanalyzer.yaml" + It("should deploy successfully", func() { + updateManifestApiCreds(manifest) + iarConfig.manageCrdInstance(crApply, manifest) + iarConfig.validateRunningStatus(shouldBeRunning) + iarConfig.validateCrStatus() + }) + It("should cleanup successfully", func() { + iarConfig.manageCrdInstance(crDelete, manifest) + iarConfig.validateRunningStatus(shouldBeTerminated) + iarConfig.waitForNamespaceDeletion() + }) + }) + + Context("Falcon Image Analyzer with Tolerations", func() { + manifest := "./config/samples/falcon_v1alpha1_falconimageanalyzer_with_tolerations.yaml" + It("should deploy successfully with tolerations", func() { + updateManifestApiCreds(manifest) + iarConfig.manageCrdInstance(crApply, manifest) + iarConfig.validateRunningStatus(shouldBeRunning) + iarConfig.validateCrStatus() + + // Get deployment name and validate tolerations + deploymentName, err := iarConfig.getDeploymentName() + Expect(err).NotTo(HaveOccurred()) + + expectedTolerations := []ExpectedToleration{ + {Key: "node-role.kubernetes.io/control-plane", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "node-role.kubernetes.io/master", Operator: "Exists", Effect: "NoSchedule"}, + {Key: "dedicated", Operator: "Equal", Value: "image-analyzer", Effect: "NoSchedule"}, + } + iarConfig.validateDeploymentTolerations(deploymentName, expectedTolerations) + }) + It("should cleanup successfully", func() { + iarConfig.manageCrdInstance(crDelete, manifest) + iarConfig.validateRunningStatus(shouldBeTerminated) + iarConfig.waitForNamespaceDeletion() + }) + }) + Context("Falcon Sidecar Sensor", func() { manifest := "./config/samples/falcon_v1alpha1_falconcontainer.yaml" It("should deploy successfully", func() { @@ -420,17 +511,17 @@ var _ = Describe("falcon", Ordered, func() { kacConfig.validateCrStatus() sidecarConfig.validateRunningStatus(shouldBeRunning) sidecarConfig.validateCrStatus() - iarConfig.validateRunningStatus(shouldBeRunning) - iarConfig.validateCrStatus() + iarDeploymentConfig.validateRunningStatus(shouldBeRunning) + iarDeploymentConfig.validateCrStatus() }) It("should cleanup successfully", func() { falconDeploymentConfig.manageCrdInstance(crDelete, manifest) kacConfig.validateRunningStatus(shouldBeTerminated) sidecarConfig.validateRunningStatus(shouldBeTerminated) - iarConfig.validateRunningStatus(shouldBeTerminated) + iarDeploymentConfig.validateRunningStatus(shouldBeTerminated) sidecarConfig.waitForNamespaceDeletion() kacConfig.waitForNamespaceDeletion() - iarConfig.waitForNamespaceDeletion() + iarDeploymentConfig.waitForNamespaceDeletion() }) }) @@ -443,17 +534,17 @@ var _ = Describe("falcon", Ordered, func() { kacConfig.validateCrStatus() nodeConfig.validateRunningStatus(shouldBeRunning) nodeConfig.validateCrStatus() - iarConfig.validateRunningStatus(shouldBeRunning) - iarConfig.validateCrStatus() + iarDeploymentConfig.validateRunningStatus(shouldBeRunning) + iarDeploymentConfig.validateCrStatus() }) It("should cleanup successfully", func() { falconDeploymentConfig.manageCrdInstance(crDelete, manifest) kacConfig.validateRunningStatus(shouldBeTerminated) nodeConfig.validateRunningStatus(shouldBeTerminated) - iarConfig.validateRunningStatus(shouldBeTerminated) + iarDeploymentConfig.validateRunningStatus(shouldBeTerminated) nodeConfig.waitForNamespaceDeletion() kacConfig.waitForNamespaceDeletion() - iarConfig.waitForNamespaceDeletion() + iarDeploymentConfig.waitForNamespaceDeletion() }) }) @@ -466,18 +557,18 @@ var _ = Describe("falcon", Ordered, func() { kacConfig.validateCrStatus() nodeConfig.validateRunningStatus(shouldBeRunning) nodeConfig.validateCrStatus() - iarConfig.validateRunningStatus(shouldBeRunning) - iarConfig.validateCrStatus() + iarDeploymentConfig.validateRunningStatus(shouldBeRunning) + iarDeploymentConfig.validateCrStatus() }) It("should cleanup successfully", func() { falconDeploymentConfig.manageCrdInstance(crDelete, manifest) kacConfig.validateRunningStatus(shouldBeTerminated) nodeConfig.validateRunningStatus(shouldBeTerminated) - iarConfig.validateRunningStatus(shouldBeTerminated) + iarDeploymentConfig.validateRunningStatus(shouldBeTerminated) secretConfig.deleteNamespace() nodeConfig.waitForNamespaceDeletion() kacConfig.waitForNamespaceDeletion() - iarConfig.waitForNamespaceDeletion() + iarDeploymentConfig.waitForNamespaceDeletion() secretConfig.waitForNamespaceDeletion() }) }) diff --git a/test/e2e/tolerations_test.go b/test/e2e/tolerations_test.go new file mode 100644 index 00000000..5cd1fc80 --- /dev/null +++ b/test/e2e/tolerations_test.go @@ -0,0 +1,163 @@ +package e2e + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + + "github.com/crowdstrike/falcon-operator/test/utils" + //nolint:golint + //nolint:revive + . "github.com/onsi/ginkgo/v2" + + //nolint:golint + //nolint:revive + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +// Toleration represents the expected toleration to validate +type ExpectedToleration struct { + Key string + Operator string + Value string + Effect string +} + +// validateDeploymentTolerations checks if a deployment has the expected tolerations +func (cr crConfig) validateDeploymentTolerations(deploymentName string, expectedTolerations []ExpectedToleration) { + By(fmt.Sprintf("validating tolerations for deployment %s in namespace %s", deploymentName, cr.namespace)) + + checkTolerations := func() error { + // Get the deployment as JSON + cmd := exec.Command("kubectl", "get", "deployment", deploymentName, + "-n", cr.namespace, "-o", "json") + output, err := utils.Run(cmd) + if err != nil { + return fmt.Errorf("failed to get deployment: %v", err) + } + + // Parse the deployment + var deployment appsv1.Deployment + err = json.Unmarshal(output, &deployment) + if err != nil { + return fmt.Errorf("failed to parse deployment JSON: %v", err) + } + + // Get the tolerations from the pod spec + podTolerations := deployment.Spec.Template.Spec.Tolerations + + // Verify each expected toleration exists + for _, expected := range expectedTolerations { + found := false + for _, toleration := range podTolerations { + if matchesToleration(toleration, expected) { + found = true + break + } + } + if !found { + return fmt.Errorf("expected toleration not found: Key=%s, Operator=%s, Value=%s, Effect=%s", + expected.Key, expected.Operator, expected.Value, expected.Effect) + } + } + + fmt.Printf("Found %d tolerations in deployment %s\n", len(podTolerations), deploymentName) + for _, t := range podTolerations { + fmt.Printf(" - Key: %s, Operator: %s, Value: %s, Effect: %s\n", + t.Key, t.Operator, t.Value, t.Effect) + } + + return nil + } + + EventuallyWithOffset(1, checkTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) +} + +// matchesToleration checks if a toleration matches the expected values +func matchesToleration(actual corev1.Toleration, expected ExpectedToleration) bool { + // Handle empty operator (defaults to Equal) + actualOperator := string(actual.Operator) + if actualOperator == "" { + actualOperator = "Equal" + } + expectedOperator := expected.Operator + if expectedOperator == "" { + expectedOperator = "Equal" + } + + return actual.Key == expected.Key && + actualOperator == expectedOperator && + actual.Value == expected.Value && + string(actual.Effect) == expected.Effect +} + +// validateDaemonSetTolerations checks if a daemonset has the expected tolerations +func (cr crConfig) validateDaemonSetTolerations(daemonsetName string, expectedTolerations []ExpectedToleration) { + By(fmt.Sprintf("validating tolerations for daemonset %s in namespace %s", daemonsetName, cr.namespace)) + + checkTolerations := func() error { + // Get the daemonset as JSON + cmd := exec.Command("kubectl", "get", "daemonset", daemonsetName, + "-n", cr.namespace, "-o", "json") + output, err := utils.Run(cmd) + if err != nil { + return fmt.Errorf("failed to get daemonset: %v", err) + } + + // Parse the daemonset + var daemonset appsv1.DaemonSet + err = json.Unmarshal(output, &daemonset) + if err != nil { + return fmt.Errorf("failed to parse daemonset JSON: %v", err) + } + + // Get the tolerations from the pod spec + podTolerations := daemonset.Spec.Template.Spec.Tolerations + + // Verify each expected toleration exists + for _, expected := range expectedTolerations { + found := false + for _, toleration := range podTolerations { + if matchesToleration(toleration, expected) { + found = true + break + } + } + if !found { + return fmt.Errorf("expected toleration not found: Key=%s, Operator=%s, Value=%s, Effect=%s", + expected.Key, expected.Operator, expected.Value, expected.Effect) + } + } + + fmt.Printf("Found %d tolerations in daemonset %s\n", len(podTolerations), daemonsetName) + for _, t := range podTolerations { + fmt.Printf(" - Key: %s, Operator: %s, Value: %s, Effect: %s\n", + t.Key, t.Operator, t.Value, t.Effect) + } + + return nil + } + + EventuallyWithOffset(1, checkTolerations, defaultTimeout, defaultPollPeriod).Should(Succeed()) +} + +// getDeploymentName retrieves the deployment name for a given CR +func (cr crConfig) getDeploymentName() (string, error) { + cmd := exec.Command("kubectl", "get", "deployments", + "-n", cr.namespace, "-o", "jsonpath={.items[0].metadata.name}") + output, err := utils.Run(cmd) + if err != nil { + return "", fmt.Errorf("failed to get deployment name: %v", err) + } + + deploymentName := strings.TrimSpace(string(output)) + if deploymentName == "" { + return "", fmt.Errorf("no deployment found in namespace %s", cr.namespace) + } + + return deploymentName, nil +}