diff --git a/cmd/main.go b/cmd/main.go index 53f6d48d7..7cb1ab3e7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ func main() { } pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + config.DefaultMutableFeatureGate.AddFlag(pflag.CommandLine) pflag.Parse() logger := zap.New(zap.UseFlagOptions(&opts)) diff --git a/go.mod b/go.mod index d87b3c887..667861cd4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 + k8s.io/component-base v0.30.2 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/cli-utils v0.37.2 @@ -29,6 +30,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect diff --git a/go.sum b/go.sum index 1ae3735c2..b1fca0687 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -231,6 +233,8 @@ k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/component-base v0.30.2 h1:pqGBczYoW1sno8q9ObExUqrYSKhtE5rW3y6gX88GZII= +k8s.io/component-base v0.30.2/go.mod h1:yQLkQDrkK8J6NtP+MGJOws+/PPeEXNpwFixsUI7h/OE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/internal/config/feature/feature.go b/internal/config/feature/feature.go new file mode 100644 index 000000000..fb8c096a5 --- /dev/null +++ b/internal/config/feature/feature.go @@ -0,0 +1,19 @@ +package feature + +import ( + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/component-base/featuregate" + + "github.com/statnett/image-scanner-operator/internal/config" +) + +// PolicyReport will ensure PolicyReport resources are created for completed scan jobs. +const PolicyReport featuregate.Feature = "PolicyReport" + +func init() { + runtime.Must(config.DefaultMutableFeatureGate.Add(defaultFeatureGates)) +} + +var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + PolicyReport: {Default: false, PreRelease: featuregate.Alpha}, +} diff --git a/internal/config/feature_gate.go b/internal/config/feature_gate.go new file mode 100644 index 000000000..d1f1b0fc9 --- /dev/null +++ b/internal/config/feature_gate.go @@ -0,0 +1,15 @@ +package config + +import ( + "k8s.io/component-base/featuregate" +) + +var ( + // DefaultMutableFeatureGate is a mutable version of DefaultFeatureGate. + // Only top-level commands/options setup should make use of this. + DefaultMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() + + // DefaultFeatureGate is a shared global FeatureGate. + // Top-level commands/options setup that needs to modify this feature gate should use DefaultMutableFeatureGate. + DefaultFeatureGate featuregate.FeatureGate = DefaultMutableFeatureGate +) diff --git a/internal/controller/stas/containerimagescan_status.go b/internal/controller/stas/containerimagescan_status.go index 997494315..bde1903f7 100644 --- a/internal/controller/stas/containerimagescan_status.go +++ b/internal/controller/stas/containerimagescan_status.go @@ -56,24 +56,34 @@ func (p *containerImageScanStatusPatch) withCondition(c *metav1ac.ConditionApply return p } -func (p *containerImageScanStatusPatch) withScanJob(job *batchv1.Job) *containerImageScanStatusPatch { +func (p *containerImageScanStatusPatch) withScanJob(job *batchv1.Job, successful bool) *containerImageScanStatusPatch { + now := metav1.Now() + p.patch.Status. - WithLastScanTime(metav1.Now()). - WithLastScanJobUID(job.UID) + WithLastScanJobUID(job.UID). + WithLastScanTime(now) + + if successful { + p.patch.Status. + WithLastSuccessfulScanTime(now) + } return p } -func (p *containerImageScanStatusPatch) withCompletedScanJob(job *batchv1.Job, vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *containerImageScanStatusPatch { - p.minSeverity = &minSeverity - p.vulnerabilities = vulnerabilities - - now := metav1.Now() +func (p *containerImageScanStatusPatch) withSummary(summary *stasv1alpha1.VulnerabilitySummary) *containerImageScanStatusPatch { p.patch.Status. - WithVulnerabilitySummary(vulnerabilitySummary(vulnerabilities, minSeverity)). - WithLastScanTime(now). - WithLastScanJobUID(job.UID). - WithLastSuccessfulScanTime(now) + WithVulnerabilitySummary(stasv1alpha1ac.VulnerabilitySummary(). + WithSeverityCount(summary.SeverityCount). + WithFixedCount(summary.FixedCount). + WithUnfixedCount(summary.UnfixedCount)) + + return p +} + +func (p *containerImageScanStatusPatch) withResults(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *containerImageScanStatusPatch { + p.vulnerabilities = vulnerabilities + p.minSeverity = &minSeverity return p } @@ -94,11 +104,18 @@ func (p *containerImageScanStatusPatch) apply(ctx context.Context, c client.Clie var err error // Repeat until resource fits in api-server by increasing minimum severity on failure. for severity := *p.minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ { - p.patch.Status.Vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity) + var vulnerabilities []stasv1alpha1.Vulnerability + + vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity) if err != nil { return err } + p.patch.Status.Vulnerabilities = make([]stasv1alpha1ac.VulnerabilityApplyConfiguration, len(vulnerabilities)) + for i, v := range vulnerabilities { + p.patch.Status.Vulnerabilities[i] = *vulnerabilityPatch(v) + } + err = c.Status().Patch(ctx, p.cis, applyPatch{p.patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) if !isResourceTooLargeError(err) { break diff --git a/internal/controller/stas/policy_report.go b/internal/controller/stas/policy_report.go new file mode 100644 index 000000000..800be398d --- /dev/null +++ b/internal/controller/stas/policy_report.go @@ -0,0 +1,134 @@ +package stas + +import ( + "context" + "fmt" + "maps" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + policyv1alpha2 "sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2" + + stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" + policyv1alpha2ac "github.com/statnett/image-scanner-operator/internal/wg-policy/applyconfiguration/wgpolicyk8s.io/v1alpha2" +) + +func newPolicyReportPatch(cis *stasv1alpha1.ContainerImageScan) *policyReportPatch { + return &policyReportPatch{ + cis: cis, + patch: policyv1alpha2ac.PolicyReport(cis.Name, cis.Namespace). + WithScope( + corev1.ObjectReference{ + Kind: cis.Spec.Workload.Kind, + Name: cis.Spec.Workload.Name, + }, + ), + } +} + +type policyReportPatch struct { + cis *stasv1alpha1.ContainerImageScan + patch *policyv1alpha2ac.PolicyReportApplyConfiguration + vulnerabilities []stasv1alpha1.Vulnerability + minSeverity stasv1alpha1.Severity +} + +func (p *policyReportPatch) withSummary(summary *stasv1alpha1.VulnerabilitySummary) *policyReportPatch { + p.patch. + WithSummary(policyv1alpha2ac.PolicyReportSummary(). + WithSkip(int(summary.SeverityCount[stasv1alpha1.SeverityUnknown.String()])). + WithWarn(int(summary.SeverityCount[stasv1alpha1.SeverityLow.String()] + summary.SeverityCount[stasv1alpha1.SeverityMedium.String()])). + WithFail(int(summary.SeverityCount[stasv1alpha1.SeverityHigh.String()] + summary.SeverityCount[stasv1alpha1.SeverityCritical.String()]))) + + return p +} + +func (p *policyReportPatch) withResults(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *policyReportPatch { + p.vulnerabilities = vulnerabilities + p.minSeverity = minSeverity + + return p +} + +func (p *policyReportPatch) apply(ctx context.Context, c client.Client, scheme *runtime.Scheme) error { + if err := SetControllerReference(p.cis, p.patch.ObjectMetaApplyConfiguration, scheme); err != nil { + return err + } + + var err error + // Repeat until resource fits in api-server by increasing minimum severity on failure. + for severity := p.minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ { + var vulnerabilities []stasv1alpha1.Vulnerability + + vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity) + if err != nil { + return err + } + + p.patch.Results = make([]policyv1alpha2ac.PolicyReportResultApplyConfiguration, len(vulnerabilities)) + for i, v := range vulnerabilities { + p.patch.Results[i] = *policyReportResultPatch(v) + } + + report := &policyv1alpha2.PolicyReport{} + report.Name = *p.patch.Name + report.Namespace = *p.patch.Namespace + + err = c.Patch(ctx, report, applyPatch{p.patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) + if !isResourceTooLargeError(err) { + break + } + } + + if err != nil { + return fmt.Errorf("when applying policy report: %w", err) + } + + return nil +} + +func policyReportResultPatch(v stasv1alpha1.Vulnerability) *policyv1alpha2ac.PolicyReportResultApplyConfiguration { + properties := map[string]string{ + "pkgName": v.PkgName, + "pkgPath": v.PkgPath, + "installedVersion": v.InstalledVersion, + "fixedVersion": v.FixedVersion, + "primaryURL": v.PrimaryURL, + } + + // Remove properties with empty values to compact report + maps.DeleteFunc(properties, func(k string, v string) bool { + return len(v) == 0 + }) + + return policyv1alpha2ac.PolicyReportResult(). + WithCategory("vulnerability scan"). + WithSource("image-scanner"). + WithPolicy(v.VulnerabilityID). + WithResult(mapPolicyResult(v.Severity)). + WithSeverity(mapPolicyResultSeverity(v.Severity)). + WithDescription(v.Title). + WithProperties(properties) +} + +func mapPolicyResultSeverity(severity string) policyv1alpha2.PolicyResultSeverity { + switch severity { + case "UNKNOWN": + return "" + default: + return policyv1alpha2.PolicyResultSeverity(strings.ToLower(severity)) + } +} + +func mapPolicyResult(severity string) policyv1alpha2.PolicyResult { + switch severity { + case "UNKNOWN": + return "skip" + case "LOW", "MEDIUM": + return "warn" + default: + return "fail" + } +} diff --git a/internal/controller/stas/scan_job_controller.go b/internal/controller/stas/scan_job_controller.go index b50ee3208..6b1bd6de3 100644 --- a/internal/controller/stas/scan_job_controller.go +++ b/internal/controller/stas/scan_job_controller.go @@ -26,8 +26,8 @@ import ( "sigs.k8s.io/json" stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" - stasv1alpha1ac "github.com/statnett/image-scanner-operator/internal/client/applyconfiguration/stas/v1alpha1" "github.com/statnett/image-scanner-operator/internal/config" + "github.com/statnett/image-scanner-operator/internal/config/feature" "github.com/statnett/image-scanner-operator/internal/controller" staserrors "github.com/statnett/image-scanner-operator/internal/errors" "github.com/statnett/image-scanner-operator/internal/pod" @@ -168,7 +168,7 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch WithReason(stasv1alpha1.ReasonScanReportDecodeError). WithMessage(fmt.Sprintf("error decoding scan report JSON from job '%s': %s", job.Name, err)), ). - withScanJob(job). + withScanJob(job, false). apply(ctx, r.Client) } @@ -182,15 +182,30 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch } } + summary := vulnerabilitySummary(vulnerabilities, minSeverity) + + if config.DefaultFeatureGate.Enabled(feature.PolicyReport) { + err = newPolicyReportPatch(cis). + withResults(vulnerabilities, minSeverity). + withSummary(summary). + apply(ctx, r.Client, r.Scheme) + if err != nil { + return err + } + } + return newContainerImageStatusPatch(cis). - withCompletedScanJob(job, vulnerabilities, minSeverity). + withScanJob(job, true). + withResults(vulnerabilities, minSeverity). + withSummary(summary). apply(ctx, r.Client) } func isResourceTooLargeError(err error) bool { - return apierrors.IsInternalError(err) && - (strings.Contains(err.Error(), "ResourceExhausted") || - strings.Contains(err.Error(), "request is too large")) + return apierrors.IsRequestEntityTooLargeError(err) || + apierrors.IsInternalError(err) && + (strings.Contains(err.Error(), "ResourceExhausted") || + strings.Contains(err.Error(), "request is too large")) } func (r *ScanJobReconciler) reconcileFailedJob(ctx context.Context, job *batchv1.Job, log io.Reader, cis *stasv1alpha1.ContainerImageScan) error { @@ -207,7 +222,7 @@ func (r *ScanJobReconciler) reconcileFailedJob(ctx context.Context, job *batchv1 WithReason("Error"). WithMessage(string(logBytes)), ). - withScanJob(job). + withScanJob(job, false). apply(ctx, r.Client) } @@ -339,8 +354,8 @@ func (r *ScanJobReconciler) getScanJobLogs(ctx context.Context, job *batchv1.Job return r.GetLogs(ctx, client.ObjectKeyFromObject(&jobPod), trivy.ScanJobContainerName) } -func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) ([]stasv1alpha1ac.VulnerabilityApplyConfiguration, error) { - var filtered []stasv1alpha1ac.VulnerabilityApplyConfiguration +func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) ([]stasv1alpha1.Vulnerability, error) { + var filtered []stasv1alpha1.Vulnerability for _, v := range orig { severity, err := stasv1alpha1.NewSeverity(v.Severity) @@ -349,14 +364,14 @@ func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1 } if severity >= minSeverity { - filtered = append(filtered, *vulnerabilityPatch(v)) + filtered = append(filtered, v) } } return filtered, nil } -func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1ac.VulnerabilitySummaryApplyConfiguration { +func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1.VulnerabilitySummary { severityCount := make(map[string]int32) for severity := minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ { severityCount[severity.String()] = 0 @@ -374,8 +389,9 @@ func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSever } } - return stasv1alpha1ac.VulnerabilitySummary(). - WithSeverityCount(severityCount). - WithFixedCount(fixedCount). - WithUnfixedCount(unfixedCount) + return &stasv1alpha1.VulnerabilitySummary{ + SeverityCount: severityCount, + FixedCount: fixedCount, + UnfixedCount: unfixedCount, + } } diff --git a/internal/controller/stas/scan_job_controller_test.go b/internal/controller/stas/scan_job_controller_test.go index a19758111..8ccffa819 100644 --- a/internal/controller/stas/scan_job_controller_test.go +++ b/internal/controller/stas/scan_job_controller_test.go @@ -13,10 +13,12 @@ import ( "github.com/stretchr/testify/mock" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + policyv1alpha2 "sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2" stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" "github.com/statnett/image-scanner-operator/internal/trivy" @@ -25,7 +27,7 @@ import ( var _ = Describe("Scan Job controller", func() { const ( - timeout = 20 * time.Minute + timeout = 20 * time.Second interval = 100 * time.Millisecond ) @@ -34,7 +36,7 @@ var _ = Describe("Scan Job controller", func() { }) Context("When scan job is complete", func() { - It("should write scan results back to CIS status", func() { + It("should write scan results back to CIS status and create policy report", func() { // Create CIS cis := &stasv1alpha1.ContainerImageScan{} Expect(yaml.FromFile(path.Join("testdata", "scan-job-successful", "successful-scan-cis.yaml"), cis)).To(Succeed()) @@ -72,10 +74,28 @@ var _ = Describe("Scan Job controller", func() { UnfixedCount: 19, } Expect(cis.Status.VulnerabilitySummary).To(Equal(expectedVulnSummary)) + + // Check policy report exists with expected content + report := &policyv1alpha2.PolicyReport{} + report.Name = cis.Name + report.Namespace = cis.Namespace + Expect(komega.Get(report)()).To(Succeed()) + Expect(report.Results).To(Not(BeEmpty())) + Expect(report.Results).Should(HaveEach( + WithTransform(func(vulnerability policyv1alpha2.PolicyReportResult) map[string]string { + return vulnerability.Properties + }, + Not(BeEmpty()), + ), + )) + expectedSummary := policyv1alpha2.PolicyReportSummary{ + Fail: 19, + } + Expect(report.Summary).To(Equal(expectedSummary)) }) Context("and scan report is too big", func() { - It("should filter report by minimum severity", func() { + It("should filter report by minimum severity and create policy report", func() { // Create CIS cis := &stasv1alpha1.ContainerImageScan{} Expect(yaml.FromFile(path.Join("testdata", "scan-job-successful-long", "cis.yaml"), cis)).To(Succeed()) @@ -103,7 +123,7 @@ var _ = Describe("Scan Job controller", func() { Expect(cis.Status.Vulnerabilities).To(Not(BeEmpty())) Expect(cis.Status.Vulnerabilities).Should(HaveEach( WithTransform(func(vulnerability stasv1alpha1.Vulnerability) string { - return string(vulnerability.Severity) + return vulnerability.Severity }, SatisfyAny( Equal("CRITICAL"), @@ -123,10 +143,33 @@ var _ = Describe("Scan Job controller", func() { UnfixedCount: 5128, } Expect(cis.Status.VulnerabilitySummary).To(Equal(expectedVulnSummary)) + + // Check policy report exists with expected content + report := &policyv1alpha2.PolicyReport{} + report.Name = cis.Name + report.Namespace = cis.Namespace + Expect(komega.Get(report)()).To(Succeed()) + Expect(report.Results).To(Not(BeEmpty())) + Expect(report.Results).Should(HaveEach( + WithTransform(func(vulnerability policyv1alpha2.PolicyReportResult) string { + return string(vulnerability.Severity) + }, + SatisfyAny( + Equal("critical"), + Equal("high"), + ), + ), + )) + expectedSummary := policyv1alpha2.PolicyReportSummary{ + Fail: 3259, + Warn: 7059, + Skip: 77, + } + Expect(report.Summary).To(Equal(expectedSummary)) }) }) - Context("but scan report is invalid JSON", func() { + Context("but scan report is invalid JSON and NOT create policy report", func() { It("should report stalled condition", func() { // Create CIS cis := &stasv1alpha1.ContainerImageScan{} @@ -156,12 +199,18 @@ var _ = Describe("Scan Job controller", func() { Expect(condition.Status).To(Equal(metav1.ConditionTrue)) Expect(condition.Reason).To(Equal("ScanReportDecodeError")) Expect(condition.Message).To(Not(BeEmpty())) + + // Check policy report does NOT exist + report := &policyv1alpha2.PolicyReport{} + report.Name = cis.Name + report.Namespace = cis.Namespace + Expect(komega.Get(report)()).Should(WithTransform(errors.ReasonForError, Equal(metav1.StatusReasonNotFound))) }) }) }) Context("When scan job is failed", func() { - It("should write scan results back to CIS status", func() { + It("should write scan results back to CIS status and NOT create policy report", func() { // Create CIS cis := &stasv1alpha1.ContainerImageScan{} Expect(yaml.FromFile(path.Join("testdata", "scan-job-failed", "failed-scan-cis.yaml"), cis)).To(Succeed()) @@ -190,6 +239,12 @@ var _ = Describe("Scan Job controller", func() { Expect(condition.Status).To(Equal(metav1.ConditionTrue)) Expect(condition.Reason).To(Equal("Error")) Expect(condition.Message).To(Not(BeEmpty())) + + // Check policy report does NOT exist + report := &policyv1alpha2.PolicyReport{} + report.Name = cis.Name + report.Namespace = cis.Namespace + Expect(komega.Get(report)()).Should(WithTransform(errors.ReasonForError, Equal(metav1.StatusReasonNotFound))) }) }) }) diff --git a/internal/controller/stas/ssa_client.go b/internal/controller/stas/ssa_client.go index b65da2398..2ec1edac1 100644 --- a/internal/controller/stas/ssa_client.go +++ b/internal/controller/stas/ssa_client.go @@ -93,6 +93,38 @@ func NewConditionsPatch(existingConditions []metav1.Condition, conditions ...*me return conditions } +// SetControllerReference sets owner as a Controller OwnerReference on controlled. +// This is used for garbage collection of the controlled object and for +// reconciling the owner object on changes to controlled (with a Watch + EnqueueRequestForOwner). +func SetControllerReference(owner metav1.Object, controlled *metav1ac.ObjectMetaApplyConfiguration, scheme *runtime.Scheme) error { + // Validate the owner. + ro, ok := owner.(runtime.Object) + if !ok { + return fmt.Errorf("%T is not a runtime.Object, cannot call SetControllerReference", owner) + } + + if err := validateOwner(owner, controlled); err != nil { + return err + } + + gvk, err := apiutil.GVKForObject(ro, scheme) + if err != nil { + return err + } + + controlled.WithOwnerReferences( + metav1ac.OwnerReference(). + WithAPIVersion(gvk.GroupVersion().String()). + WithKind(gvk.Kind). + WithName(owner.GetName()). + WithUID(owner.GetUID()). + WithBlockOwnerDeletion(true). + WithController(true), + ) + + return nil +} + // SetOwnerReference is a helper method to make sure the given object contains an object reference to the object provided. // This allows you to declare that owner has a dependency on the object without specifying it as a controller. // If a reference to the same object already exists, it'll be overwritten with the newly provided version. diff --git a/internal/controller/stas/suite_test.go b/internal/controller/stas/suite_test.go index 74aa53fe5..c46281f93 100644 --- a/internal/controller/stas/suite_test.go +++ b/internal/controller/stas/suite_test.go @@ -26,9 +26,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + policyv1alpha2 "sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2" stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" "github.com/statnett/image-scanner-operator/internal/config" + "github.com/statnett/image-scanner-operator/internal/config/feature" "github.com/statnett/image-scanner-operator/internal/pod" ) @@ -61,9 +63,14 @@ var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.TODO()) + Expect(config.DefaultMutableFeatureGate.OverrideDefault(feature.PolicyReport, true)).To(Succeed()) + By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "config", "crd", "bases"), + filepath.Join("..", "..", "..", "config", "wg-policy", "crd"), + }, ErrorIfCRDPathMissing: true, } @@ -76,7 +83,7 @@ var _ = BeforeSuite(func() { Expect(appsv1.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(stasv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) Expect(batchv1.AddToScheme(scheme.Scheme)).To(Succeed()) - //+kubebuilder:scaffold:scheme + Expect(policyv1alpha2.AddToScheme(scheme.Scheme)).To(Succeed()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) diff --git a/internal/operator/operator.go b/internal/operator/operator.go index 865e4fbb2..613d282b2 100644 --- a/internal/operator/operator.go +++ b/internal/operator/operator.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/metrics/server" + policyv1alpha2 "sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2" stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" "github.com/statnett/image-scanner-operator/internal/config" @@ -39,9 +40,8 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - - //+kubebuilder:scaffold:scheme utilruntime.Must(stasv1alpha1.AddToScheme(scheme)) + utilruntime.Must(policyv1alpha2.AddToScheme(scheme)) } type Operator struct{}