Skip to content

Commit

Permalink
Merge pull request #1293 from estroz/bugfix/webhook-admissionreview-v…
Browse files Browse the repository at this point in the history
…ersion

🐛 pkg/webhook/admission: set GVK on AdmissionReview responses
  • Loading branch information
k8s-ci-robot committed Dec 10, 2020
2 parents 3410e75 + 27ef229 commit f7a3dc6
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 23 deletions.
37 changes: 30 additions & 7 deletions pkg/webhook/admission/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
v1 "k8s.io/api/admission/v1"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
Expand Down Expand Up @@ -73,14 +74,17 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// Both v1 and v1beta1 AdmissionReview types are exactly the same, so the v1beta1 type can
// be decoded into the v1 type. However the runtime codec's decoder guesses which type to
// decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an`
// decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an
// unregistered type to the v1 GVK, the decoder will coerce a v1beta1 AdmissionReview to v1.
// The actual AdmissionReview GVK will be used to write a typed response in case the
// webhook config permits multiple versions, otherwise this response will fail.
req := Request{}
ar := unversionedAdmissionReview{}
// avoid an extra copy
ar.Request = &req.AdmissionRequest
ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
if _, _, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar); err != nil {
_, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar)
if err != nil {
wh.log.Error(err, "unable to decode the request")
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
Expand All @@ -90,20 +94,39 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// TODO: add panic-recovery for Handle
reviewResponse = wh.Handle(r.Context(), req)
wh.writeResponse(w, reviewResponse)
wh.writeResponseTyped(w, reviewResponse, actualAdmRevGVK)
}

// writeResponse writes response to w generically, i.e. without encoding GVK information.
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
encoder := json.NewEncoder(w)
responseAdmissionReview := v1.AdmissionReview{
wh.writeAdmissionResponse(w, v1.AdmissionReview{Response: &response.AdmissionResponse})
}

// writeResponseTyped writes response to w with GVK set to admRevGVK, which is necessary
// if multiple AdmissionReview versions are permitted by the webhook.
func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, admRevGVK *schema.GroupVersionKind) {
ar := v1.AdmissionReview{
Response: &response.AdmissionResponse,
}
err := encoder.Encode(responseAdmissionReview)
// Default to a v1 AdmissionReview, otherwise the API server may not recognize the request
// if multiple AdmissionReview versions are permitted by the webhook config.
// TODO(estroz): this should be configurable since older API servers won't know about v1.
if admRevGVK == nil || *admRevGVK == (schema.GroupVersionKind{}) {
ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
} else {
ar.SetGroupVersionKind(*admRevGVK)
}
wh.writeAdmissionResponse(w, ar)
}

// writeAdmissionResponse writes ar to w.
func (wh *Webhook) writeAdmissionResponse(w io.Writer, ar v1.AdmissionReview) {
err := json.NewEncoder(w).Encode(ar)
if err != nil {
wh.log.Error(err, "unable to encode the response")
wh.writeResponse(w, Errored(http.StatusInternalServerError, err))
} else {
res := responseAdmissionReview.Response
res := ar.Response
if log := wh.log; log.V(1).Enabled() {
if res.Result != nil {
log = log.WithValues("code", res.Result.Code, "reason", res.Result.Reason)
Expand Down
70 changes: 54 additions & 16 deletions pkg/webhook/admission/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import (

var _ = Describe("Admission Webhooks", func() {

const (
gvkJSONv1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1"`
gvkJSONv1beta1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1beta1"`
)

Describe("HTTP Handler", func() {
var respRecorder *httptest.ResponseRecorder
webhook := &Webhook{
Expand All @@ -51,10 +56,10 @@ var _ = Describe("Admission Webhooks", func() {
It("should return bad-request when given an empty body", func() {
req := &http.Request{Body: nil}

expected := []byte(`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
`)
expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
`
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.Bytes()).To(Equal(expected))
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should return bad-request when given the wrong content-type", func() {
Expand All @@ -63,10 +68,11 @@ var _ = Describe("Admission Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBuffer(nil)},
}

expected := []byte(`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}}
`)
expected :=
`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}}
`
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.Bytes()).To(Equal(expected))
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should return bad-request when given an undecodable body", func() {
Expand All @@ -75,14 +81,14 @@ var _ = Describe("Admission Webhooks", func() {
Body: nopCloser{Reader: bytes.NewBufferString("{")},
}

expected := []byte(
expected :=
`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}}
`)
`
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.Bytes()).To(Equal(expected))
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should return the response given by the handler", func() {
It("should return the response given by the handler with version defaulted to v1", func() {
req := &http.Request{
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
Expand All @@ -92,10 +98,42 @@ var _ = Describe("Admission Webhooks", func() {
log: logf.RuntimeLog.WithName("webhook"),
}

expected := []byte(`{"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
`)
expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
`, gvkJSONv1)
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should return the v1 response given by the handler", func() {
req := &http.Request{
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))},
}
webhook := &Webhook{
Handler: &fakeHandler{},
log: logf.RuntimeLog.WithName("webhook"),
}

expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
`, gvkJSONv1)
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should return the v1beta1 response given by the handler", func() {
req := &http.Request{
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1beta1))},
}
webhook := &Webhook{
Handler: &fakeHandler{},
log: logf.RuntimeLog.WithName("webhook"),
}

expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
`, gvkJSONv1beta1)
webhook.ServeHTTP(respRecorder, req)
Expect(respRecorder.Body.Bytes()).To(Equal(expected))
Expect(respRecorder.Body.String()).To(Equal(expected))
})

It("should present the Context from the HTTP request, if any", func() {
Expand All @@ -116,13 +154,13 @@ var _ = Describe("Admission Webhooks", func() {
log: logf.RuntimeLog.WithName("webhook"),
}

expected := []byte(fmt.Sprintf(`{"response":{"uid":"","allowed":true,"status":{"metadata":{},"reason":%q,"code":200}}}
`, value))
expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"reason":%q,"code":200}}}
`, gvkJSONv1, value)

ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value))
cancel()
webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
Expect(respRecorder.Body.Bytes()).To(Equal(expected))
Expect(respRecorder.Body.String()).To(Equal(expected))
})
})
})
Expand Down

0 comments on commit f7a3dc6

Please sign in to comment.