From b4fedde831ed72042917949315b56ad19163337d Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Wed, 30 Aug 2023 20:19:44 -0500 Subject: [PATCH] CMP-2132: Implement suspend and resume scan schedule This commit implements the logic and tests necessary to suspend and resume scan schedules using the `ScanSetting` custom resource. You can find more details on the overall justification, use cases, and implementation details in the enhancement: https://github.com/ComplianceAsCode/compliance-operator/pull/375 --- CHANGELOG.md | 7 +- ...e.openshift.io_compliancecheckresults.yaml | 2 +- ...e.openshift.io_complianceremediations.yaml | 2 +- ...mpliance.openshift.io_compliancescans.yaml | 2 +- ...pliance.openshift.io_compliancesuites.yaml | 7 +- ...ompliance.openshift.io_profilebundles.yaml | 2 +- .../compliance.openshift.io_profiles.yaml | 2 +- .../bases/compliance.openshift.io_rules.yaml | 2 +- ...ance.openshift.io_scansettingbindings.yaml | 10 +- .../compliance.openshift.io_scansettings.yaml | 7 +- ...pliance.openshift.io_tailoredprofiles.yaml | 2 +- .../compliance.openshift.io_variables.yaml | 2 +- doc/usage.md | 47 +++++ .../v1alpha1/compliancesuite_types.go | 4 + .../v1alpha1/scansettingbinding_types.go | 20 ++ .../v1alpha1/zz_generated.deepcopy.go | 1 - .../suitererunner_cron_compat.go | 3 +- .../scansettingbinding_controller.go | 77 ++++++- tests/e2e/framework/common.go | 112 ++++++++++ tests/e2e/serial/main_test.go | 195 ++++++++++++++++++ 20 files changed, 490 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 671761760..6ad64261a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,12 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Enhancements -- +- Users can now pause scan schedules by setting the `ScanSetting.suspend` + attribute to `True`. This allows users to suspend a scan, and reactivate it + without having to delete and recreate the `ScanSettingBinding`, making it + more ergonomic to pause scans during maintenance periods. See the + [enhancement](https://github.com/ComplianceAsCode/compliance-operator/pull/375) + for more details. ### Fixes diff --git a/config/crd/bases/compliance.openshift.io_compliancecheckresults.yaml b/config/crd/bases/compliance.openshift.io_compliancecheckresults.yaml index ff8f26350..63318767d 100644 --- a/config/crd/bases/compliance.openshift.io_compliancecheckresults.yaml +++ b/config/crd/bases/compliance.openshift.io_compliancecheckresults.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: compliancecheckresults.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_complianceremediations.yaml b/config/crd/bases/compliance.openshift.io_complianceremediations.yaml index a506201cf..31f502a0b 100644 --- a/config/crd/bases/compliance.openshift.io_complianceremediations.yaml +++ b/config/crd/bases/compliance.openshift.io_complianceremediations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: complianceremediations.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_compliancescans.yaml b/config/crd/bases/compliance.openshift.io_compliancescans.yaml index 875c9252a..58dd9ca32 100644 --- a/config/crd/bases/compliance.openshift.io_compliancescans.yaml +++ b/config/crd/bases/compliance.openshift.io_compliancescans.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: compliancescans.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_compliancesuites.yaml b/config/crd/bases/compliance.openshift.io_compliancesuites.yaml index b05225a94..aa8f09567 100644 --- a/config/crd/bases/compliance.openshift.io_compliancesuites.yaml +++ b/config/crd/bases/compliance.openshift.io_compliancesuites.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: compliancesuites.compliance.openshift.io spec: group: compliance.openshift.io @@ -323,6 +323,11 @@ spec: scheduled scans will start running only after the initial results are ready. type: string + suspend: + default: false + description: Defines if a schedule should be suspended and is a boolean + value, defaulting to False. + type: boolean required: - scans type: object diff --git a/config/crd/bases/compliance.openshift.io_profilebundles.yaml b/config/crd/bases/compliance.openshift.io_profilebundles.yaml index 490fe9be7..7fbe8b5fe 100644 --- a/config/crd/bases/compliance.openshift.io_profilebundles.yaml +++ b/config/crd/bases/compliance.openshift.io_profilebundles.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: profilebundles.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_profiles.yaml b/config/crd/bases/compliance.openshift.io_profiles.yaml index 82022c16c..5da9ae792 100644 --- a/config/crd/bases/compliance.openshift.io_profiles.yaml +++ b/config/crd/bases/compliance.openshift.io_profiles.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: profiles.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_rules.yaml b/config/crd/bases/compliance.openshift.io_rules.yaml index 7da9d002a..15361c013 100644 --- a/config/crd/bases/compliance.openshift.io_rules.yaml +++ b/config/crd/bases/compliance.openshift.io_rules.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: rules.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_scansettingbindings.yaml b/config/crd/bases/compliance.openshift.io_scansettingbindings.yaml index d44fe3622..26b341f10 100644 --- a/config/crd/bases/compliance.openshift.io_scansettingbindings.yaml +++ b/config/crd/bases/compliance.openshift.io_scansettingbindings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: scansettingbindings.compliance.openshift.io spec: group: compliance.openshift.io @@ -16,7 +16,11 @@ spec: singular: scansettingbinding scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + name: v1alpha1 schema: openAPIV3Schema: description: ScanSettingBinding is the Schema for the scansettingbindings @@ -124,6 +128,8 @@ spec: - name type: object x-kubernetes-map-type: atomic + phase: + type: string type: object type: object served: true diff --git a/config/crd/bases/compliance.openshift.io_scansettings.yaml b/config/crd/bases/compliance.openshift.io_scansettings.yaml index b5ef50246..b997c7a67 100644 --- a/config/crd/bases/compliance.openshift.io_scansettings.yaml +++ b/config/crd/bases/compliance.openshift.io_scansettings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: scansettings.compliance.openshift.io spec: group: compliance.openshift.io @@ -247,6 +247,11 @@ spec: be strict and error out. `false` means that we don't need to be strict and we can proceed. type: boolean + suspend: + default: false + description: Defines if a schedule should be suspended and is a boolean + value, defaulting to False. + type: boolean timeout: default: 30m description: Timeout is the maximum amount of time the scan can run. If diff --git a/config/crd/bases/compliance.openshift.io_tailoredprofiles.yaml b/config/crd/bases/compliance.openshift.io_tailoredprofiles.yaml index 9d59f9509..0763b2897 100644 --- a/config/crd/bases/compliance.openshift.io_tailoredprofiles.yaml +++ b/config/crd/bases/compliance.openshift.io_tailoredprofiles.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: tailoredprofiles.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/config/crd/bases/compliance.openshift.io_variables.yaml b/config/crd/bases/compliance.openshift.io_variables.yaml index 99d021332..d3920a0d2 100644 --- a/config/crd/bases/compliance.openshift.io_variables.yaml +++ b/config/crd/bases/compliance.openshift.io_variables.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.0 + controller-gen.kubebuilder.io/version: v0.13.0 name: variables.compliance.openshift.io spec: group: compliance.openshift.io diff --git a/doc/usage.md b/doc/usage.md index 7647edef0..46226a4c6 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -293,6 +293,53 @@ or `ocp4-var-role-worker`: `oc get ccr -n openshift-compliance -o yaml | jq '.items[] | select(.valuesUsed | contains("ocp4-var-role-master") or contains("ocp4-var-role-worker"))'` +## Suspending and resuming scan schedules + +The `ScanSetting` CRD exposes a `schedule` attribute that allows you to +schedule compliance scans as a cron job syntax. The Compliance Operator uses +Kubernetes `CronJob` resources to implement the schedule for a scan suite, +which is sometimes referred to as a suite rerunner. + +Scan schedules are associated with a `ComplianceSuite`, which may contain at +least one `ComplianceScan`. This means the schedule associated with a +`ComplianceSuite` applies to all `ComplianceScan` objects within that suite. +This may be useful to prevent scans from happening during planned maintenance +windows, where results might be inaccurate depending on the state of the +cluster. + +You can suspend a `ComplianceSuite` by updating the `ScanSetting` you used when +you created the `ScanSettingBinding`. + +``` +$ oc patch ss/default -p 'suspend: true' --type merge +``` + +Any `ScanSettingBinding` using the suspended `ScanSetting` will show a +`SUSPENDED` status: + +``` +$ oc get ssb +NAME STATUS +cis-node SUSPENDED +``` + +You can disable the `suspend` attribute to resume the scan schedule: + +``` +$ oc patch ss/default -p 'suspend: false' --type merge +``` + +The `ScanSettingBinding` will return to a `READY` state: + +``` +$ oc get ssb +NAME STATUS +cis-node READY +``` + +Note that this functionality does not pause, suspend, or stop a scan that is +already in progress. + ## Extracting raw results The scans provide two kinds of raw results: the full report in the ARF format diff --git a/pkg/apis/compliance/v1alpha1/compliancesuite_types.go b/pkg/apis/compliance/v1alpha1/compliancesuite_types.go index 8b14709d0..75bf348c7 100644 --- a/pkg/apis/compliance/v1alpha1/compliancesuite_types.go +++ b/pkg/apis/compliance/v1alpha1/compliancesuite_types.go @@ -82,6 +82,10 @@ type ComplianceSuiteSettings struct { // Note the scan will still be triggered immediately, and the scheduled // scans will start running only after the initial results are ready. Schedule string `json:"schedule,omitempty"` + // Defines if a schedule should be suspended and is a boolean value, + // defaulting to False. + // +kubebuilder:default=false + Suspend bool `json:"suspend,omitempty"` } // ComplianceSuiteSpec defines the desired state of ComplianceSuite diff --git a/pkg/apis/compliance/v1alpha1/scansettingbinding_types.go b/pkg/apis/compliance/v1alpha1/scansettingbinding_types.go index 0b3febd0d..3291eca64 100644 --- a/pkg/apis/compliance/v1alpha1/scansettingbinding_types.go +++ b/pkg/apis/compliance/v1alpha1/scansettingbinding_types.go @@ -5,6 +5,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type ScanSettingBindingStatusPhase string + +const ( + ScanSettingBindingPhasePending ScanSettingBindingStatusPhase = "PENDING" + ScanSettingBindingPhaseReady ScanSettingBindingStatusPhase = "READY" + ScanSettingBindingPhaseInvalid ScanSettingBindingStatusPhase = "INVALID" + ScanSettingBindingPhaseSuspended ScanSettingBindingStatusPhase = "SUSPENDED" +) + type NamedObjectReference struct { Name string `json:"name,omitempty"` Kind string `json:"kind,omitempty"` @@ -16,6 +25,7 @@ type NamedObjectReference struct { // ScanSettingBinding is the Schema for the scansettingbindings API // +kubebuilder:subresource:status // +kubebuilder:resource:path=scansettingbindings,scope=Namespaced,shortName=ssb +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.phase` type ScanSettingBinding struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -32,6 +42,7 @@ type ScanSettingBinding struct { type ScanSettingBindingSpec struct{} type ScanSettingBindingStatus struct { + Phase ScanSettingBindingStatusPhase `json:"phase,omitempty"` // +optional Conditions Conditions `json:"conditions,omitempty"` // Reference to the object generated from this ScanSettingBinding @@ -76,6 +87,15 @@ func (s *ScanSettingBindingStatus) SetConditionReady() { }) } +func (s *ScanSettingBindingStatus) SetConditionSuspended() { + s.Conditions.SetCondition(Condition{ + Type: "Ready", + Status: corev1.ConditionFalse, + Reason: "Suspended", + Message: "The scan setting binding uses a scan setting that is suspended", + }) +} + func init() { SchemeBuilder.Register(&ScanSettingBinding{}, &ScanSettingBindingList{}) } diff --git a/pkg/apis/compliance/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/compliance/v1alpha1/zz_generated.deepcopy.go index 3df2bfa27..2af88722d 100644 --- a/pkg/apis/compliance/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/compliance/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2022. diff --git a/pkg/controller/compliancesuite/suitererunner_cron_compat.go b/pkg/controller/compliancesuite/suitererunner_cron_compat.go index bb1974e16..cf8af755e 100644 --- a/pkg/controller/compliancesuite/suitererunner_cron_compat.go +++ b/pkg/controller/compliancesuite/suitererunner_cron_compat.go @@ -72,11 +72,12 @@ func (r *ReconcileComplianceSuite) cronJobCompatCreate( if !ok { return fmt.Errorf("failed to cast object to v1 CronJob") } - if getObjTyped.Spec.Schedule == suite.Spec.Schedule { + if getObjTyped.Spec.Schedule == suite.Spec.Schedule && getObjTyped.Spec.Suspend == &suite.Spec.Suspend { return nil } cronJobCopy := getObjTyped.DeepCopy() cronJobCopy.Spec.Schedule = suite.Spec.Schedule + cronJobCopy.Spec.Suspend = &suite.Spec.Suspend logger.Info("Updating v1 rerunner", "CronJob.Name", cronJobCopy.GetName()) return r.Client.Update(context.TODO(), cronJobCopy) } diff --git a/pkg/controller/scansettingbinding/scansettingbinding_controller.go b/pkg/controller/scansettingbinding/scansettingbinding_controller.go index aaca45849..cce474e46 100644 --- a/pkg/controller/scansettingbinding/scansettingbinding_controller.go +++ b/pkg/controller/scansettingbinding/scansettingbinding_controller.go @@ -122,6 +122,7 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec if instance.Status.Conditions.GetCondition("Ready") == nil { ssb := instance.DeepCopy() ssb.Status.SetConditionPending() + ssb.Status.Phase = compliancev1alpha1.ScanSettingBindingPhasePending err := r.Client.Status().Update(context.TODO(), ssb) if err != nil { return reconcile.Result{}, fmt.Errorf("Couldn't update ScanSettingBinding status: %s", err) @@ -169,6 +170,7 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec msg := "The TailoredProfile referenced has an error and is not usable" ssb := instance.DeepCopy() ssb.Status.SetConditionInvalid(msg) + ssb.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseInvalid if updateErr := r.Client.Status().Update(context.TODO(), ssb); updateErr != nil { return reconcile.Result{}, fmt.Errorf("couldn't update ScanSettingBinding condition: %w", updateErr) } @@ -194,6 +196,7 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec ssb := instance.DeepCopy() ssb.Status.SetConditionInvalid(msg) + ssb.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseInvalid if updateErr := r.Client.Status().Update(context.TODO(), ssb); updateErr != nil { return reconcile.Result{}, fmt.Errorf("couldn't update ScanSettingBinding condition: %w", updateErr) } @@ -211,6 +214,41 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec } } + if instance.SettingsRef != nil { + if err != nil { + return reconcile.Result{}, fmt.Errorf("Failed to get ScanSetting: %s", err) + } + if suite.Spec.ComplianceSuiteSettings.Suspend { + if !scanSettingBindingHasSuspendedCondition(instance) { + err = r.setSuspendedCondition(instance) + if err != nil { + return reconcile.Result{}, fmt.Errorf("Failed to update ScanSettingBinding status: %s", err) + } + return reconcile.Result{Requeue: true, RequeueAfter: requeueAfterDefault}, nil + } + cs := compliancev1alpha1.ComplianceSuite{} + err = r.Client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: request.Namespace}, &cs) + // If the ComplianceSuite doesn't exist, then we should + // return and not requeue the request because the + // ScanSettingBinding is being created from a suspended + // state and we don't want to create the + // ComplianceSuite or kick off any scans, yet. If or + // when the ScanSetting is activated, we will proceed + // like normal and create the ComplianceSuite and its + // ComplianceScans. + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + } else if !suite.Spec.ComplianceSuiteSettings.Suspend { + if scanSettingBindingHasSuspendedCondition(instance) { + if err = r.removeSuspendedCondition(instance); err != nil { + return reconcile.Result{}, fmt.Errorf("Failed to update ScanSettingBinding status: %s", err) + } + return reconcile.Result{Requeue: true, RequeueAfter: requeueAfterDefault}, nil + } + } + } + found := compliancev1alpha1.ComplianceSuite{} err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: suite.Namespace, Name: suite.Name}, &found) if errors.IsNotFound(err) { @@ -224,6 +262,7 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec ssb := instance.DeepCopy() ssb.Status.SetConditionReady() + ssb.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseReady ssb.Status.OutputRef = &corev1.TypedLocalObjectReference{ APIGroup: &compliancev1alpha1.SchemeGroupVersion.Group, Kind: "ComplianceSuite", @@ -268,6 +307,7 @@ func (r *ReconcileScanSettingBinding) Reconcile(ctx context.Context, request rec if scanSettingBindingStatusNeedsUpdate(instance) { ssb := instance.DeepCopy() ssb.Status.SetConditionReady() + ssb.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseReady group := found.GroupVersionKind().Group ssb.Status.OutputRef = &corev1.TypedLocalObjectReference{ APIGroup: &group, @@ -762,5 +802,40 @@ func suiteNeedsUpdate(have, found *compliancev1alpha1.ComplianceSuite) bool { } func scanSettingBindingStatusNeedsUpdate(ssb *compliancev1alpha1.ScanSettingBinding) bool { - return ssb.Status.Conditions.GetCondition("Ready") == nil || ssb.Status.OutputRef == nil || ssb.Status.OutputRef.Name == "" + if ssb.Status.Conditions.GetCondition("Ready") == nil { + return true + } else if ssb.Status.OutputRef == nil { + return true + } else if ssb.Status.OutputRef.Name == "" { + return true + } else { + return false + } +} + +func scanSettingBindingHasSuspendedCondition(ssb *compliancev1alpha1.ScanSettingBinding) bool { + c := ssb.Status.Conditions.GetCondition("Ready") + return c != nil && c.Status == "False" && c.Reason == "Suspended" +} + +func (r *ReconcileScanSettingBinding) setSuspendedCondition(ssb *compliancev1alpha1.ScanSettingBinding) error { + log.Info("Suspending ScanSettingBinding", "ScanSettingBinding", ssb.Name) + c := ssb.DeepCopy() + c.Status.SetConditionSuspended() + c.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseSuspended + if err := r.Client.Status().Update(context.TODO(), c); err != nil { + return err + } + return nil +} + +func (r *ReconcileScanSettingBinding) removeSuspendedCondition(ssb *compliancev1alpha1.ScanSettingBinding) error { + log.Info("Resuming ScanSettingBinding", "ScanSettingBinding", ssb.Name) + c := ssb.DeepCopy() + c.Status.SetConditionReady() + c.Status.Phase = compliancev1alpha1.ScanSettingBindingPhaseReady + if err := r.Client.Status().Update(context.TODO(), c); err != nil { + return err + } + return nil } diff --git a/tests/e2e/framework/common.go b/tests/e2e/framework/common.go index be69d19ad..bdebd1a82 100644 --- a/tests/e2e/framework/common.go +++ b/tests/e2e/framework/common.go @@ -787,6 +787,39 @@ func (f *Framework) WaitForScanStatus(namespace, name string, targetStatus compv return nil } +func (f *Framework) WaitForScanSettingBindingStatus(namespace, name string, targetStatus compv1alpha1.ScanSettingBindingStatusPhase) error { + b := &compv1alpha1.ScanSettingBinding{} + var err error + // retry and ignore errors until timeout + timeoutErr := wait.Poll(RetryInterval, Timeout, func() (bool, error) { + err = f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, b) + if err != nil { + if apierrors.IsNotFound(err) { + log.Printf("Waiting for availability of %s ScanSettingBinding\n", name) + return false, nil + } + log.Printf("Retrying. Got error: %v\n", err) + return false, nil + } + + if b.Status.Phase == targetStatus { + return true, nil + } + log.Printf("Waiting for ScanSettingBinding %s to reach status %s, it is currently %s\n", name, targetStatus, b.Status.Phase) + return false, nil + }) + + if timeoutErr != nil { + return fmt.Errorf("failed waiting for ScanSettingBinding %s due to timeout: %s", name, timeoutErr) + } + if err != nil { + return fmt.Errorf("failed waiting for ScanSettingBinding %s to reach status %s: %s", name, targetStatus, err) + } + + log.Printf("ScanSettingBinding status %s\n", b.Status.Phase) + return nil +} + // waitForScanStatus will poll until the compliancescan that we're lookingfor reaches a certain status, or until // a timeout is reached. func (f *Framework) WaitForSuiteScansStatus(namespace, name string, targetStatus compv1alpha1.ComplianceScanStatusPhase, targetComplianceStatus compv1alpha1.ComplianceScanStatusResult) error { @@ -1014,6 +1047,57 @@ func (f *Framework) AssertScanHasValidPVCReferenceWithSize(scanName, size, names return nil } +func (f *Framework) AssertScanDoesNotExist(name, namespace string) error { + cs := &compv1alpha1.ComplianceScan{} + defer f.logContainerOutput(namespace, name) + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, cs) + if apierrors.IsNotFound(err) { + return nil + } + return err +} + +func (f *Framework) AssertComplianceSuiteDoesNotExist(name, namespace string) error { + cs := &compv1alpha1.ComplianceSuite{} + defer f.logContainerOutput(namespace, name) + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, cs) + if apierrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + return fmt.Errorf("Failed to assert ComplianceSuite %s does not exist.", name) +} + +func (f *Framework) AssertScanSettingBindingConditionIsReady(name string, namespace string) error { + ssb := &compv1alpha1.ScanSettingBinding{} + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, ssb) + if err != nil { + return fmt.Errorf("Failed to get ScanSettingBinding %s: %w", name, err) + } + condition := ssb.Status.Conditions.GetCondition("Ready") + if condition != nil && condition.Status == "True" { + return nil + } + return fmt.Errorf("expected ScanSettingBinding %s %s condition to be valid", ssb.Name, condition.Type) + +} + +func (f *Framework) AssertScanSettingBindingConditionIsSuspended(name string, namespace string) error { + ssb := &compv1alpha1.ScanSettingBinding{} + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, ssb) + if err != nil { + return fmt.Errorf("Failed to get ScanSettingBinding %s: %w", name, err) + } + + condition := ssb.Status.Conditions.GetCondition("Ready") + if condition != nil && condition.Status == "False" && condition.Reason == "Suspended" { + return nil + } + return fmt.Errorf("expected ScanSettingBinding %s %s condition to reflect suspended status", ssb.Name, condition.Type) + +} + func (f *Framework) ScanHasWarnings(scanName, namespace string) error { cs := &compv1alpha1.ComplianceScan{} err := f.Client.Get(context.TODO(), types.NamespacedName{Name: scanName, Namespace: namespace}, cs) @@ -2211,3 +2295,31 @@ func (f *Framework) WaitForGenericRemediationToBeAutoApplied(remName, remNamespa } return nil } + +func (f *Framework) AssertCronJobIsSuspended(name string) error { + job := &batchv1.CronJob{} + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: f.OperatorNamespace}, job) + if err != nil { + return err + } + if !*job.Spec.Suspend { + msg := fmt.Sprintf("Expected CronJob %s to be suspended", name) + return errors.New(msg) + } + log.Printf("CronJob %s is suspended", name) + return nil +} + +func (f *Framework) AssertCronJobIsNotSuspended(name string) error { + job := &batchv1.CronJob{} + err := f.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: f.OperatorNamespace}, job) + if err != nil { + return err + } + if *job.Spec.Suspend { + msg := fmt.Sprintf("Expected CronJob %s to be active", name) + return errors.New(msg) + } + log.Printf("CronJob %s is active", name) + return nil +} diff --git a/tests/e2e/serial/main_test.go b/tests/e2e/serial/main_test.go index 102e38ea7..86a47482d 100644 --- a/tests/e2e/serial/main_test.go +++ b/tests/e2e/serial/main_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + compsuitectrl "github.com/ComplianceAsCode/compliance-operator/pkg/controller/compliancesuite" configv1 "github.com/openshift/api/config/v1" mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1" corev1 "k8s.io/api/core/v1" @@ -1408,6 +1409,200 @@ func TestKubeletConfigRemediation(t *testing.T) { } } +func TestSuspendScanSetting(t *testing.T) { + f := framework.Global + + // Creates a new `ScanSetting`, where the actual scan schedule doesn't necessarily matter, but `suspend` is set to `False` + scanSettingName := framework.GetObjNameFromTest(t) + "-scansetting" + scanSetting := compv1alpha1.ScanSetting{ + ObjectMeta: metav1.ObjectMeta{ + Name: scanSettingName, + Namespace: f.OperatorNamespace, + }, + ComplianceSuiteSettings: compv1alpha1.ComplianceSuiteSettings{ + AutoApplyRemediations: false, + Schedule: "0 1 * * *", + Suspend: false, + }, + Roles: []string{"master", "worker"}, + } + if err := f.Client.Create(context.TODO(), &scanSetting, nil); err != nil { + t.Fatal(err) + } + defer f.Client.Delete(context.TODO(), &scanSetting) + + // Bind the new ScanSetting to a Profile + bindingName := framework.GetObjNameFromTest(t) + "-binding" + scanSettingBinding := compv1alpha1.ScanSettingBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: f.OperatorNamespace, + }, + Profiles: []compv1alpha1.NamedObjectReference{ + { + Name: "ocp4-cis", + Kind: "Profile", + APIGroup: "compliance.openshift.io/v1alpha1", + }, + }, + SettingsRef: &compv1alpha1.NamedObjectReference{ + Name: scanSetting.Name, + Kind: "ScanSetting", + APIGroup: "compliance.openshift.io/v1alpha1", + }, + } + if err := f.Client.Create(context.TODO(), &scanSettingBinding, nil); err != nil { + t.Fatal(err) + } + defer f.Client.Delete(context.TODO(), &scanSettingBinding) + + // Wait until the first scan completes since the CronJob is created + // after the scan is done + if err := f.WaitForSuiteScansStatus(f.OperatorNamespace, bindingName, compv1alpha1.PhaseDone, compv1alpha1.ResultNonCompliant); err != nil { + t.Fatal(err) + } + + suite := &compv1alpha1.ComplianceSuite{} + key := types.NamespacedName{Name: bindingName, Namespace: f.OperatorNamespace} + if err := f.Client.Get(context.TODO(), key, suite); err != nil { + t.Fatal(err) + } + + // Assert the CronJob is not suspended. + if err := f.AssertCronJobIsNotSuspended(compsuitectrl.GetRerunnerName(suite.Name)); err != nil { + t.Fatal(err) + } + if err := f.AssertScanSettingBindingConditionIsReady(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } + + // Suspend the `ScanSetting` using the `suspend` attribute + scanSettingUpdate := &compv1alpha1.ScanSetting{} + if err := f.Client.Get(context.TODO(), types.NamespacedName{Namespace: f.OperatorNamespace, Name: scanSettingName}, scanSettingUpdate); err != nil { + t.Fatalf("failed to get ScanSetting %s", scanSettingName) + } + scanSettingUpdate.Suspend = true + if err := f.Client.Update(context.TODO(), scanSettingUpdate); err != nil { + t.Fatal(err) + } + + if err := f.WaitForScanSettingBindingStatus(f.OperatorNamespace, bindingName, compv1alpha1.ScanSettingBindingPhaseSuspended); err != nil { + t.Fatalf("ScanSettingBinding %s failed to suspend", bindingName) + } + if err := f.AssertCronJobIsSuspended(compsuitectrl.GetRerunnerName(suite.Name)); err != nil { + t.Fatal(err) + } + if err := f.AssertScanSettingBindingConditionIsSuspended(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } + + // Resume the `ComplianceScan` by updating the `ScanSetting.suspend` attribute to `False` + scanSettingUpdate = &compv1alpha1.ScanSetting{} + if err := f.Client.Get(context.TODO(), types.NamespacedName{Namespace: f.OperatorNamespace, Name: scanSettingName}, scanSettingUpdate); err != nil { + t.Fatalf("failed to get ScanSetting %s", scanSettingName) + } + scanSettingUpdate.Suspend = false + if err := f.Client.Update(context.TODO(), scanSettingUpdate); err != nil { + t.Fatal(err) + } + + if err := f.WaitForScanSettingBindingStatus(f.OperatorNamespace, bindingName, compv1alpha1.ScanSettingBindingPhaseReady); err != nil { + t.Fatalf("ScanSettingBinding %s failed to resume", bindingName) + } + if err := f.AssertCronJobIsNotSuspended(compsuitectrl.GetRerunnerName(suite.Name)); err != nil { + t.Fatal(err) + } + if err := f.AssertScanSettingBindingConditionIsReady(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } +} + +func TestSuspendScanSettingDoesNotCreateScan(t *testing.T) { + f := framework.Global + + // Creates a new `ScanSetting` with `suspend` set to `True` + scanSettingName := framework.GetObjNameFromTest(t) + "-scansetting" + scanSetting := compv1alpha1.ScanSetting{ + ObjectMeta: metav1.ObjectMeta{ + Name: scanSettingName, + Namespace: f.OperatorNamespace, + }, + ComplianceSuiteSettings: compv1alpha1.ComplianceSuiteSettings{ + AutoApplyRemediations: false, + Schedule: "0 1 * * *", + Suspend: true, + }, + Roles: []string{"master", "worker"}, + } + if err := f.Client.Create(context.TODO(), &scanSetting, nil); err != nil { + t.Fatal(err) + } + defer f.Client.Delete(context.TODO(), &scanSetting) + + // Bind the new `ScanSetting` to a `Profile` + bindingName := framework.GetObjNameFromTest(t) + "-binding" + scanSettingBinding := compv1alpha1.ScanSettingBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: f.OperatorNamespace, + }, + Profiles: []compv1alpha1.NamedObjectReference{ + { + Name: "ocp4-cis", + Kind: "Profile", + APIGroup: "compliance.openshift.io/v1alpha1", + }, + }, + SettingsRef: &compv1alpha1.NamedObjectReference{ + Name: scanSetting.Name, + Kind: "ScanSetting", + APIGroup: "compliance.openshift.io/v1alpha1", + }, + } + if err := f.Client.Create(context.TODO(), &scanSettingBinding, nil); err != nil { + t.Fatal(err) + } + defer f.Client.Delete(context.TODO(), &scanSettingBinding) + + // Assert the ScanSettingBinding is Suspended + if err := f.WaitForScanSettingBindingStatus(f.OperatorNamespace, bindingName, compv1alpha1.ScanSettingBindingPhaseSuspended); err != nil { + t.Fatalf("ScanSettingBinding %s failed to suspend: %v", bindingName, err) + } + + if err := f.AssertScanSettingBindingConditionIsSuspended(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } + if err := f.AssertComplianceSuiteDoesNotExist(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } + + scanName := "ocp4-cis" + err := f.AssertScanDoesNotExist(scanName, f.OperatorNamespace) + if err != nil { + t.Fatal(err) + } + + // Update the `ScanSetting.suspend` attribute to `False` + scanSetting.Suspend = false + if err := f.Client.Update(context.TODO(), &scanSetting); err != nil { + t.Fatal(err) + } + // Assert the scan is performed + if err := f.WaitForSuiteScansStatus(f.OperatorNamespace, bindingName, compv1alpha1.PhaseDone, compv1alpha1.ResultNonCompliant); err != nil { + t.Fatal(err) + } + + if err := f.WaitForScanSettingBindingStatus(f.OperatorNamespace, bindingName, compv1alpha1.ScanSettingBindingPhaseReady); err != nil { + t.Fatal(err) + } + if err := f.AssertCronJobIsNotSuspended(compsuitectrl.GetRerunnerName(bindingName)); err != nil { + t.Fatal(err) + } + if err := f.AssertScanSettingBindingConditionIsReady(bindingName, f.OperatorNamespace); err != nil { + t.Fatal(err) + } +} + //testExecution{ // Name: "TestNodeSchedulingErrorFailsTheScan", // IsParallel: false,