Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
677b39c
#150 added schedule and cronjob logic to scheduledscan controller
Ilyesbdlala May 12, 2023
609a946
#150 WIP Added tests for scheduled scans with cron config
Ilyesbdlala May 12, 2023
4b48d06
#150 Added auto-gen files and config
Ilyesbdlala May 12, 2023
0fe8e47
fix newline in makefile of operator
Ilyesbdlala May 16, 2023
d83c5f0
#180 Added "Schedule" print column to ScheduledScan CRD
Ilyesbdlala May 16, 2023
f8e4a1d
#180 Regenerated CRD yaml files
Ilyesbdlala May 16, 2023
b3afdd4
Fixed earliest time logic in getNextSchedule for ScheduledScans
Ilyesbdlala May 16, 2023
ba35756
#180 Fix to generated scheduledScan yaml file
Ilyesbdlala May 16, 2023
1e4ea5c
#150 Added a fake Clock that allows for testing crontab ScheduledScans
Ilyesbdlala May 23, 2023
779db14
#150 Commented out the test for cronjob scheduled scan until a solut…
Ilyesbdlala Jun 6, 2023
f95bd32
#150 replaced fakeClock with active waiting
Ilyesbdlala Jun 6, 2023
1e8f7ec
#150 Replaced tests of active waiting with polling for scheduledSca…
Ilyesbdlala Jun 13, 2023
185482f
#150 Increased the timeout for the scheduledScan Cronjob test
Ilyesbdlala Jun 13, 2023
9c4fa1c
#150 Added a go tag to seperate between slow tests and fast tests
Ilyesbdlala Jun 16, 2023
ede2c2d
#150 Removed fakeClock definition since it's no longer used
Ilyesbdlala Jul 11, 2023
2ddcfc1
#150 removed unused library in operator test suite `time`
Ilyesbdlala Jul 11, 2023
4ae1326
#150 Added k8s events to the parsing and handling schedule/interval …
Ilyesbdlala Jul 14, 2023
21fc70b
#150 Added missing rbac permissions create and patch k8s events
Ilyesbdlala Jul 14, 2023
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
12 changes: 11 additions & 1 deletion operator/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "Start Operator (minio)",
"type": "go",
"request": "launch",
"mode": "auto",
"mode": "debug",
"program": "main.go",
"env": {
"MINIO_ACCESS_KEY": "minioadmin",
Expand All @@ -26,6 +26,16 @@
"request": "launch",
"mode": "auto",
"program": "main.go"
},
{
"name": "Debug Unit Tests",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/controllers/execution",
"args": ["-test.v"],
//"preLaunchTask": "makefileMagic",
"env": {"KUBEBUILDER_ASSETS": "${workspaceFolder}/testbin/bin"}
}
]
}
11 changes: 11 additions & 0 deletions operator/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "makefileMagic",
"command": "bash",
"args": ["-c", "source ${workspaceFolder}/testbin/setup-envtest.sh && fetch_envtest_tools ${workspaceFolder}/testbin && setup_envtest_env ${workspaceFolder}/testbin"],
"type": "shell"
},
]
}
8 changes: 6 additions & 2 deletions operator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ vet: ## Run go vet against code.

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -tags="fast slow" ./... -coverprofile cover.out

.PHONY: test-fast
test-fast: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -tags="fast" ./... -coverprofile cover.out

.PHONY: view-coverage
view-coverage:
Expand Down Expand Up @@ -144,7 +148,7 @@ helm-deploy:
--set="image.pullPolicy=IfNotPresent" \
--set="lurker.image.repository=docker.io/$(IMG_NS)/$(LURKER_IMG)" \
--set="lurker.image.tag=$(IMG_TAG)" \
--set="lurker.image.pullPolicy=IfNotPresent"
--set="lurker.image.pullPolicy=IfNotPresent" \
--set="minio.auth.rootUser = $(MINIO_ROOT_USER)" \
--set="minio.auth.rootPassword = $(MINIO_ROOT_PASSWORD)"

