Skip to content

Commit be59d64

Browse files
authored
Merge pull request #1129 from Shpectator/admission-webhooks-status-response
⚠️ admission responses with raw Status
2 parents ba987e4 + 9f235ae commit be59d64

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed

pkg/webhook/admission/response.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,14 @@ func PatchResponseFromRaw(original, current []byte) Response {
9696
},
9797
}
9898
}
99+
100+
// validationResponseFromStatus returns a response for admitting a request with provided Status object.
101+
func validationResponseFromStatus(allowed bool, status metav1.Status) Response {
102+
resp := Response{
103+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
104+
Allowed: allowed,
105+
Result: &status,
106+
},
107+
}
108+
return resp
109+
}

pkg/webhook/admission/validator.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import (
2020
"context"
2121
"net/http"
2222

23+
goerrors "errors"
24+
2325
"k8s.io/api/admission/v1beta1"
26+
"k8s.io/apimachinery/pkg/api/errors"
2427
"k8s.io/apimachinery/pkg/runtime"
2528
)
2629

@@ -68,6 +71,10 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
6871

6972
err = obj.ValidateCreate()
7073
if err != nil {
74+
var apiStatus errors.APIStatus
75+
if goerrors.As(err, &apiStatus) {
76+
return validationResponseFromStatus(false, apiStatus.Status())
77+
}
7178
return Denied(err.Error())
7279
}
7380
}
@@ -86,6 +93,10 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
8693

