diff --git a/internal/controller/stas/containerimagescan_controller.go b/internal/controller/stas/containerimagescan_controller.go index 3d124425..20e24dfd 100644 --- a/internal/controller/stas/containerimagescan_controller.go +++ b/internal/controller/stas/containerimagescan_controller.go @@ -8,9 +8,9 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -114,7 +114,6 @@ func (r *ContainerImageScanReconciler) reconcile(ctx context.Context, cis *stasv logf.FromContext(ctx).Info("Reconciling") result := ctrl.Result{} - cleanCis := cis.DeepCopy() scanJob, err := r.newScanJob(ctx, cis) if err != nil { @@ -133,18 +132,20 @@ func (r *ContainerImageScanReconciler) reconcile(ctx context.Context, cis *stasv return result, err } - condition := metav1.Condition{ - Type: string(kstatus.ConditionReconciling), - Status: metav1.ConditionTrue, - Reason: "ScanJobCreated", - Message: fmt.Sprintf("Job '%s' created to scan image.", scanJob.Name), - } - meta.SetStatusCondition(&cis.Status.Conditions, condition) - meta.RemoveStatusCondition(&cis.Status.Conditions, string(kstatus.ConditionStalled)) + condition := metav1ac.Condition(). + WithType(string(kstatus.ConditionReconciling)). + WithStatus(metav1.ConditionTrue). + WithReason("ScanJobCreated"). + WithMessage(fmt.Sprintf("Job '%s' created to scan image.", scanJob.Name)) + patch := newContainerImageStatusPatch(cis) + patch.Status. + WithConditions(NewConditionsPatch(cis.Status.Conditions, condition)...) - cis.Status.ObservedGeneration = cis.Generation + if err := upgradeStatusManagedFields(ctx, r.Client, cis); err != nil { + return result, err + } - return result, r.Status().Patch(ctx, cis, client.MergeFrom(cleanCis)) + return result, r.Status().Patch(ctx, cis, applyPatch{patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) } func (r *ContainerImageScanReconciler) newScanJob(ctx context.Context, cis *stasv1alpha1.ContainerImageScan) (*batchv1.Job, error) { diff --git a/internal/controller/stas/containerimagescan_status.go b/internal/controller/stas/containerimagescan_status.go new file mode 100644 index 00000000..6b3258c1 --- /dev/null +++ b/internal/controller/stas/containerimagescan_status.go @@ -0,0 +1,45 @@ +package stas + +import ( + stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" + stasv1alpha1ac "github.com/statnett/image-scanner-operator/internal/client/applyconfiguration/stas/v1alpha1" +) + +func newContainerImageStatusPatch(cis *stasv1alpha1.ContainerImageScan) *stasv1alpha1ac.ContainerImageScanApplyConfiguration { + status := stasv1alpha1ac.ContainerImageScanStatus(). + WithObservedGeneration(cis.Generation). + WithLastScanJobUID(cis.Status.LastScanJobUID) + status.LastScanTime = cis.Status.LastScanTime + status.LastSuccessfulScanTime = cis.Status.LastSuccessfulScanTime + + if cis.Status.VulnerabilitySummary != nil { + status = status.WithVulnerabilitySummary( + stasv1alpha1ac.VulnerabilitySummary(). + WithSeverityCount(cis.Status.VulnerabilitySummary.SeverityCount). + WithFixedCount(cis.Status.VulnerabilitySummary.FixedCount). + WithUnfixedCount(cis.Status.VulnerabilitySummary.UnfixedCount), + ) + } + + if len(cis.Status.Vulnerabilities) > 0 { + status.Vulnerabilities = make([]stasv1alpha1ac.VulnerabilityApplyConfiguration, len(cis.Status.Vulnerabilities)) + for i, v := range cis.Status.Vulnerabilities { + status.Vulnerabilities[i] = *vulnerabilityPatch(v) + } + } + + return stasv1alpha1ac.ContainerImageScan(cis.Name, cis.Namespace). + WithStatus(status) +} + +func vulnerabilityPatch(v stasv1alpha1.Vulnerability) *stasv1alpha1ac.VulnerabilityApplyConfiguration { + return stasv1alpha1ac.Vulnerability(). + WithVulnerabilityID(v.VulnerabilityID). + WithPkgName(v.PkgName). + WithInstalledVersion(v.InstalledVersion). + WithSeverity(v.Severity). + WithPkgPath(v.PkgPath). + WithFixedVersion(v.FixedVersion). + WithTitle(v.Title). + WithPrimaryURL(v.PrimaryURL) +} diff --git a/internal/controller/stas/scan_job_controller.go b/internal/controller/stas/scan_job_controller.go index 92d229c7..daad5144 100644 --- a/internal/controller/stas/scan_job_controller.go +++ b/internal/controller/stas/scan_job_controller.go @@ -12,11 +12,10 @@ import ( corev1 "k8s.io/api/core/v1" eventsv1 "k8s.io/api/events/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" + metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -27,6 +26,7 @@ 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/controller" staserrors "github.com/statnett/image-scanner-operator/internal/errors" @@ -160,20 +160,22 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch err := json.NewDecoderCaseSensitivePreserveInts(log).Decode(&vulnerabilities) if err != nil { - cleanCis := cis.DeepCopy() - - condition := metav1.Condition{ - Type: string(kstatus.ConditionStalled), - Status: metav1.ConditionTrue, - Reason: stasv1alpha1.ReasonScanReportDecodeError, - Message: fmt.Sprintf("error decoding scan report JSON from job '%s': %s", job.Name, err), + condition := metav1ac.Condition(). + WithType(string(kstatus.ConditionStalled)). + WithStatus(metav1.ConditionTrue). + WithReason(stasv1alpha1.ReasonScanReportDecodeError). + WithMessage(fmt.Sprintf("error decoding scan report JSON from job '%s': %s", job.Name, err)) + patch := newContainerImageStatusPatch(cis) + patch.Status. + WithConditions(NewConditionsPatch(cis.Status.Conditions, condition)...). + WithLastScanTime(metav1.Now()). + WithLastScanJobUID(job.UID) + + if err := upgradeStatusManagedFields(ctx, r.Client, cis); err != nil { + return err } - meta.SetStatusCondition(&cis.Status.Conditions, condition) - meta.RemoveStatusCondition(&cis.Status.Conditions, string(kstatus.ConditionReconciling)) - cis.Status.LastScanTime = ptr.To(metav1.Now()) - cis.Status.LastScanJobUID = job.UID - err = r.Status().Patch(ctx, cis, client.MergeFrom(cleanCis)) + err = r.Status().Patch(ctx, cis, applyPatch{patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) if err != nil { logf.FromContext(ctx).Error(err, "when patching status", "condition", condition) } @@ -195,25 +197,28 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch } func (r *ScanJobReconciler) updateCISStatus(ctx context.Context, job *batchv1.Job, cis *stasv1alpha1.ContainerImageScan, vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) error { - cleanCis := cis.DeepCopy() now := metav1.Now() - cis.Status.VulnerabilitySummary = vulnerabilitySummary(vulnerabilities, minSeverity) - // Clear any conditions since we now have a successful scan report - cis.Status.Conditions = nil - cis.Status.LastScanTime = &now - cis.Status.LastScanJobUID = job.UID - cis.Status.LastSuccessfulScanTime = &now + patch := newContainerImageStatusPatch(cis) + patch.Status. + WithVulnerabilitySummary(vulnerabilitySummary(vulnerabilities, minSeverity)). + WithLastScanTime(now). + WithLastScanJobUID(job.UID). + WithLastSuccessfulScanTime(now) + + if err := upgradeStatusManagedFields(ctx, r.Client, cis); err != nil { + return err + } var err error // Repeat until resource fits in api-server by increasing minimum severity on failure. for severity := minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ { - cis.Status.Vulnerabilities, err = filterVulnerabilities(vulnerabilities, severity) + patch.Status.Vulnerabilities, err = filterVulnerabilities(vulnerabilities, severity) if err != nil { return err } - err = r.Status().Patch(ctx, cis, client.MergeFrom(cleanCis)) + err = r.Status().Patch(ctx, cis, applyPatch{patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) if err == nil || !isResourceTooLargeError(err) { return err } @@ -229,27 +234,27 @@ func isResourceTooLargeError(err error) bool { } func (r *ScanJobReconciler) reconcileFailedJob(ctx context.Context, job *batchv1.Job, log io.Reader, cis *stasv1alpha1.ContainerImageScan) error { - cleanCis := cis.DeepCopy() - logBytes, err := io.ReadAll(log) if err != nil { return err } - condition := metav1.Condition{ - Type: string(kstatus.ConditionStalled), - Status: metav1.ConditionTrue, - Reason: "Error", - Message: string(logBytes), + condition := metav1ac.Condition(). + WithType(string(kstatus.ConditionStalled)). + WithStatus(metav1.ConditionTrue). + WithReason("Error"). + WithMessage(string(logBytes)) + patch := newContainerImageStatusPatch(cis) + patch.Status. + WithConditions(NewConditionsPatch(cis.Status.Conditions, condition)...). + WithLastScanTime(metav1.Now()). + WithLastScanJobUID(job.UID) + + if err := upgradeStatusManagedFields(ctx, r.Client, cis); err != nil { + return err } - meta.SetStatusCondition(&cis.Status.Conditions, condition) - meta.RemoveStatusCondition(&cis.Status.Conditions, string(kstatus.ConditionReconciling)) - now := metav1.Now() - cis.Status.LastScanTime = &now - cis.Status.LastScanJobUID = job.UID - - err = r.Status().Patch(ctx, cis, client.MergeFrom(cleanCis)) + err = r.Status().Patch(ctx, cis, applyPatch{patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner) if err != nil { logf.FromContext(ctx).Error(err, "when patching status", "condition", condition) } @@ -385,8 +390,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) ([]stasv1alpha1.Vulnerability, error) { - var filtered []stasv1alpha1.Vulnerability +func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) ([]stasv1alpha1ac.VulnerabilityApplyConfiguration, error) { + var filtered []stasv1alpha1ac.VulnerabilityApplyConfiguration for _, v := range orig { severity, err := stasv1alpha1.NewSeverity(v.Severity) @@ -395,14 +400,14 @@ func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1 } if severity >= minSeverity { - filtered = append(filtered, v) + filtered = append(filtered, *vulnerabilityPatch(v)) } } return filtered, nil } -func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1.VulnerabilitySummary { +func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1ac.VulnerabilitySummaryApplyConfiguration { severityCount := make(map[string]int32) for severity := minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ { severityCount[severity.String()] = 0 @@ -420,9 +425,8 @@ func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSever } } - return &stasv1alpha1.VulnerabilitySummary{ - SeverityCount: severityCount, - FixedCount: fixedCount, - UnfixedCount: unfixedCount, - } + return stasv1alpha1ac.VulnerabilitySummary(). + WithSeverityCount(severityCount). + WithFixedCount(fixedCount). + WithUnfixedCount(unfixedCount) } diff --git a/internal/controller/stas/ssa_client.go b/internal/controller/stas/ssa_client.go index d3b3943a..b65da239 100644 --- a/internal/controller/stas/ssa_client.go +++ b/internal/controller/stas/ssa_client.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "fmt" + "time" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -76,6 +78,21 @@ func (fieldValidationStrict) ApplyToSubResourcePatch(opts *client.SubResourcePat opts.Raw.FieldValidation = "Strict" } +func NewConditionsPatch(existingConditions []metav1.Condition, conditions ...*metav1ac.ConditionApplyConfiguration) []*metav1ac.ConditionApplyConfiguration { + for _, condition := range conditions { + if condition.LastTransitionTime.IsZero() { + existingCondition := meta.FindStatusCondition(existingConditions, *condition.Type) + if existingCondition != nil && existingCondition.Status == *condition.Status { + condition.WithLastTransitionTime(existingCondition.LastTransitionTime) + } else { + condition.WithLastTransitionTime(metav1.NewTime(time.Now())) + } + } + } + + return conditions +} + // 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. @@ -122,8 +139,9 @@ func validateOwner(owner metav1.Object, object *metav1ac.ObjectMetaApplyConfigur return nil } -func upgradeManagedFields(ctx context.Context, r client.Client, obj client.Object, fieldOwner client.FieldOwner, opts ...csaupgrade.Option) error { - if err := r.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil { +// upgradeManagedFields upgrades the managed fields owned by fieldOwner from CSA to SSA. +func upgradeManagedFields(ctx context.Context, c client.Client, obj client.Object, opts ...csaupgrade.Option) error { + if err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil { // If not found, there is nothing to patch return ctrlerrors.Ignore(err, errors.IsNotFound) } @@ -136,9 +154,14 @@ func upgradeManagedFields(ctx context.Context, r client.Client, obj client.Objec } if patch != nil { - return r.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, patch)) + return c.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, patch)) } // No work to be done - already upgraded return nil } + +// upgradeStatusManagedFields upgrades the status subresource managed fields owned by fieldOwner from CSA to SSA. +func upgradeStatusManagedFields(ctx context.Context, c client.Client, obj client.Object) error { + return upgradeManagedFields(ctx, c, obj, csaupgrade.Subresource("status")) +} diff --git a/internal/controller/stas/workload_controller.go b/internal/controller/stas/workload_controller.go index 98d04f51..5122504f 100644 --- a/internal/controller/stas/workload_controller.go +++ b/internal/controller/stas/workload_controller.go @@ -208,7 +208,7 @@ func (r *PodReconciler) reconcile(ctx context.Context, pod *corev1.Pod) error { cisObj.Namespace = *cis.Namespace cisObj.Name = *cis.Name - if err := upgradeManagedFields(ctx, r.Client, cisObj, fieldOwner); err != nil { + if err := upgradeManagedFields(ctx, r.Client, cisObj); err != nil { return err }