Skip to content
Merged
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
19 changes: 19 additions & 0 deletions documentation/docs/api/crds/scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,24 @@ ttlSecondsAfterFinished: 30 #deletes the scan after 30 seconds after completion
ttlSecondsAfterFinished can also be set for the scan (as part of the [jobTemplate](https://www.securecodebox.io/docs/api/crds/scan-type#jobtemplate-required)), [parser](https://www.securecodebox.io/docs/api/crds/parse-definition) and [hook](https://www.securecodebox.io/docs/api/crds/scan-completion-hook#ttlsecondsafterfinished-optional) jobs individually. Setting these will only delete the jobs, not the entire scan.
:::

### Suspend (Optional)

`suspend` specifies whether the Scan should be suspended. When a Scan is suspended, the reconciler will not process it, effectively pausing all operations until it is resumed. This behaves similar to the suspend field in Kubernetes Jobs.

When set to `true`, the scan will not progress through its lifecycle states (Init, Scanning, Parsing, etc.). However, TTL-based cleanup still works on suspended scans that are in Done or Errored states to prevent accumulation of completed suspended scans.

Defaults to `false` if not set.

```yaml
suspend: true # Suspends the scan, preventing any operations
```

To resume a suspended scan, you can patch the scan resource:

```bash
kubectl patch scan my-scan --type merge -p '{"spec":{"suspend":false}}'
```

## Metadata

Metadata is a standard field on Kubernetes resources. It contains multiple relevant fields, e.g. the name of the resource, its namespace and a `creationTimestamp` of the resource. See more on the [Kubernetes Docs](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/) and the [Kubernetes API Reference](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/object-meta/).
Expand Down Expand Up @@ -443,4 +461,5 @@ spec:
cpu: 4
memory: 4Gi
ttlSecondsAfterFinished: 300
suspend: false
```
20 changes: 20 additions & 0 deletions documentation/docs/api/crds/scheduled-scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ When `retriggerOnScanTypeChange` is enabled, it will automatically trigger a new

Defaults to `false` if not set.

### Suspend (Optional)

`suspend` specifies whether the ScheduledScan should be suspended. When a ScheduledScan is suspended, no new Scans will be created according to the schedule. This behaves similar to the suspend field in Kubernetes CronJobs.

When set to `true`, the ScheduledScan will continue to reconcile (updating status and tracking the schedule), but it will skip creating new Scan resources. Any scans that are already running will continue to completion.

Defaults to `false` if not set.

```yaml
suspend: true # Suspends the scheduled scan, preventing new scan creation
```

To resume a suspended scheduled scan:

```bash
kubectl patch scheduledscan my-scheduled-scan --type merge -p '{"spec":{"suspend":false}}'
```

## Example with an Interval

```yaml
Expand All @@ -81,6 +99,7 @@ spec:
failedJobsHistoryLimit: 5
concurrencyPolicy: "Allow"
retriggerOnScanTypeChange: false
suspend: false
```

## Example with a Cron Schedule
Expand All @@ -102,4 +121,5 @@ spec:
failedJobsHistoryLimit: 5
concurrencyPolicy: "Forbid"
retriggerOnScanTypeChange: true
suspend: false
```
4 changes: 4 additions & 0 deletions operator/apis/execution/v1/scan_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ type ScanSpec struct {
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
// ttlSecondsAfterFinished limits the lifetime of a Scan that has finished execution (either Done or Errored). If this field is set ttlSecondsAfterFinished after the Scan finishes, it is eligible to be automatically deleted. When the Scan is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Scan won't be automatically deleted. If this is set to zero, the Scan becomes eligible to be deleted immediately after it finishes.
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`
// Suspend specifies whether the Scan should be suspended. When a Scan is suspended, the reconciler will not process it, effectively pausing all operations until it is resumed. This behaves similar to the suspend field in Kubernetes Jobs. TTL-based cleanup still works on suspended scans that are Done or Errored.
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
Suspend *bool `json:"suspend,omitempty"`
}

type ScanState string
Expand Down
5 changes: 5 additions & 0 deletions operator/apis/execution/v1/scheduledscan_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ type ScheduledScanSpec struct {
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
RetriggerOnScanTypeChange bool `json:"retriggerOnScanTypeChange,omitempty"`

// Suspend specifies whether the ScheduledScan should be suspended. When a ScheduledScan is suspended, no new Scans will be created according to the schedule. This behaves similar to the suspend field in Kubernetes CronJobs.
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
Suspend *bool `json:"suspend,omitempty"`
}

// ConcurrencyPolicy describes how the job will be handled.
Expand Down
10 changes: 10 additions & 0 deletions operator/apis/execution/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions operator/controllers/execution/scans/scan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ func (r *ScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.

log.V(5).Info("Scan Found", "Type", scan.Spec.ScanType, "State", scan.Status.State)

// Check if the scan is suspended. If so, skip reconciliation unless the scan is in a terminal state
// where TTL-based cleanup should still work.
if scan.Spec.Suspend != nil && *scan.Spec.Suspend {
if scan.Status.State != executionv1.ScanStateDone && scan.Status.State != executionv1.ScanStateErrored {
log.V(7).Info("Scan is suspended, skipping reconciliation")
return ctrl.Result{}, nil
}
// For Done/Errored scans, continue to allow TTL cleanup
}

// Handle Finalizer if the scan is getting deleted
if !scan.ObjectMeta.DeletionTimestamp.IsZero() {
// Check if this Scan has not yet been converted to new CRD
Expand Down
116 changes: 116 additions & 0 deletions operator/controllers/execution/scans/scan_reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,120 @@ var _ = Describe("ScanControllers", func() {
})

})

Context("Suspend Functionality", func() {
It("should return true for TTL cleanup on suspended Done scan", func() {
finishTime := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
var timeout int32 = 30
suspend := true
var scan = &executionv1.Scan{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "nmap",
},
Spec: executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
TTLSecondsAfterFinished: &timeout,
Suspend: &suspend,
},
Status: executionv1.ScanStatus{
State: executionv1.ScanStateDone,
FinishedAt: &metav1.Time{Time: finishTime},
},
}
// TTL cleanup should still work even when suspended
Expect(reconciler.checkIfTTLSecondsAfterFinishedIsCompleted(scan)).To(BeTrue())
})

It("should return true for TTL cleanup on suspended Errored scan", func() {
finishTime := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
var timeout int32 = 30
suspend := true
var scan = &executionv1.Scan{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "nmap",
},
Spec: executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
TTLSecondsAfterFinished: &timeout,
Suspend: &suspend,
},
Status: executionv1.ScanStatus{
State: executionv1.ScanStateErrored,
FinishedAt: &metav1.Time{Time: finishTime},
},
}
// TTL cleanup should still work even when suspended
Expect(reconciler.checkIfTTLSecondsAfterFinishedIsCompleted(scan)).To(BeTrue())
})

It("should identify a suspended scan correctly", func() {
suspend := true
var scan = &executionv1.Scan{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "nmap",
},
Spec: executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
Suspend: &suspend,
},
Status: executionv1.ScanStatus{
State: executionv1.ScanStateInit,
},
}
// Verify the suspend flag is properly set
Expect(scan.Spec.Suspend).NotTo(BeNil())
Expect(*scan.Spec.Suspend).To(BeTrue())
})

It("should identify a non-suspended scan correctly when Suspend is false", func() {
suspend := false
var scan = &executionv1.Scan{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "nmap",
},
Spec: executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
Suspend: &suspend,
},
Status: executionv1.ScanStatus{
State: executionv1.ScanStateInit,
},
}
// Verify the suspend flag is properly set to false
Expect(scan.Spec.Suspend).NotTo(BeNil())
Expect(*scan.Spec.Suspend).To(BeFalse())
})

It("should handle nil Suspend field as not suspended", func() {
var scan = &executionv1.Scan{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "nmap",
},
Spec: executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
Suspend: nil, // Not set, should default to false
},
Status: executionv1.ScanStatus{
State: executionv1.ScanStateInit,
},
}
// When Suspend is nil, the scan should not be considered suspended
if scan.Spec.Suspend != nil {
Expect(*scan.Spec.Suspend).To(BeFalse())
} else {
// nil is treated as not suspended
Expect(scan.Spec.Suspend).To(BeNil())
}
})
})
})
6 changes: 6 additions & 0 deletions operator/controllers/execution/scheduledscan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Reques

InProgressScans := getScansInProgress(childScans.Items)

// Check if the ScheduledScan is suspended. If so, skip creating new scans.
if scheduledScan.Spec.Suspend != nil && *scheduledScan.Spec.Suspend {
log.V(7).Info("ScheduledScan is suspended, skipping scan creation")
return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
}

// check if it is time to start the next Scan
if !time.Now().Before(nextSchedule) {
// check concurrency policy
Expand Down
68 changes: 68 additions & 0 deletions operator/controllers/execution/scheduledscan_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,72 @@ var _ = Describe("ScheduledScan controller", func() {
Expect(firstScanName).ShouldNot(Equal(secondScanName))
})
})

Context("Suspend Functionality", func() {
It("should identify a suspended ScheduledScan correctly", func() {
suspend := true
scheduledScan := &executionv1.ScheduledScan{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scan",
Namespace: "test-namespace",
},
Spec: executionv1.ScheduledScanSpec{
Interval: metav1.Duration{Duration: 1 * time.Hour},
ScanSpec: &executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
},
Suspend: &suspend,
},
}
// Verify the suspend flag is properly set
Expect(scheduledScan.Spec.Suspend).NotTo(BeNil())
Expect(*scheduledScan.Spec.Suspend).To(BeTrue())
})

It("should identify a non-suspended ScheduledScan correctly when Suspend is false", func() {
suspend := false
scheduledScan := &executionv1.ScheduledScan{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scan",
Namespace: "test-namespace",
},
Spec: executionv1.ScheduledScanSpec{
Interval: metav1.Duration{Duration: 1 * time.Hour},
ScanSpec: &executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
},
Suspend: &suspend,
},
}
// Verify the suspend flag is properly set to false
Expect(scheduledScan.Spec.Suspend).NotTo(BeNil())
Expect(*scheduledScan.Spec.Suspend).To(BeFalse())
})

It("should handle nil Suspend field as not suspended", func() {
scheduledScan := &executionv1.ScheduledScan{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scan",
Namespace: "test-namespace",
},
Spec: executionv1.ScheduledScanSpec{
Interval: metav1.Duration{Duration: 1 * time.Hour},
ScanSpec: &executionv1.ScanSpec{
ScanType: "nmap",
Parameters: []string{"scanme.nmap.org"},
},
Suspend: nil, // Not set, should default to false
},
}
// When Suspend is nil, the scan should not be considered suspended
if scheduledScan.Spec.Suspend != nil {
Expect(*scheduledScan.Spec.Suspend).To(BeFalse())
} else {
// nil is treated as not suspended
Expect(scheduledScan.Spec.Suspend).To(BeNil())
}
})
})
})
7 changes: 7 additions & 0 deletions operator/crds/cascading.securecodebox.io_cascadingrules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2767,6 +2767,13 @@ spec:
scanType:
description: The name of the scanType which should be started.
type: string
suspend:
default: false
description: Suspend specifies whether the Scan should be suspended.
When a Scan is suspended, the reconciler will not process it,
effectively pausing all operations until it is resumed. This
behaves similar to the suspend field in Kubernetes Jobs.
type: boolean
tolerations:
description: Tolerations are a different way to control on which
nodes your scan is executed. See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
Expand Down
7 changes: 7 additions & 0 deletions operator/crds/execution.securecodebox.io_scans.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2710,6 +2710,13 @@ spec:
scanType:
description: The name of the scanType which should be started.
type: string
suspend:
default: false
description: Suspend specifies whether the Scan should be suspended.
When a Scan is suspended, the reconciler will not process it, effectively
pausing all operations until it is resumed. This behaves similar
to the suspend field in Kubernetes Jobs.
type: boolean
tolerations:
description: Tolerations are a different way to control on which nodes
your scan is executed. See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
Expand Down
14 changes: 14 additions & 0 deletions operator/crds/execution.securecodebox.io_scheduledscans.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,13 @@ spec:
scanType:
description: The name of the scanType which should be started.
type: string
suspend:
default: false
description: Suspend specifies whether the Scan should be suspended.
When a Scan is suspended, the reconciler will not process it,
effectively pausing all operations until it is resumed. This
behaves similar to the suspend field in Kubernetes Jobs.
type: boolean
tolerations:
description: Tolerations are a different way to control on which
nodes your scan is executed. See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
Expand Down Expand Up @@ -4540,6 +4547,13 @@ spec:
format: int32
minimum: 0
type: integer
suspend:
default: false
description: Suspend specifies whether the ScheduledScan should be
suspended. When a ScheduledScan is suspended, no new Scans will
be created according to the schedule. This behaves similar to the
suspend field in Kubernetes CronJobs.
type: boolean
required:
- scanSpec
type: object
Expand Down
Loading