diff --git a/deploy/static/trivy-operator.yaml b/deploy/static/trivy-operator.yaml index ea33e9bd2..517979317 100644 --- a/deploy/static/trivy-operator.yaml +++ b/deploy/static/trivy-operator.yaml @@ -2992,9 +2992,9 @@ metadata: app.kubernetes.io/managed-by: kubectl data: OPERATOR_LOG_DEV_MODE: "false" - OPERATOR_SCAN_JOB_TTL: "" + OPERATOR_SCAN_JOB_TTL: "30s" OPERATOR_SCAN_JOB_TIMEOUT: "5m" - OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT: "10" + OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT: "20" OPERATOR_CONCURRENT_NODE_COLLECTOR_LIMIT: "1" OPERATOR_SCAN_JOB_RETRY_AFTER: "30s" OPERATOR_BATCH_DELETE_LIMIT: "10" diff --git a/pkg/vulnerabilityreport/controller/scanjob.go b/pkg/vulnerabilityreport/controller/scanjob.go index cdf5956ad..dc6e038fb 100644 --- a/pkg/vulnerabilityreport/controller/scanjob.go +++ b/pkg/vulnerabilityreport/controller/scanjob.go @@ -357,6 +357,10 @@ func (r *ScanJobController) completedContainers(ctx context.Context, scanJob *ba } func (r *ScanJobController) deleteJob(ctx context.Context, job *batchv1.Job) error { + if job.Spec.TTLSecondsAfterFinished != nil { + return nil + } + err := r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)) if err != nil { if k8sapierror.IsNotFound(err) { diff --git a/tests/itest/helper/helper.go b/tests/itest/helper/helper.go index 3c4336589..4f93383b5 100644 --- a/tests/itest/helper/helper.go +++ b/tests/itest/helper/helper.go @@ -356,6 +356,48 @@ func (h *Helper) HasVulnerabilityReportOwnedBy(obj client.Object) func() (bool, } } +func (h *Helper) HasScanJobPodOwnedBy(obj client.Object) func() (bool, error) { + return func() (bool, error) { + gvk, err := apiutil.GVKForObject(obj, h.scheme) + if err != nil { + return false, err + } + var podList corev1.PodList + err = h.kubeClient.List(context.Background(), &podList, client.MatchingLabels{ + trivyoperator.LabelResourceKind: gvk.Kind, + trivyoperator.LabelResourceName: obj.GetName(), + trivyoperator.LabelResourceNamespace: obj.GetNamespace(), + }) + if err != nil { + return false, err + } + return len(podList.Items) == 1, nil + } +} + +func (h *Helper) GetScanJobPodOwnedBy(obj client.Object) func() (corev1.Pod, error) { + return func() (corev1.Pod, error) { + gvk, err := apiutil.GVKForObject(obj, h.scheme) + if err != nil { + return corev1.Pod{}, err + } + var podList corev1.PodList + err = h.kubeClient.List(context.Background(), &podList, client.MatchingLabels{ + trivyoperator.LabelResourceKind: gvk.Kind, + trivyoperator.LabelResourceName: obj.GetName(), + trivyoperator.LabelResourceNamespace: obj.GetNamespace(), + }) + if err != nil { + return corev1.Pod{}, err + } + + if len(podList.Items) == 0 { + return corev1.Pod{}, fmt.Errorf("no scan job pod found for owner %s/%s", obj.GetNamespace(), obj.GetName()) + } + return podList.Items[0], nil + } +} + func (h *Helper) HasConfigAuditReportOwnedBy(obj client.Object) func() (bool, error) { return func() (bool, error) { gvk, err := apiutil.GVKForObject(obj, h.scheme) diff --git a/tests/itest/trivy-operator/behavior/behavior.go b/tests/itest/trivy-operator/behavior/behavior.go index 31db4a600..af764f5e6 100644 --- a/tests/itest/trivy-operator/behavior/behavior.go +++ b/tests/itest/trivy-operator/behavior/behavior.go @@ -1,13 +1,10 @@ package behavior import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "context" + "errors" "time" - "github.com/aquasecurity/trivy-operator/tests/itest/helper" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -15,11 +12,17 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/rand" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/aquasecurity/trivy-operator/tests/itest/helper" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) // Inputs represents required inputs to shared behavior containers. type Inputs struct { AssertTimeout time.Duration + PollingInterval time.Duration PrimaryNamespace string PrimaryWorkloadPrefix string @@ -455,3 +458,63 @@ func ConfigurationCheckerBehavior(inputs *Inputs) func() { }) } } + +func VulnerabilityScanJobTTLBehavior(inputs *Inputs) func() { + return func() { + + Context("When unmanaged Pod is created", func() { + + var ctx context.Context + var pod *corev1.Pod + + BeforeAll(func() { + ctx = context.Background() + pod = helper.NewPod(). + WithRandomName("unmanaged-nginx"). + WithNamespace(inputs.PrimaryNamespace). + WithContainer("nginx", "nginx:1.16"). + Build() + + err := inputs.Create(ctx, pod) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should create VulnerabilityReport", func() { + Eventually(inputs.HasVulnerabilityReportOwnedBy(pod), inputs.AssertTimeout, inputs.PollingInterval).Should(BeTrue()) + }) + + It("Should keep ScanJob in completed state", func() { + Eventually(inputs.HasScanJobPodOwnedBy(pod), inputs.AssertTimeout, inputs.PollingInterval).Should(BeTrue()) + + Eventually(ctx, func() (string, error) { + scanJobPod, err := inputs.GetScanJobPodOwnedBy(pod)() + + if err != nil { + return "", err + } + + if scanJobPod.Status.ContainerStatuses == nil || len(scanJobPod.Status.ContainerStatuses) == 0 { + return "", errors.New("no container statuses found") + } + + containerStatus := scanJobPod.Status.ContainerStatuses[0] + if containerStatus.State.Terminated == nil { + return "", errors.New("container is not terminated") + } + + return containerStatus.State.Terminated.Reason, nil + }, inputs.AssertTimeout, inputs.PollingInterval).Should(Equal("Completed")) + }) + + AfterAll(func() { + err := inputs.Delete(ctx, pod) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should delete ScanJob after ttl expired", func() { + Eventually(inputs.HasScanJobPodOwnedBy(pod), inputs.AssertTimeout, inputs.PollingInterval).Should(BeFalse()) + }) + + }) + } +} diff --git a/tests/itest/trivy-operator/starboard_operator_test.go b/tests/itest/trivy-operator/starboard_operator_test.go index e03d31d98..2e9ff7b91 100644 --- a/tests/itest/trivy-operator/starboard_operator_test.go +++ b/tests/itest/trivy-operator/starboard_operator_test.go @@ -13,6 +13,8 @@ var _ = Describe("Trivy Operator", func() { // TODO Refactor to run this container in a separate test suite Describe("Configuration Checker", ConfigurationCheckerBehavior(&inputs)) + Describe("Vulnerability ScanJob", Ordered, VulnerabilityScanJobTTLBehavior(&inputs)) + // TODO Refactor to run this container in a separate test suite // Describe("CIS Kubernetes Benchmark", CISKubernetesBenchmarkBehavior(&inputs)) diff --git a/tests/itest/trivy-operator/suite_test.go b/tests/itest/trivy-operator/suite_test.go index cc77f1399..ea91279ac 100644 --- a/tests/itest/trivy-operator/suite_test.go +++ b/tests/itest/trivy-operator/suite_test.go @@ -64,6 +64,7 @@ var _ = BeforeSuite(func() { inputs = behavior.Inputs{ AssertTimeout: 5 * time.Minute, + PollingInterval: 5 * time.Second, PrimaryNamespace: corev1.NamespaceDefault, PrimaryWorkloadPrefix: "wordpress", Client: kubeClient,