Skip to content

Commit

Permalink
chore: use server-side-apply for CIS spec (#982)
Browse files Browse the repository at this point in the history
Co-authored-by: Erik Godding Boye <[email protected]>
  • Loading branch information
tenstad and erikgb authored Jun 6, 2024
1 parent 2678075 commit 9566f7c
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 35 deletions.
144 changes: 144 additions & 0 deletions internal/controller/stas/ssa_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package stas

import (
"context"
"encoding/json"
"fmt"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
"k8s.io/client-go/util/csaupgrade"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"

ctrlerrors "github.com/statnett/image-scanner-operator/internal/errors"
)

const (
fieldOwner = client.FieldOwner("image-scanner-operator")

// crRegressionFieldManager is the field manager that was introduced by a regression in controller-runtime
// version 0.15.0; fixed in 15.1 and 0.16.0: https://github.com/kubernetes-sigs/controller-runtime/pull/2435
crRegressionFieldManager = "Go-http-client"

// beforeFirstApplyFieldManager seems to be a manager set when managedFields got introduced?
// Or, ref. apelisse: I can't remember, but I think at some point we didn't track managedFields until the object had been applied at least once.
// And we put all the changes that happened before that first apply under that manager.
beforeFirstApplyFieldManager = "before-first-apply"
)

type applyPatch struct {
// must use any type until apply configurations implements a common interface
patch any
}

func (p applyPatch) Type() types.PatchType {
return types.ApplyPatchType
}

func (p applyPatch) Data(_ client.Object) ([]byte, error) {
return json.Marshal(p.patch)
}

// FieldValidationStrict instructs the server on how to handle
// objects in the request (POST/PUT/PATCH) containing unknown
// or duplicate fields. This will fail the request with a BadRequest
// error if any unknown fields would be dropped from the object, or if any
// duplicate fields are present. The error returned from the server
// will contain all unknown and duplicate fields encountered.
var FieldValidationStrict = fieldValidationStrict{}

var (
_ client.PatchOption = fieldValidationStrict{}
_ client.SubResourcePatchOption = fieldValidationStrict{}
)

type fieldValidationStrict struct{}

func (fieldValidationStrict) ApplyToPatch(opts *client.PatchOptions) {
if opts.Raw == nil {
opts.Raw = &metav1.PatchOptions{}
}

opts.Raw.FieldValidation = "Strict"
}

func (fieldValidationStrict) ApplyToSubResourcePatch(opts *client.SubResourcePatchOptions) {
if opts.Raw == nil {
opts.Raw = &metav1.PatchOptions{}
}

opts.Raw.FieldValidation = "Strict"
}

// 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.
func SetOwnerReference(owner metav1.Object, owned *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 SetOwnerReference", owner)
}

if err := validateOwner(owner, owned); err != nil {
return err
}

gvk, err := apiutil.GVKForObject(ro, scheme)
if err != nil {
return err
}

owned.WithOwnerReferences(
metav1ac.OwnerReference().
WithAPIVersion(gvk.GroupVersion().String()).
WithKind(gvk.Kind).
WithName(owner.GetName()).
WithUID(owner.GetUID()),
)

return nil
}

func validateOwner(owner metav1.Object, object *metav1ac.ObjectMetaApplyConfiguration) error {
ownerNs := owner.GetNamespace()
if ownerNs != "" {
objNs := ptr.Deref(object.Namespace, "")
if objNs == "" {
return fmt.Errorf("cluster-scoped resource must not have a namespace-scoped owner, owner's namespace %s", ownerNs)
}

if ownerNs != objNs {
return fmt.Errorf("cross-namespace owner references are disallowed, owner's namespace %s, obj's namespace %s", owner.GetNamespace(), objNs)
}
}

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 {
// If not found, there is nothing to patch
return ctrlerrors.Ignore(err, errors.IsNotFound)
}

csaManagers := sets.New(string(fieldOwner), crRegressionFieldManager, beforeFirstApplyFieldManager)

patch, err := csaupgrade.UpgradeManagedFieldsPatch(obj, csaManagers, string(fieldOwner), opts...)
if err != nil {
return err
}

if patch != nil {
return r.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, patch))
}

