diff --git a/internal/controller/stas/ssa_client.go b/internal/controller/stas/ssa_client.go new file mode 100644 index 00000000..d3b3943a --- /dev/null +++ b/internal/controller/stas/ssa_client.go @@ -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 +} diff --git a/internal/controller/stas/workload_controller.go b/internal/controller/stas/workload_controller.go index 81924f61..98d04f51 100644 --- a/internal/controller/stas/workload_controller.go +++ b/internal/controller/stas/workload_controller.go @@ -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" @@ -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 }