Skip to content

Commit

Permalink
chore: use server-side-apply for CIS status (#1027)
Browse files Browse the repository at this point in the history
  • Loading branch information
erikgb authored Jul 17, 2024
1 parent f67ea03 commit 25367f8
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 62 deletions.
25 changes: 13 additions & 12 deletions internal/controller/stas/containerimagescan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
45 changes: 45 additions & 0 deletions internal/controller/stas/containerimagescan_status.go
Original file line number Diff line number Diff line change
@@ -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)
}
96 changes: 50 additions & 46 deletions internal/controller/stas/scan_job_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
}
29 changes: 26 additions & 3 deletions internal/controller/stas/ssa_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand All @@ -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"))
}
2 changes: 1 addition & 1 deletion internal/controller/stas/workload_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 25367f8

Please sign in to comment.