diff --git a/documentation/docs/api/crds/scan.md b/documentation/docs/api/crds/scan.md index e4ba2a60e8..e637c660ec 100644 --- a/documentation/docs/api/crds/scan.md +++ b/documentation/docs/api/crds/scan.md @@ -340,6 +340,17 @@ resources: memory: 4Gi ``` +### TTLSecondsAfterFinished +`ttlSecondsAfterFinished` deletes the scan after a specified duration. + +```yaml +ttlSecondsAfterFinished: 30 #deletes the scan after 30 seconds after completion +``` + +:::note +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 deleted the jobs not the entire scan. +::: + ## 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/). diff --git a/operator/apis/execution/v1/scan_types.go b/operator/apis/execution/v1/scan_types.go index d8bed434bf..ff1869675d 100644 --- a/operator/apis/execution/v1/scan_types.go +++ b/operator/apis/execution/v1/scan_types.go @@ -146,6 +146,8 @@ type ScanSpec struct { // Resources lets you control resource limits and requests for the parser container. See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 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"` } type ScanState string @@ -168,7 +170,7 @@ const ( type ScanStatus struct { State ScanState `json:"state,omitempty"` - // FinishedAt contains the time where the scan (including parser & hooks) has been marked as "Done" + // FinishedAt contains the time where the scan (including parser & hooks) has been marked as "Done", or "Errored" FinishedAt *metav1.Time `json:"finishedAt,omitempty"` ErrorDescription string `json:"errorDescription,omitempty"` diff --git a/operator/apis/execution/v1/zz_generated.deepcopy.go b/operator/apis/execution/v1/zz_generated.deepcopy.go index f031854be9..7669a340c6 100644 --- a/operator/apis/execution/v1/zz_generated.deepcopy.go +++ b/operator/apis/execution/v1/zz_generated.deepcopy.go @@ -711,6 +711,11 @@ func (in *ScanSpec) DeepCopyInto(out *ScanSpec) { (*in).DeepCopyInto(*out) } in.Resources.DeepCopyInto(&out.Resources) + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScanSpec. diff --git a/operator/controllers/execution/scans/scan_controller.go b/operator/controllers/execution/scans/scan_controller.go index 812abc24b1..69c709eb30 100644 --- a/operator/controllers/execution/scans/scan_controller.go +++ b/operator/controllers/execution/scans/scan_controller.go @@ -107,6 +107,14 @@ func (r *ScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. err = r.setHookStatus(&scan) case executionv1.ScanStateHookProcessing: err = r.executeHooks(&scan) + case executionv1.ScanStateErrored: + if r.checkIfTTLSecondsAfterFinishedisCompleted(&scan) { + err = r.deleteScan(&scan) + } + case executionv1.ScanStateDone: + if r.checkIfTTLSecondsAfterFinishedisCompleted(&scan) { + err = r.deleteScan(&scan) + } case executionv1.ScanStateReadAndWriteHookProcessing: fallthrough case executionv1.ScanStateReadAndWriteHookCompleted: @@ -114,6 +122,13 @@ func (r *ScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. case executionv1.ScanStateReadOnlyHookProcessing: err = r.migrateHookStatus(&scan) } + + if scan.Spec.TTLSecondsAfterFinished != nil && (scan.Status.State == executionv1.ScanStateDone || scan.Status.State == executionv1.ScanStateErrored) { + return ctrl.Result{ + Requeue: true, + RequeueAfter: time.Duration(*scan.Spec.TTLSecondsAfterFinished) * time.Second, + }, err + } if err != nil { return ctrl.Result{}, err } @@ -241,6 +256,11 @@ func updateScanStateMetrics(scan executionv1.Scan) { func (r *ScanReconciler) updateScanStatus(ctx context.Context, scan *executionv1.Scan) error { updateScanStateMetrics(*scan) + if scan.Status.State == executionv1.ScanStateDone || scan.Status.State == executionv1.ScanStateErrored { + if scan.Status.FinishedAt == nil { + scan.Status.FinishedAt = &metav1.Time{Time: time.Now()} + } + } if err := r.Status().Update(ctx, scan); err != nil { r.Log.Error(err, "unable to update Scan status") diff --git a/operator/controllers/execution/scans/scan_reconciler.go b/operator/controllers/execution/scans/scan_reconciler.go index 8f5959d08d..cd0b3c2a7c 100644 --- a/operator/controllers/execution/scans/scan_reconciler.go +++ b/operator/controllers/execution/scans/scan_reconciler.go @@ -20,6 +20,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" resource "k8s.io/apimachinery/pkg/api/resource" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -416,3 +417,34 @@ func (r *ScanReconciler) constructJobForScan(scan *executionv1.Scan, scanTypeSpe return job, nil } + +func (r *ScanReconciler) checkIfTTLSecondsAfterFinishedisCompleted(scan *executionv1.Scan) bool { + if scan.Spec.TTLSecondsAfterFinished == nil { + return false + } + interval := time.Duration(*scan.Spec.TTLSecondsAfterFinished) * time.Second + if scan.Status.FinishedAt != nil { + scanTimeout := scan.Status.FinishedAt.Add(interval) + now := time.Now() + if now.After(scanTimeout) { + return true + } + } + return false +} + +func (r *ScanReconciler) deleteScan(scan *executionv1.Scan) error { + ctx := context.Background() + err := r.Client.Delete(ctx, scan) + if err != nil { + if apierrors.IsNotFound(err) { + r.Log.Info("Scan was already deleted, nothing to do") + } else { + r.Log.Error(err, "Unexpected error while trying to delete Scan") + return err + } + } else { + r.Log.Info("Scan was deleted successfully", "scan", scan.Name) + } + return nil +} diff --git a/operator/crds/cascading.securecodebox.io_cascadingrules.yaml b/operator/crds/cascading.securecodebox.io_cascadingrules.yaml index ec2d0ee6bf..45b88803c8 100644 --- a/operator/crds/cascading.securecodebox.io_cascadingrules.yaml +++ b/operator/crds/cascading.securecodebox.io_cascadingrules.yaml @@ -2684,6 +2684,13 @@ spec: type: string type: object type: array + ttlSecondsAfterFinished: + description: 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. + format: int32 + type: integer volumeMounts: description: VolumeMounts allows to specify volume mounts for the scan container. diff --git a/operator/crds/execution.securecodebox.io_scans.yaml b/operator/crds/execution.securecodebox.io_scans.yaml index 437f8b3c14..be953384b4 100644 --- a/operator/crds/execution.securecodebox.io_scans.yaml +++ b/operator/crds/execution.securecodebox.io_scans.yaml @@ -2623,6 +2623,13 @@ spec: type: string type: object type: array + ttlSecondsAfterFinished: + description: 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. + format: int32 + type: integer volumeMounts: description: VolumeMounts allows to specify volume mounts for the scan container. @@ -4277,7 +4284,7 @@ spec: type: object finishedAt: description: FinishedAt contains the time where the scan (including - parser & hooks) has been marked as "Done" + parser & hooks) has been marked as "Done", or "Errored" format: date-time type: string orderedHookStatuses: diff --git a/operator/crds/execution.securecodebox.io_scheduledscans.yaml b/operator/crds/execution.securecodebox.io_scheduledscans.yaml index ab760302a6..9b871e7393 100644 --- a/operator/crds/execution.securecodebox.io_scheduledscans.yaml +++ b/operator/crds/execution.securecodebox.io_scheduledscans.yaml @@ -2678,6 +2678,13 @@ spec: type: string type: object type: array + ttlSecondsAfterFinished: + description: 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. + format: int32 + type: integer volumeMounts: description: VolumeMounts allows to specify volume mounts for the scan container.