Expand Down
6 changes: 6 additions & 0 deletions operator/apis/execution/v1/scheduledscan_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ type ScheduledScanSpec struct {

// Interval describes how often the scan should be repeated
// Examples: '12h', '30m'
// +kubebuilder:validation:Optional
Interval metav1.Duration `json:"interval"`

// The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
// +kubebuilder:validation:Optional
Schedule string `json:"schedule"`

// SuccessfulJobsHistoryLimit determines how many past Scans will be kept until the oldest one will be deleted, defaults to 3. When set to 0, Scans will be deleted directly after completion
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Minimum=0
Expand Down Expand Up @@ -59,6 +64,7 @@ type ScheduledScanStatus struct {
// +kubebuilder:printcolumn:name="UID",type=string,JSONPath=`.metadata.uid`,description="K8s Resource UID",priority=1
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.scanSpec.scanType`,description="Scan Type"
// +kubebuilder:printcolumn:name="Interval",type=string,JSONPath=`.spec.interval`,description="Interval"
// +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=`.spec.schedule`,description="Schedule"
// +kubebuilder:printcolumn:name="Findings",type=string,JSONPath=`.status.findings.count`,description="Total Finding Count"
// +kubebuilder:printcolumn:name="Parameters",type=string,JSONPath=`.spec.scanSpec.parameters`,description="Arguments passed to the Scanner",priority=1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ spec:
jsonPath: .spec.interval
name: Interval
type: string
- description: Schedule
jsonPath: .spec.schedule
name: Schedule
type: string
- description: Total Finding Count
jsonPath: .status.findings.count
name: Findings
Expand Down Expand Up @@ -4240,6 +4244,9 @@ spec:
type: object
type: array
type: object
schedule:
description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
type: string
successfulJobsHistoryLimit:
description: SuccessfulJobsHistoryLimit determines how many past Scans
will be kept until the oldest one will be deleted, defaults to 3.
Expand All @@ -4248,7 +4255,6 @@ spec:
minimum: 0
type: integer
required:
- interval
- scanSpec
type: object
status:
Expand Down
7 changes: 7 additions & 0 deletions operator/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- batch
resources:
Expand Down
3 changes: 3 additions & 0 deletions operator/controllers/execution/scantype_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ScanTypeReconciler struct {
// +kubebuilder:rbac:groups="execution.securecodebox.io",resources=scheduledscans,verbs=get;list;watch;create;update;patch
// +kubebuilder:rbac:groups="execution.securecodebox.io/status",resources=scheduledscans,verbs=get;update;patch

// Allows the ScanType Controller to create and patch Events
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// Reconcile compares the Service object against the state of the cluster and updates both if needed
func (r *ScanTypeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log
Expand Down
17 changes: 10 additions & 7 deletions operator/controllers/execution/scantype_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//
// SPDX-License-Identifier: Apache-2.0

//go:build fast
// +build fast

package controllers

import (
Expand Down Expand Up @@ -33,10 +36,10 @@ var _ = Describe("ScanType controller", func() {

createNamespace(ctx, namespace)
createScanType(ctx, namespace)
scheduledScan := createScheduledScan(ctx, namespace, true)
scheduledScan := createScheduledScanWithInterval(ctx, namespace, true)

// ensure that the ScheduledScan has been triggered
waitForScheduledScanToBeTriggered(ctx, namespace)
waitForScheduledScanToBeTriggered(ctx, namespace, timeout)
k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
initialExecutionTime := *scheduledScan.Status.LastScheduleTime

Expand Down Expand Up @@ -74,10 +77,10 @@ var _ = Describe("ScanType controller", func() {

createNamespace(ctx, namespace)
createScanType(ctx, namespace)
scheduledScan := createScheduledScan(ctx, namespace, true)
scheduledScan := createScheduledScanWithInterval(ctx, namespace, true)

// ensure that the ScheduledScan has been triggered
waitForScheduledScanToBeTriggered(ctx, namespace)
waitForScheduledScanToBeTriggered(ctx, namespace, timeout)
k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
initialExecutionTime := *scheduledScan.Status.LastScheduleTime

Expand All @@ -104,10 +107,10 @@ var _ = Describe("ScanType controller", func() {

createNamespace(ctx, namespace)
createScanType(ctx, namespace)
scheduledScan := createScheduledScan(ctx, namespace, false)
scheduledScan := createScheduledScanWithInterval(ctx, namespace, false)

// ensure that the ScheduledScan has been triggered
waitForScheduledScanToBeTriggered(ctx, namespace)
waitForScheduledScanToBeTriggered(ctx, namespace, timeout)
k8sClient.Get(ctx, types.NamespacedName{Name: "test-scan", Namespace: namespace}, &scheduledScan)
initialExecutionTime := *scheduledScan.Status.LastScheduleTime

Expand Down Expand Up @@ -139,7 +142,7 @@ var _ = Describe("ScanType controller", func() {
})
})

func waitForScheduledScanToBeTriggered(ctx context.Context, namespace string) {
func waitForScheduledScanToBeTriggered(ctx context.Context, namespace string, timeout time.Duration) {
var scheduledScan executionv1.ScheduledScan
By("Wait for ScheduledScan to trigger the initial Scan")
Eventually(func() bool {
Expand Down
58 changes: 50 additions & 8 deletions operator/controllers/execution/scheduledscan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (
"time"

"github.com/go-logr/logr"
"github.com/robfig/cron"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand All @@ -31,15 +33,19 @@ var (
// ScheduledScanReconciler reconciles a ScheduledScan object
type ScheduledScanReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
}

// +kubebuilder:rbac:groups=execution.securecodebox.io,resources=scheduledscans,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=execution.securecodebox.io,resources=scheduledscans/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=execution.securecodebox.io,resources=scans,verbs=get;list;create
// +kubebuilder:rbac:groups=execution.securecodebox.io,resources=scans/status,verbs=get

// Allows the ScheduledScan Controller to create and patch Events
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// Reconcile comapares the ScheduledScan Resource with the State of the Cluster and updates both accordingly
func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("scheduledscan", req.NamespacedName)
Expand Down Expand Up @@ -100,11 +106,10 @@ func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}

// Calculate the next schedule
var nextSchedule time.Time
if scheduledScan.Status.LastScheduleTime != nil {
nextSchedule = scheduledScan.Status.LastScheduleTime.Add(scheduledScan.Spec.Interval.Duration)
} else {
nextSchedule = time.Now().Add(-1 * time.Second)
nextSchedule, err := getNextSchedule(r, scheduledScan, time.Now())
if err != nil {
log.Error(err, "Unable to calculate next schedule")
return ctrl.Result{}, err
}

// check if it is time to start the next Scan
Expand Down Expand Up @@ -156,12 +161,48 @@ func (r *ScheduledScanReconciler) Reconcile(ctx context.Context, req ctrl.Reques
}

// Recalculate next schedule
nextSchedule = time.Now().Add(scheduledScan.Spec.Interval.Duration)
nextSchedule, err = getNextSchedule(r, scheduledScan, time.Now())
}

return ctrl.Result{RequeueAfter: nextSchedule.Sub(time.Now())}, nil
}

func getNextSchedule(r *ScheduledScanReconciler, scheduledScan executionv1.ScheduledScan, now time.Time) (next time.Time, err error) {
// check if the Cron schedule is set
if scheduledScan.Spec.Schedule != "" {
sched, err := cron.ParseStandard(scheduledScan.Spec.Schedule)
if err != nil {
r.Recorder.Event(&scheduledScan, "Warning", "ScheduleParseError", fmt.Sprintf("Unparseable schedule %q: %v", scheduledScan.Spec.Schedule, err))
return time.Time{}, fmt.Errorf("Unparseable schedule %q: %v", scheduledScan.Spec.Schedule, err)
}

// for optimization purposes, cheat a bit and start from our last observed run time
// we could reconstitute this here, but there's not much point, since we've
// just updated it.
var earliestTime time.Time
if scheduledScan.Status.LastScheduleTime != nil {
earliestTime = scheduledScan.Status.LastScheduleTime.Time
} else {
earliestTime = scheduledScan.ObjectMeta.CreationTimestamp.Time
}
if earliestTime.After(now) {
return sched.Next(now), nil
}
return sched.Next(earliestTime), nil
}
if scheduledScan.Spec.Interval.Duration > 0 {
var nextSchedule time.Time
if scheduledScan.Status.LastScheduleTime != nil {
nextSchedule = scheduledScan.Status.LastScheduleTime.Add(scheduledScan.Spec.Interval.Duration)
} else {
nextSchedule = time.Now().Add(-1 * time.Second)
}
return nextSchedule, nil
}
r.Recorder.Event(&scheduledScan, "Warning", "NoScheduleOrInterval", "No valid schedule or interval found")
return time.Time{}, fmt.Errorf("No schedule or interval found")
}

// Copy over securecodebox.io annotations from the scheduledScan to the created scan
func getAnnotationsForScan(scheduledScan executionv1.ScheduledScan) map[string]string {
annotations := map[string]string{}
Expand Down Expand Up @@ -212,6 +253,7 @@ func (r *ScheduledScanReconciler) deleteOldScans(scans []executionv1.Scan, maxCo

// SetupWithManager sets up the controller and initializes every thing it needs
func (r *ScheduledScanReconciler) SetupWithManager(mgr ctrl.Manager) error {
// set up a real clock, since we're not in a test
ctx := context.Background()
if err := mgr.GetFieldIndexer().IndexField(ctx, &executionv1.Scan{}, ownerKey, func(rawObj client.Object) []string {
// grab the job object, extract the owner...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

//go:build slow
// +build slow

package controllers

import (
"context"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
executionv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
//+kubebuilder:scaffold:imports
)

var _ = Describe("ScheduledScan controller", func() {
Context("A Scan is triggred due to a Scheduled Scan with Schedule in Spec", func() {
It("The ScheduledScan's should be triggered according to the Schedule", func() {
ctx := context.Background()
namespace := "scantype-multiple-scheduled-scan-triggerd-test-schedule"

createNamespace(ctx, namespace)
createScanType(ctx, namespace)
scheduledScan := createScheduledScanWithSchedule(ctx, namespace, true)

var scanlist executionv1.ScanList

// ensure that the ScheduledScan has been triggered
waitForScheduledScanToBeTriggered(ctx, namespace, 90*time.Second)
k8sClient.List(ctx, &scanlist, client.InNamespace(namespace))

Expect(scheduledScan.Spec.Schedule).Should(Equal("*/1 * * * *"))
Expect(scanlist.Items).Should(HaveLen(1))
})
})
})
10 changes: 7 additions & 3 deletions operator/controllers/execution/scheduledscan_controller_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

//go:build fast
// +build fast

package controllers

import (
Expand Down Expand Up @@ -64,18 +68,18 @@ var _ = Describe("ScheduledScan controller", func() {
}
})
})
Context("A Scan is triggred due to a Scheduled Scan", func() {
Context("A Scan is triggred due to a Scheduled Scan with Interval in Spec", func() {
It("The ScheduledScan's Finding Summary shoud be updated of with the results of the successful Scan", func() {
ctx := context.Background()
namespace := "scantype-multiple-scheduled-scan-triggerd-test"

createNamespace(ctx, namespace)
createScanType(ctx, namespace)
scheduledScan := createScheduledScan(ctx, namespace, true)
scheduledScan := createScheduledScanWithInterval(ctx, namespace, true)

var scanlist executionv1.ScanList
// ensure that the ScheduledScan has been triggered
waitForScheduledScanToBeTriggered(ctx, namespace)
waitForScheduledScanToBeTriggered(ctx, namespace, timeout)
k8sClient.List(ctx, &scanlist, client.InNamespace(namespace))

Expect(scanlist.Items).Should(HaveLen(1))
Expand Down
7 changes: 4 additions & 3 deletions operator/controllers/execution/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ var _ = BeforeSuite(func() {
Log: ctrl.Log.WithName("controllers").WithName("ScanTypeController"),
}).SetupWithManager(k8sManager)
err = (&ScheduledScanReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ScheduledScanController"),
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
Recorder: k8sManager.GetEventRecorderFor("ScheduledScanController"),
Log: ctrl.Log.WithName("controllers").WithName("ScheduledScanController"),
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

Expand Down
Loading