@@ -21,29 +21,56 @@ import (
2121"encoding/json"
2222"errors"
2323"net/http"
24+ "slices"
2425
26+ "gomodules.xyz/jsonpatch/v2"
2527admissionv1 "k8s.io/api/admission/v1"
2628apierrors "k8s.io/apimachinery/pkg/api/errors"
2729metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830"k8s.io/apimachinery/pkg/runtime"
31+ "k8s.io/apimachinery/pkg/util/sets"
2932)
3033
3134// CustomDefaulter defines functions for setting defaults on resources.
3235type CustomDefaulter interface {
3336Default (ctx context.Context , obj runtime.Object ) error
3437}
3538
39+ type defaulterOptions struct {
40+ removeUnknownOrOmitableFields bool
41+ }
42+
43+ // DefaulterOption defines the type of a CustomDefaulter's option
44+ type DefaulterOption func (* defaulterOptions )
45+
46+ // DefaulterRemoveUnknownOrOmitableFields makes the defaulter prune fields that are in the json object retrieved by the
47+ // webhook but not in the local go type json representation. This happens for example when the CRD in the apiserver has
48+ // fields that our go type doesn't know about, because it's outdated, or the field has a zero value and is `omitempty`.
49+ func DefaulterRemoveUnknownOrOmitableFields (o * defaulterOptions ) {
50+ o .removeUnknownOrOmitableFields = true
51+ }
52+
3653// WithCustomDefaulter creates a new Webhook for a CustomDefaulter interface.
37- func WithCustomDefaulter (scheme * runtime.Scheme , obj runtime.Object , defaulter CustomDefaulter ) * Webhook {
54+ func WithCustomDefaulter (scheme * runtime.Scheme , obj runtime.Object , defaulter CustomDefaulter , opts ... DefaulterOption ) * Webhook {
55+ options := & defaulterOptions {}
56+ for _ , o := range opts {
57+ o (options )
58+ }
3859return & Webhook {
39- Handler : & defaulterForType {object : obj , defaulter : defaulter , decoder : NewDecoder (scheme )},
60+ Handler : & defaulterForType {
61+ object : obj ,
62+ defaulter : defaulter ,
63+ decoder : NewDecoder (scheme ),
64+ removeUnknownOrOmitableFields : options .removeUnknownOrOmitableFields ,
65+ },
4066}
4167}
4268
4369type defaulterForType struct {
44- defaulter CustomDefaulter
45- object runtime.Object
46- decoder Decoder
70+ defaulter CustomDefaulter
71+ object runtime.Object
72+ decoder Decoder
73+ removeUnknownOrOmitableFields bool
4774}
4875
4976// Handle handles admission requests.
@@ -76,6 +103,12 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
76103return Errored (http .StatusBadRequest , err )
77104}
78105
106+ // Keep a copy of the object if needed
107+ var originalObj runtime.Object
108+ if ! h .removeUnknownOrOmitableFields {
109+ originalObj = obj .DeepCopyObject ()
110+ }
111+
79112// Default the object
80113if err := h .defaulter .Default (ctx , obj ); err != nil {
81114var apiStatus apierrors.APIStatus
@@ -90,5 +123,43 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
90123if err != nil {
91124return Errored (http .StatusInternalServerError , err )
92125}
93- return PatchResponseFromRaw (req .Object .Raw , marshalled )
126+
127+ handlerResponse := PatchResponseFromRaw (req .Object .Raw , marshalled )
128+ if ! h .removeUnknownOrOmitableFields {
129+ handlerResponse = h .dropSchemeRemovals (handlerResponse , originalObj , req .Object .Raw )
130+ }
131+ return handlerResponse
132+ }
133+
134+ func (h * defaulterForType ) dropSchemeRemovals (r Response , original runtime.Object , raw []byte ) Response {
135+ const opRemove = "remove"
136+ if ! r .Allowed || r .PatchType == nil {
137+ return r
138+ }
139+
140+ // If we don't have removals in the patch.
141+ if ! slices .ContainsFunc (r .Patches , func (o jsonpatch.JsonPatchOperation ) bool { return o .Operation == opRemove }) {
142+ return r
143+ }
144+
145+ // Get the raw to original patch
146+ marshalledOriginal , err := json .Marshal (original )
147+ if err != nil {
148+ return Errored (http .StatusInternalServerError , err )
149+ }
150+
151+ patchOriginal , err := jsonpatch .CreatePatch (raw , marshalledOriginal )
152+ if err != nil {
153+ return Errored (http .StatusInternalServerError , err )
154+ }
155+ removedByScheme := sets .New (slices .DeleteFunc (patchOriginal , func (p jsonpatch.JsonPatchOperation ) bool { return p .Operation != opRemove })... )
156+
157+ r .Patches = slices .DeleteFunc (r .Patches , func (p jsonpatch.JsonPatchOperation ) bool {
158+ return removedByScheme .Has (p )
159+ })
160+
161+ if len (r .Patches ) == 0 {
162+ r .PatchType = nil
163+ }
164+ return r
94165}
0 commit comments