@@ -21,29 +21,56 @@ import (
21
21
"encoding/json"
22
22
"errors"
23
23
"net/http"
24
+ "slices"
24
25
26
+ "gomodules.xyz/jsonpatch/v2"
25
27
admissionv1 "k8s.io/api/admission/v1"
26
28
apierrors "k8s.io/apimachinery/pkg/api/errors"
27
29
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
30
"k8s.io/apimachinery/pkg/runtime"
31
+ "k8s.io/apimachinery/pkg/util/sets"
29
32
)
30
33
31
34
// CustomDefaulter defines functions for setting defaults on resources.
32
35
type CustomDefaulter interface {
33
36
Default (ctx context.Context , obj runtime.Object ) error
34
37
}
35
38
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
+
36
53
// 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
+ }
38
59
return & 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
+ },
40
66
}
41
67
}
42
68
43
69
type 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
47
74
}
48
75
49
76
// Handle handles admission requests.
@@ -76,6 +103,12 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
76
103
return Errored (http .StatusBadRequest , err )
77
104
}
78
105
106
+ // Keep a copy of the object if needed
107
+ var originalObj runtime.Object
108
+ if ! h .removeUnknownOrOmitableFields {
109
+ originalObj = obj .DeepCopyObject ()
110
+ }
111
+
79
112
// Default the object
80
113
if err := h .defaulter .Default (ctx , obj ); err != nil {
81
114
var apiStatus apierrors.APIStatus
@@ -90,5 +123,43 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
90
123
if err != nil {
91
124
return Errored (http .StatusInternalServerError , err )
92
125
}
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
94
165
}
0 commit comments