// No work to be done - already upgraded
return nil
}
70 changes: 35 additions & 35 deletions internal/controller/stas/workload_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,16 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

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 @@ -171,52 +170,53 @@ func (r *PodReconciler) reconcile(ctx context.Context, pod *corev1.Pod) error {
}

for containerName, image := range images {
cis := &stasv1alpha1.ContainerImageScan{}
cis.Namespace = pod.Namespace

cis.Name, err = imageScanName(podController, containerName, image.Image)
name, err := imageScanName(podController, containerName, image.Image)
if err != nil {
return err
}

mutateFn := func() error {
cis.Labels = pod.GetLabels()
cis.Spec.Workload.Group = podController.GetObjectKind().GroupVersionKind().Group
cis.Spec.Workload.Kind = podController.GetObjectKind().GroupVersionKind().Kind
cis.Spec.Workload.Name = podController.GetName()
cis.Spec.Workload.ContainerName = containerName
cis.Spec.Image = image.Image
cis.Spec.Tag = image.Tag
// Ensure MinSeverity unset until we eventually make use of it
cis.Spec.MinSeverity = nil

if v := podController.GetAnnotations()[stasv1alpha1.WorkloadAnnotationKeyIgnoreUnfixed]; v == "true" {
cis.Spec.IgnoreUnfixed = ptr.To(true)
} else {
cis.Spec.IgnoreUnfixed = ptr.To(false)
}
cis := stasv1alpha1ac.ContainerImageScan(name, pod.Namespace).
WithLabels(pod.GetLabels()).
WithSpec(
stasv1alpha1ac.ContainerImageScanSpec().
WithWorkload(
stasv1alpha1ac.Workload().
WithGroup(podController.GetObjectKind().GroupVersionKind().Group).
WithKind(podController.GetObjectKind().GroupVersionKind().Kind).
WithName(podController.GetName()).
WithContainerName(containerName),
).
WithName(image.Image.Name).
WithDigest(image.Image.Digest).
WithTag(image.Tag).
WithIgnoreUnfixed(podController.GetAnnotations()[stasv1alpha1.WorkloadAnnotationKeyIgnoreUnfixed] == "true"),
)

owners := getCISOwners(containerName, image)
if len(owners) == 0 {
// Safeguard to validate assumption in `cisOwnerLookup`.
return errors.New("Found no owners for CIS")
}

owners := getCISOwners(containerName, image)
if len(owners) == 0 {
// Safeguard to validate assumption in `cisOwnerLookup`.
return errors.New("Found no owners for CIS")
for _, owner := range owners {
if err := SetOwnerReference(&owner, cis.ObjectMetaApplyConfiguration, r.Scheme); err != nil {
return err
}
}

for _, owner := range owners {
if err := controllerutil.SetOwnerReference(&owner, cis, r.Scheme); err != nil {
return err
}
}
cisObj := &stasv1alpha1.ContainerImageScan{}
cisObj.Namespace = *cis.Namespace
cisObj.Name = *cis.Name

return nil
if err := upgradeManagedFields(ctx, r.Client, cisObj, fieldOwner); err != nil {
return err
}

_, err = controllerutil.CreateOrUpdate(ctx, r.Client, cis, mutateFn)
if err != nil {
if err := r.Patch(ctx, cisObj, applyPatch{cis}, FieldValidationStrict, client.ForceOwnership, fieldOwner); err != nil {
return err
}

err = r.garbageCollectObsoleteImageScans(ctx, pod, cis)
err = r.garbageCollectObsoleteImageScans(ctx, pod, cisObj)
if err != nil {
return err
}
Expand Down

0 comments on commit 9566f7c

Please sign in to comment.