8794
err = obj.ValidateUpdate(oldObj)
8895
if err != nil {
96+
var apiStatus errors.APIStatus
97+
if goerrors.As(err, &apiStatus) {
98+
return validationResponseFromStatus(false, apiStatus.Status())
99+
}
89100
return Denied(err.Error())
90101
}
91102
}
@@ -100,6 +111,10 @@ func (h *validatingHandler) Handle(ctx context.Context, req Request) Response {
100111

101112
err = obj.ValidateDelete()
102113
if err != nil {
114+
var apiStatus errors.APIStatus
115+
if goerrors.As(err, &apiStatus) {
116+
return validationResponseFromStatus(false, apiStatus.Status())
117+
}
103118
return Denied(err.Error())
104119
}
105120
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package admission
2+
3+
import (
4+
"context"
5+
goerrors "errors"
6+
"net/http"
7+
8+
. "github.com/onsi/ginkgo"
9+
. "github.com/onsi/gomega"
10+
11+
"k8s.io/api/admission/v1beta1"
12+
apierrs "k8s.io/apimachinery/pkg/api/errors"
13+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/client-go/kubernetes/scheme"
17+
)
18+
19+
var _ = Describe("validatingHandler", func() {
20+
21+
decoder, _ := NewDecoder(scheme.Scheme)
22+
23+
Context("when dealing with successful results", func() {
24+
25+
f := &fakeValidator{ErrorToReturn: nil}
26+
handler := validatingHandler{validator: f, decoder: decoder}
27+
28+
It("should return 200 in response when create succeeds", func() {
29+
30+
response := handler.Handle(context.TODO(), Request{
31+
AdmissionRequest: v1beta1.AdmissionRequest{
32+
Operation: v1beta1.Create,
33+
Object: runtime.RawExtension{
34+
Raw: []byte("{}"),
35+
Object: handler.validator,
36+
},
37+
},
38+
})
39+
40+
Expect(response.Allowed).Should(BeTrue())
41+
Expect(response.Result.Code).Should(Equal(int32(http.StatusOK)))
42+
})
43+
44+
It("should return 200 in response when update succeeds", func() {
45+
46+
response := handler.Handle(context.TODO(), Request{
47+
AdmissionRequest: v1beta1.AdmissionRequest{
48+
Operation: v1beta1.Update,
49+
Object: runtime.RawExtension{
50+
Raw: []byte("{}"),
51+
Object: handler.validator,
52+
},
53+
OldObject: runtime.RawExtension{
54+
Raw: []byte("{}"),
55+
Object: handler.validator,
56+
},
57+
},
58+
})
59+
Expect(response.Allowed).Should(BeTrue())
60+
Expect(response.Result.Code).Should(Equal(int32(http.StatusOK)))
61+
})
62+
63+
It("should return 200 in response when delete succeeds", func() {
64+
65+
response := handler.Handle(context.TODO(), Request{
66+
AdmissionRequest: v1beta1.AdmissionRequest{
67+
Operation: v1beta1.Delete,
68+
OldObject: runtime.RawExtension{
69+
Raw: []byte("{}"),
70+
Object: handler.validator,
71+
},
72+
},
73+
})
74+
Expect(response.Allowed).Should(BeTrue())
75+
Expect(response.Result.Code).Should(Equal(int32(http.StatusOK)))
76+
})
77+
78+
})
79+
80+
Context("when dealing with Status errors", func() {
81+
82+
expectedError := &apierrs.StatusError{
83+
ErrStatus: v1.Status{
84+
Message: "some message",
85+
Code: http.StatusUnprocessableEntity,
86+
},
87+
}
88+
f := &fakeValidator{ErrorToReturn: expectedError}
89+
handler := validatingHandler{validator: f, decoder: decoder}
90+
91+
It("should propagate the Status from ValidateCreate's return value to the HTTP response", func() {
92+
93+
response := handler.Handle(context.TODO(), Request{
94+
AdmissionRequest: v1beta1.AdmissionRequest{
95+
Operation: v1beta1.Create,
96+
Object: runtime.RawExtension{
97+
Raw: []byte("{}"),
98+
Object: handler.validator,
99+
},
100+
},
101+
})
102+
103+
Expect(response.Allowed).Should(BeFalse())
104+
Expect(response.Result.Code).Should(Equal(expectedError.Status().Code))
105+
Expect(*response.Result).Should(Equal(expectedError.Status()))
106+
107+
})
108+
109+
It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func() {
110+
111+
response := handler.Handle(context.TODO(), Request{
112+
AdmissionRequest: v1beta1.AdmissionRequest{
113+
Operation: v1beta1.Update,
114+
Object: runtime.RawExtension{
115+
Raw: []byte("{}"),
116+
Object: handler.validator,
117+
},
118+
OldObject: runtime.RawExtension{
119+
Raw: []byte("{}"),
120+
Object: handler.validator,
121+
},
122+
},
123+
})
124+
125+
Expect(response.Allowed).Should(BeFalse())
126+
Expect(response.Result.Code).Should(Equal(expectedError.Status().Code))
127+
Expect(*response.Result).Should(Equal(expectedError.Status()))
128+
129+
})
130+
131+
It("should propagate the Status from ValidateDelete's return value to the HTTP response", func() {
132+
133+
response := handler.Handle(context.TODO(), Request{
134+
AdmissionRequest: v1beta1.AdmissionRequest{
135+
Operation: v1beta1.Delete,
136+
OldObject: runtime.RawExtension{
137+
Raw: []byte("{}"),
138+
Object: handler.validator,
139+
},
140+
},
141+
})
142+
143+
Expect(response.Allowed).Should(BeFalse())
144+
Expect(response.Result.Code).Should(Equal(expectedError.Status().Code))
145+
Expect(*response.Result).Should(Equal(expectedError.Status()))
146+
147+
})
148+
149+
})
150+
Context("when dealing with non-status errors", func() {
151+
152+
expectedError := goerrors.New("some error")
153+
f := &fakeValidator{ErrorToReturn: expectedError}
154+
handler := validatingHandler{validator: f, decoder: decoder}
155+
156+
It("should return 403 response when ValidateCreate with error message embedded", func() {
157+
158+
response := handler.Handle(context.TODO(), Request{
159+
AdmissionRequest: v1beta1.AdmissionRequest{
160+
Operation: v1beta1.Create,
161+
Object: runtime.RawExtension{
162+
Raw: []byte("{}"),
163+
Object: handler.validator,
164+
},
165+
},
166+
})
167+
Expect(response.Allowed).Should(BeFalse())
168+
Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden)))
169+
Expect(string(response.Result.Reason)).Should(Equal(expectedError.Error()))
170+
171+
})
172+
173+
It("should return 403 response when ValidateUpdate returns non-APIStatus error", func() {
174+
175+
response := handler.Handle(context.TODO(), Request{
176+
AdmissionRequest: v1beta1.AdmissionRequest{
177+
Operation: v1beta1.Update,
178+
Object: runtime.RawExtension{
179+
Raw: []byte("{}"),
180+
Object: handler.validator,
181+
},
182+
OldObject: runtime.RawExtension{
183+
Raw: []byte("{}"),
184+
Object: handler.validator,
185+
},
186+
},
187+
})
188+
Expect(response.Allowed).Should(BeFalse())
189+
Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden)))
190+
Expect(string(response.Result.Reason)).Should(Equal(expectedError.Error()))
191+
192+
})
193+
194+
It("should return 403 response when ValidateDelete returns non-APIStatus error", func() {
195+
196+
response := handler.Handle(context.TODO(), Request{
197+
AdmissionRequest: v1beta1.AdmissionRequest{
198+
Operation: v1beta1.Delete,
199+
OldObject: runtime.RawExtension{
200+
Raw: []byte("{}"),
201+
Object: handler.validator,
202+
},
203+
},
204+
})
205+
Expect(response.Allowed).Should(BeFalse())
206+
Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden)))
207+
Expect(string(response.Result.Reason)).Should(Equal(expectedError.Error()))
208+
209+
})
210+
211+
})
212+
213+
PIt("should return 400 in response when create fails on decode", func() {})
214+
215+
PIt("should return 400 in response when update fails on decoding new object", func() {})
216+
217+
PIt("should return 400 in response when update fails on decoding old object", func() {})
218+
219+
PIt("should return 400 in response when delete fails on decode", func() {})
220+
221+
})
222+
223+
type fakeValidator struct {
224+
ErrorToReturn error `json:"ErrorToReturn,omitempty"`
225+
}
226+
227+
var _ Validator = &fakeValidator{}
228+
229+
var fakeValidatorVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: "fakeValidator"}
230+
231+
func (v *fakeValidator) ValidateCreate() error {
232+
return v.ErrorToReturn
233+
}
234+
235+
func (v *fakeValidator) ValidateUpdate(old runtime.Object) error {
236+
return v.ErrorToReturn
237+
}
238+
239+
func (v *fakeValidator) ValidateDelete() error {
240+
return v.ErrorToReturn
241+
}
242+
243+
func (v *fakeValidator) GetObjectKind() schema.ObjectKind { return v }
244+
245+
func (v *fakeValidator) DeepCopyObject() runtime.Object {
246+
return &fakeValidator{ErrorToReturn: v.ErrorToReturn}
247+
}
248+
249+
func (v *fakeValidator) GroupVersionKind() schema.GroupVersionKind {
250+
return fakeValidatorVK
251+
}
252+
253+
func (v *fakeValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {}

0 commit comments

Comments
 (0)