Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ $ make deploy
or build a catalog and deploy from OLM:
```
$ make catalog && make catalog-deploy
```
```

### FileIntegrity API:

Expand All @@ -49,6 +49,7 @@ spec:
- key: "myNode"
operator: "Exists"
effect: "NoSchedule"
priorityClassName: "system-cluster-critical"
config:
name: "myconfig"
namespace: "openshift-file-integrity"
Expand All @@ -62,10 +63,11 @@ status:
In the `spec`:
* **nodeSelector**: Selector for nodes to schedule the scan instances on.
* **tolerations**: Specify tolerations to schedule on nodes with custom taints. When not specified, a default toleration allowing running on master and infra nodes is applied.
* **priorityClassName**: (Optional) Specifies the `PriorityClass` for the pods created by the operator. If the PriorityClass is invalid or not found, it will be ignored and cleared from the spec.
* **config**: Point to a ConfigMap containing an AIDE configuration to use instead of the CoreOS optimized default. See "Applying an AIDE config" below.
* **config.gracePeriod**: The number of seconds to pause in between AIDE integrity checks. Frequent AIDE checks on a node may be resource intensive, so it can be useful to specify a longer interval. Defaults to 900 (15 mins).
* **config.maxBackups**: The maximum number of AIDE database and log backups (leftover from the re-init process) to keep on a node. Older backups beyond this number are automatically pruned by the daemon. Defaults to 5.
* **config.initialDelay**: An optional field. The number of seconds to wait before starting the first AIDE integrity check. Defaults to 0.
* **config.initialDelay**: An optional field. The number of seconds to wait before starting the first AIDE integrity check. Defaults to 0.

In the `status`:
* **phase**: The running status of the `FileIntegrity` instance. Can be `Initializing`, `Pending`, or `Active`. `Initializing` is displayed if the FileIntegrity is currently initializing or re-initializing the AIDE database, `Pending` if the FileIntegrity deployment is still being created, and `Active` if the scans are active and ongoing. For node scan results, see the `FileIntegrityNodeStatus` objects explained below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ metadata:
]
capabilities: Seamless Upgrades
categories: Monitoring,Security
createdAt: "2024-11-21T18:02:08Z"
createdAt: "2026-01-12T12:08:39Z"
olm.skipRange: '>=1.0.0 <1.3.6'
operatorframework.io/cluster-monitoring: "true"
operatorframework.io/suggested-namespace: openshift-file-integrity
Expand Down Expand Up @@ -67,6 +67,14 @@ spec:
- get
- list
- watch
- apiGroups:
- scheduling.k8s.io
resources:
- priorityclasses
verbs:
- get
- list
- watch
serviceAccountName: file-integrity-operator
deployments:
- name: file-integrity-operator
Expand Down Expand Up @@ -320,4 +328,3 @@ spec:
- image: quay.io/file-integrity-operator/file-integrity-operator:latest
name: operator
version: 1.3.6
replaces: file-integrity-operator.v1.3.5
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ spec:
additionalProperties:
type: string
type: object
priorityClassName:
description: |-
Specifies the PriorityClass to use for the pods created by the operator.
This is an optional field. If PriorityClass is invalid or not found,
it will be ignored and cleared from the spec.
type: string
tolerations:
default:
- effect: NoSchedule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ spec:
additionalProperties:
type: string
type: object
priorityClassName:
description: |-
Specifies the PriorityClass to use for the pods created by the operator.
This is an optional field. If PriorityClass is invalid or not found,
it will be ignored and cleared from the spec.
type: string
tolerations:
default:
- effect: NoSchedule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ metadata:
]
capabilities: Seamless Upgrades
categories: Monitoring,Security
olm.skipRange: '>=1.0.0 <1.3.5-dev'
olm.skipRange: '>=1.0.0 <1.3.6'
operatorframework.io/cluster-monitoring: "true"
operatorframework.io/suggested-namespace: openshift-file-integrity
operators.openshift.io/infrastructure-features: '["disconnected", "fips"]'
Expand Down
8 changes: 8 additions & 0 deletions config/rbac/operator_clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ rules:
- get
- list
- watch
- apiGroups:
- scheduling.k8s.io
resources:
- priorityclasses
verbs:
- get
- list
- watch
4 changes: 4 additions & 0 deletions pkg/apis/fileintegrity/v1alpha1/fileintegrity_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type FileIntegritySpec struct {
// Specifies tolerations for custom taints. Defaults to allowing scheduling on master and infra nodes.
// +kubebuilder:default={{key: "node-role.kubernetes.io/master", operator: "Exists", effect: "NoSchedule"},{key: "node-role.kubernetes.io/infra", operator: "Exists", effect: "NoSchedule"}}
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
// Specifies the PriorityClass to use for the pods created by the operator.
// This is an optional field. If PriorityClass is invalid or not found,
// it will be ignored and cleared from the spec.
PriorityClassName string `json:"priorityClassName,omitempty"`
}

// FileIntegrityConfig defines the name, namespace, and data key for an AIDE config to use for integrity checking.
Expand Down
70 changes: 68 additions & 2 deletions pkg/controller/fileintegrity/fileintegrity_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/openshift/file-integrity-operator/pkg/common"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
kerr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -44,7 +45,12 @@ func AddFileIntegrityController(mgr manager.Manager, met *metrics.Metrics) error

// newReconciler returns a new reconcile.Reconciler
func newFileIntegrityReconciler(mgr manager.Manager, met *metrics.Metrics) reconcile.Reconciler {
return &FileIntegrityReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Metrics: met}
return &FileIntegrityReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Metrics: met,
Recorder: mgr.GetEventRecorderFor("fileintegrityctrl"),
}
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
Expand Down Expand Up @@ -359,6 +365,29 @@ func (r *FileIntegrityReconciler) reconcileUserConfig(instance *v1alpha1.FileInt
return true, nil
}

// validatePriorityClass checks if the specified PriorityClass exists and is valid.
// Returns true if valid (or empty), false otherwise. Following the pattern of compliance-operator,
// if PriorityClass is invalid or not found, it will be ignored.
func (r *FileIntegrityReconciler) validatePriorityClass(ctx context.Context, pcName string, logger logr.Logger) bool {
if pcName == "" {
return true // Empty is valid (uses default)
}

pc := &schedulingv1.PriorityClass{}
err := r.Client.Get(ctx, types.NamespacedName{Name: pcName}, pc)
if err != nil {
if kerr.IsNotFound(err) {
logger.Info("PriorityClass not found, ignoring", "priorityClassName", pcName)
return false
}
logger.Error(err, "Error checking PriorityClass, ignoring", "priorityClassName", pcName)
return false
}

logger.Info("PriorityClass validated successfully", "priorityClassName", pcName, "value", pc.Value)
return true
}

// gets the image of the first container in the operator deployment spec. We expect this to be the deployment named
// file-integrity-operator in the openshift-file-integrity namespace.
func (r *FileIntegrityReconciler) getOperatorDeploymentImage() (string, error) {
Expand Down Expand Up @@ -395,6 +424,29 @@ func (r *FileIntegrityReconciler) FileIntegrityControllerReconcile(request recon
return reconcile.Result{}, err
}

// Validate PriorityClass if specified. If invalid or not found, it will be ignored.
if instance.Spec.PriorityClassName != "" {
if !r.validatePriorityClass(context.TODO(), instance.Spec.PriorityClassName, reqLogger) {
reqLogger.Info("Invalid or non-existent PriorityClass specified, will be ignored",
"priorityClassName", instance.Spec.PriorityClassName)
// Generate Warning event for the user
r.Recorder.Eventf(instance, corev1.EventTypeWarning, "PriorityClass",
"Error while getting priority class '%s', PriorityClass not found or invalid",
instance.Spec.PriorityClassName)
// Clear the PriorityClassName to avoid DaemonSet pod creation failures
instanceCopy := instance.DeepCopy()
instanceCopy.Spec.PriorityClassName = ""
if err := r.Client.Update(context.TODO(), instanceCopy); err != nil {
reqLogger.Error(err, "Error clearing invalid PriorityClassName")
// Continue anyway, the daemonset will fail to create pods but won't block reconciliation
} else {
reqLogger.Info("Cleared invalid PriorityClassName from FileIntegrity spec")
// Requeue to reconcile with the updated spec
return reconcile.Result{Requeue: true}, nil
}
}
}

// Get the operator image to later set as the daemonSet image. They need to always match before we deprecate RELATED_IMAGE_OPERATOR.
operatorImage, err := r.getOperatorDeploymentImage()
if err != nil {
Expand Down Expand Up @@ -538,9 +590,10 @@ func (r *FileIntegrityReconciler) FileIntegrityControllerReconcile(request recon
imgNeedsUpdate := updateDSImage(dsCopy, operatorImage, reqLogger)
nsNeedsUpdate := updateDSNodeSelector(dsCopy, instance, reqLogger)
tolsNeedsUpdate := updateDSTolerations(dsCopy, instance, reqLogger)
pcNeedsUpdate := updateDSPriorityClassName(dsCopy, instance, reqLogger)
volsNeedUpdate := updateDSContainerVolumes(dsCopy, instance, operatorImage, reqLogger)

if argsNeedUpdate || imgNeedsUpdate || nsNeedsUpdate || tolsNeedsUpdate || volsNeedUpdate || scriptsUpdated {
if argsNeedUpdate || imgNeedsUpdate || nsNeedsUpdate || tolsNeedsUpdate || pcNeedsUpdate || volsNeedUpdate || scriptsUpdated {
if err := r.Client.Update(context.TODO(), dsCopy); err != nil {
return reconcile.Result{}, err
}
Expand Down Expand Up @@ -602,6 +655,17 @@ func updateDSTolerations(currentDS *appsv1.DaemonSet, fi *v1alpha1.FileIntegrity
return needsUpdate
}

func updateDSPriorityClassName(currentDS *appsv1.DaemonSet, fi *v1alpha1.FileIntegrity, logger logr.Logger) bool {
pcRef := &currentDS.Spec.Template.Spec.PriorityClassName
expectedPC := fi.Spec.PriorityClassName
needsUpdate := *pcRef != expectedPC
if needsUpdate {
logger.Info("FileIntegrity needed priorityClassName update")
*pcRef = expectedPC
}
return needsUpdate
}

// Returns true when the daemon pod args derived from the FileIntegrity object differ from the current DS.
// Returns false if there was no difference.
// If an update is needed, this will update the arguments from the given DaemonSet
Expand Down Expand Up @@ -730,6 +794,7 @@ func reinitAideDaemonset(reinitDaemonSetName string, fi *v1alpha1.FileIntegrity,
Spec: corev1.PodSpec{
NodeSelector: selector.MatchLabels,
Tolerations: fi.Spec.Tolerations,
PriorityClassName: fi.Spec.PriorityClassName,
ServiceAccountName: common.OperatorServiceAccountName,
InitContainers: []corev1.Container{
{
Expand Down Expand Up @@ -855,6 +920,7 @@ func aideDaemonset(dsName string, fi *v1alpha1.FileIntegrity, operatorImage stri
Spec: corev1.PodSpec{
NodeSelector: fi.Spec.NodeSelector,
Tolerations: fi.Spec.Tolerations,
PriorityClassName: fi.Spec.PriorityClassName,
ServiceAccountName: common.DaemonServiceAccountName,
InitContainers: []corev1.Container{
{
Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/fileintegrity/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/openshift/file-integrity-operator/pkg/controller/metrics"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlLog "sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -32,6 +33,7 @@ type FileIntegrityReconciler struct {
client.Client
*runtime.Scheme
*metrics.Metrics
Recorder record.EventRecorder
}

// These are perms for all controllers.
Expand All @@ -48,6 +50,7 @@ type FileIntegrityReconciler struct {
//+kubebuilder:rbac:groups=fileintegrity.openshift.io,resources=fileintegrities/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=fileintegrity.openshift.io,resources=fileintegrities/finalizers,verbs=update
//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update
//+kubebuilder:rbac:groups=scheduling.k8s.io,resources=priorityclasses,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
Expand Down
66 changes: 66 additions & 0 deletions tests/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,72 @@ func TestFileIntegrityTolerations(t *testing.T) {
assertSingleNodeConditionIsSuccess(t, f, testIntegrityNamePrefix+"-tolerations", taintedNode, namespace, 2*time.Second, 5*time.Minute)
}

func TestFileIntegrityPriorityClassName(t *testing.T) {
f, testctx, namespace := setupPriorityClassTest(t, testIntegrityNamePrefix+"-priorityclass")
defer testctx.Cleanup()
defer func() {
if err := cleanNodes(f, namespace); err != nil {
t.Fatal(err)
}
if err := resetBundleTestMetrics(f, namespace); err != nil {
t.Fatal(err)
}
}()
defer logContainerOutput(t, f, namespace, testIntegrityNamePrefix+"-priorityclass")

// wait to go active.
err := waitForScanStatus(t, f, namespace, testIntegrityNamePrefix+"-priorityclass", v1alpha1.PhaseActive)
if err != nil {
t.Errorf("Timeout waiting for scan status")
}

t.Log("Verifying that the DaemonSet pods have the correct priorityClassName")
dsName := common.DaemonSetName(testIntegrityNamePrefix + "-priorityclass")
if err := verifyDaemonSetPriorityClassName(t, f, namespace, dsName, "system-node-critical"); err != nil {
t.Errorf("Failed to verify priorityClassName: %v", err)
}
}

func TestFileIntegrityInvalidPriorityClassName(t *testing.T) {
f, testctx, namespace := setupInvalidPriorityClassTest(t, testIntegrityNamePrefix+"-invalidpc")
defer testctx.Cleanup()
defer func() {
if err := cleanNodes(f, namespace); err != nil {
t.Fatal(err)
}
if err := resetBundleTestMetrics(f, namespace); err != nil {
t.Fatal(err)
}
}()
defer logContainerOutput(t, f, namespace, testIntegrityNamePrefix+"-invalidpc")

// wait to go active even with invalid priority class (it should be cleared)
err := waitForScanStatus(t, f, namespace, testIntegrityNamePrefix+"-invalidpc", v1alpha1.PhaseActive)
if err != nil {
t.Errorf("Timeout waiting for scan status")
}

t.Log("Verifying that the invalid PriorityClassName was cleared")
fileIntegrity := &v1alpha1.FileIntegrity{}
err = f.Client.Get(context.TODO(), types.NamespacedName{Name: testIntegrityNamePrefix + "-invalidpc", Namespace: namespace}, fileIntegrity)
if err != nil {
t.Errorf("Failed to get FileIntegrity: %v", err)
}
if fileIntegrity.Spec.PriorityClassName != "" {
t.Errorf("Expected priorityClassName to be cleared, but got: %s", fileIntegrity.Spec.PriorityClassName)
}

t.Log("Verifying that the DaemonSet was created without priorityClassName")
dsName := common.DaemonSetName(testIntegrityNamePrefix + "-invalidpc")
ds, err := f.KubeClient.AppsV1().DaemonSets(namespace).Get(context.TODO(), dsName, metav1.GetOptions{})
if err != nil {
t.Errorf("Failed to get DaemonSet: %v", err)
}
if ds.Spec.Template.Spec.PriorityClassName != "" {
t.Errorf("Expected DaemonSet priorityClassName to be empty, but got: %s", ds.Spec.Template.Spec.PriorityClassName)
}
}

func TestFileIntegrityLogCompress(t *testing.T) {
f, testctx, namespace := setupTest(t)
testName := testIntegrityNamePrefix + "-logcompress"
Expand Down
Loading