diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index 654f237d0..e6526494f 100644 --- a/api/falcon/v1alpha1/falconadmission_types.go +++ b/api/falcon/v1alpha1/falconadmission_types.go @@ -198,13 +198,18 @@ 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"` // 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=20 + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } type FalconAdmissionServiceAccount struct { @@ -215,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/api/falcon/v1alpha1/falconimageanalyzer_types.go b/api/falcon/v1alpha1/falconimageanalyzer_types.go index e82cc9be5..89df59707 100644 --- a/api/falcon/v1alpha1/falconimageanalyzer_types.go +++ b/api/falcon/v1alpha1/falconimageanalyzer_types.go @@ -55,9 +55,14 @@ 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 Falcon Image Analyzer Sensor. + // +kubebuilder:default:={} + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=9 + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } type FalconImageAnalyzerConfigSpec struct { @@ -85,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"` @@ -142,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"` } diff --git a/api/falcon/v1alpha1/zz_generated.deepcopy.go b/api/falcon/v1alpha1/zz_generated.deepcopy.go index d11d124ac..aea42a74e 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/bundle/manifests/falcon-operator.clusterserviceversion.yaml b/bundle/manifests/falcon-operator.clusterserviceversion.yaml index 2496a0dfb..191abed26 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-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" @@ -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 @@ -2398,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/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml index 745dcc51f..46b30aa48 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falconadmissions.yaml @@ -643,15 +643,57 @@ 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: - 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 8f9d943c2..0b338175d 100644 --- a/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml +++ b/bundle/manifests/falcon.crowdstrike.com_falcondeployments.yaml @@ -699,15 +699,57 @@ 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: - 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: @@ -3729,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: @@ -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 ef9c55363..498db4b71 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: @@ -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_falconadmissions.yaml b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml index 3c6c4ff2e..3bf82426c 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml @@ -643,15 +643,57 @@ 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: - 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 ed934b4b8..efdbdab09 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falcondeployments.yaml @@ -699,15 +699,57 @@ 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: - 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: @@ -3737,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: @@ -3792,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: |- @@ -4028,6 +4071,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/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconimageanalyzers.yaml index 3e3bccd17..892031eb6 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: @@ -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/manager/kustomization.yaml b/config/manager/kustomization.yaml index 598fb81d8..f03170420 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 diff --git a/config/manifests/bases/falcon-operator.clusterserviceversion.yaml b/config/manifests/bases/falcon-operator.clusterserviceversion.yaml index fedca6b8c..ab76a290a 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 88e6dd419..7f97cf4c4 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: @@ -7371,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: |- @@ -7607,6 +7692,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: @@ -8759,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: |- @@ -8995,6 +9123,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/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index 3138ca011..e514ec20b 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 } @@ -592,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) @@ -685,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, @@ -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 existingTol.Key == specTol.Key && existingTol.Effect == specTol.Effect { + found = true + break + } + } + + if !found { + mergedTolerations = append(mergedTolerations, existingTol) + } + } + + 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) + 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 6dae8e3d4..a3549e14f 100644 --- a/internal/controller/admission/falconadmission_controller_test.go +++ b/internal/controller/admission/falconadmission_controller_test.go @@ -1225,5 +1225,458 @@ 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()) + }) + + 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/admission/rbac.go b/internal/controller/admission/rbac.go index e7246714a..590e125f2 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 a484b353d..ae1ddfc74 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, @@ -388,6 +389,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 +771,7 @@ func AdmissionDeployment(name string, namespace string, component string, imageU PriorityClassName: common.FalconPriorityClassName, Containers: *kacContainers, Volumes: volumes, + Tolerations: falconAdmission.Spec.AdmissionConfig.Tolerations, }, }, }, @@ -815,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 3a0244a69..bf54900ac 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 e20d452a4..6d5c4a738 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 !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) + 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 existingTol.Key == specTol.Key && existingTol.Effect == specTol.Effect { + found = true + break + } + } + + if !found { + mergedTolerations = append(mergedTolerations, existingTol) + } + } + + 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) + 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 248c31c0b..7e8c287a3 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller_test.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller_test.go @@ -773,5 +773,450 @@ 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, + }), + )) + }) + + 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/cr_config_test.go b/test/e2e/cr_config_test.go index eb3e2ad0e..af7f076fb 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 3a315230c..8da55f007 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_helpers_test.go b/test/e2e/e2e_helpers_test.go index f23c1b82f..dd8848ef4 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 524e04d6a..7eb62ac52 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: @@ -52,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" @@ -150,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() @@ -168,7 +171,7 @@ var _ = Describe("falcon", Ordered, func() { } if kind != "" { - validateNoReconcileLoop(controllerPodName, namespace, kind, reconcileLoopValidationDuration) + validateNoReconcileLoop(controllerPodName, namespace, kind, reconcileLoopValidationDuration, reconcileLoopThreshold) } }) @@ -264,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 @@ -697,4 +700,472 @@ var _ = Describe("falcon", Ordered, func() { secretConfig.waitForNamespaceDeletion() }) }) + + 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") + 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()) + + 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() { + By("loading and modifying the FalconImageAnalyzer manifest with initial toleration") + var iar falconv1alpha1.FalconImageAnalyzer + err := loadManifest(manifest, &iar) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Set initial toleration with Exists operator + iar.Spec.Tolerations = []corev1.Toleration{ + { + 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 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=='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()) + + 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() { + 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 { + // 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')]}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + + // 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) + } + + // 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 + } + 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() { + iarConfig.manageCrdInstance(crDelete, manifest) + iarConfig.validateRunningStatus(shouldBeTerminated) + iarConfig.waitForNamespaceDeletion() + }) + }) + + Context("Falcon Admission Controller Tolerations", Label("FalconAdmission"), func() { + manifest := "./config/samples/falcon_v1alpha1_falconadmission.yaml" + It("should deploy 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()) + + if reconcileLoopCheck { + By("validating no reconcile loop after toleration changes") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) + } + }) + + 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()) + + if reconcileLoopCheck { + By("validating no reconcile loop after initial tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) + } + + 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()) + + if reconcileLoopCheck { + By("validating no reconcile loop after updating tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) + } + + 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()) + + if reconcileLoopCheck { + By("validating no reconcile loop after adding multiple tolerations") + validateNoReconcileLoop(controllerPodName, namespace, kacConfig.kind, reconcileLoopValidationDuration, 3) + } + + By("validating both tolerations with same Key but different Effect exist") + validateBothEffects := func() error { + // 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')]}") + output, err := utils.Run(cmd) + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + + // 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) + } + + // 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 + } + EventuallyWithOffset(1, validateBothEffects, defaultTimeout, defaultPollPeriod).Should(Succeed()) + }) + + It("should cleanup successfully", func() { + kacConfig.manageCrdInstance(crDelete, manifest) + kacConfig.validateRunningStatus(shouldBeTerminated) + kacConfig.waitForNamespaceDeletion() + }) + }) })