Skip to content

Commit c30f09c

Browse files
committed
Use a zero-change patch to do find scheme caused fields removal.
1 parent 13c4991 commit c30f09c

File tree

2 files changed

+59
-20
lines changed

2 files changed

+59
-20
lines changed

pkg/webhook/admission/defaulter_custom.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"net/http"
24+
"slices"
2425

26+
"gomodules.xyz/jsonpatch/v2"
2527
admissionv1 "k8s.io/api/admission/v1"
2628
apierrors "k8s.io/apimachinery/pkg/api/errors"
2729
metav1 "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.
@@ -71,32 +74,59 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response {
7174
ctx = NewContextWithRequest(ctx, req)
7275

7376
// Get the object in the request
74-
original := h.object.DeepCopyObject()
75-
if err := h.decoder.Decode(req, original); err != nil {
77+
obj := h.object.DeepCopyObject()
78+
if err := h.decoder.Decode(req, obj); err != nil {
7679
return Errored(http.StatusBadRequest, err)
7780
}
7881

82+
// Keep a copy of the object
83+
originalObj := obj.DeepCopyObject()
84+
7985
// Default the object
80-
updated := original.DeepCopyObject()
81-
if err := h.defaulter.Default(ctx, updated); err != nil {
86+
if err := h.defaulter.Default(ctx, obj); err != nil {
8287
var apiStatus apierrors.APIStatus
8388
if errors.As(err, &apiStatus) {
8489
return validationResponseFromStatus(false, apiStatus.Status())
8590
}
8691
return Denied(err.Error())
8792
}
8893

89-
// Create the patch.
90-
// We need to decode and marshall the original because the type registered in the
91-
// decoder might not match the latest version of the API.
92-
// Creating a diff from the raw object might cause new fields to be dropped.
93-
marshalledOriginal, err := json.Marshal(original)
94+
// Create the patch
95+
marshalled, err := json.Marshal(obj)
9496
if err != nil {
9597
return Errored(http.StatusInternalServerError, err)
9698
}
97-
marshalledUpdated, err := json.Marshal(updated)
99+
handlerResponse := PatchResponseFromRaw(req.Object.Raw, marshalled)
100+
101+
return h.dropSchemeRemovals(handlerResponse, originalObj, req.Object.Raw)
102+
}
103+
104+
func (h *defaulterForType) dropSchemeRemovals(r Response, original runtime.Object, raw []byte) Response {
105+
const opRemove = "remove"
106+
if !r.Allowed || r.PatchType == nil {
107+
return r
108+
}
109+
110+
// If we don't have removals in the patch.
111+
if !slices.ContainsFunc(r.Patches, func(o jsonpatch.JsonPatchOperation) bool { return o.Operation == opRemove }) {
112+
return r
113+
}
114+
115+
// Get the raw to original patch
116+
marshalledOriginal, err := json.Marshal(original)
98117
if err != nil {
99118
return Errored(http.StatusInternalServerError, err)
100119
}
101-
return PatchResponseFromRaw(marshalledOriginal, marshalledUpdated)
120+
121+
patchOriginal := PatchResponseFromRaw(raw, marshalledOriginal).Patches
122+
removedByScheme := sets.New(slices.DeleteFunc(patchOriginal, func(p jsonpatch.JsonPatchOperation) bool { return p.Operation != opRemove })...)
123+
124+
r.Patches = slices.DeleteFunc(r.Patches, func(p jsonpatch.JsonPatchOperation) bool {
125+
return removedByScheme.Has(p)
126+
})
127+
128+
if len(r.Patches) == 0 {
129+
r.PatchType = nil
130+
}
131+
return r
102132
}

pkg/webhook/admission/defaulter_custom_test.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,30 @@ import (
2929

3030
var _ = Describe("Defaulter Handler", func() {
3131

32-
It("should should not lose unknown fields", func() {
32+
It("should not lose unknown fields", func() {
3333
obj := &TestDefaulter{}
3434
handler := WithCustomDefaulter(admissionScheme, obj, &TestCustomDefaulter{})
3535

3636
resp := handler.Handle(context.TODO(), Request{
3737
AdmissionRequest: admissionv1.AdmissionRequest{
3838
Operation: admissionv1.Create,
3939
Object: runtime.RawExtension{
40-
Raw: []byte(`{"newField":"foo"}`),
40+
Raw: []byte(`{"newField":"foo", "totalReplicas":5}`),
4141
},
4242
},
4343
})
4444
Expect(resp.Allowed).Should(BeTrue())
45-
Expect(resp.Patches).To(Equal([]jsonpatch.JsonPatchOperation{{
46-
Operation: "add",
47-
Path: "/replica",
48-
Value: 2.0,
49-
}}))
45+
Expect(resp.Patches).To(Equal([]jsonpatch.JsonPatchOperation{
46+
{
47+
Operation: "add",
48+
Path: "/replica",
49+
Value: 2.0,
50+
},
51+
{
52+
Operation: "remove",
53+
Path: "/totalReplicas",
54+
},
55+
}))
5056
Expect(resp.Result.Code).Should(Equal(int32(http.StatusOK)))
5157
})
5258

@@ -70,15 +76,17 @@ var _ = Describe("Defaulter Handler", func() {
7076
var _ runtime.Object = &TestDefaulter{}
7177

7278
type TestDefaulter struct {
73-
Replica int `json:"replica,omitempty"`
79+
Replica int `json:"replica,omitempty"`
80+
TotalReplicas int `json:"totalReplicas,omitempty"`
7481
}
7582

7683
var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: "TestDefaulter"}
7784

7885
func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d }
7986
func (d *TestDefaulter) DeepCopyObject() runtime.Object {
8087
return &TestDefaulter{
81-
Replica: d.Replica,
88+
Replica: d.Replica,
89+
TotalReplicas: d.TotalReplicas,
8290
}
8391
}
8492

@@ -103,5 +111,6 @@ func (d *TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) e
103111
if o.Replica < 2 {
104112
o.Replica = 2
105113
}
114+
o.TotalReplicas = 0
106115
return nil
107116
}

0 commit comments

Comments
 (0)