Skip to content

Commit

Permalink
handle request error in webhook (stolostron#414)
Browse files Browse the repository at this point in the history
Signed-off-by: ldpliu <[email protected]>
  • Loading branch information
ldpliu authored Nov 18, 2021
1 parent abf7edd commit 74667b7
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 224 deletions.
70 changes: 2 additions & 68 deletions pkg/webhook/clusterset/validatingWebhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"

"github.com/open-cluster-management/multicloud-operators-foundation/cmd/webhook/app/options"
serve "github.com/open-cluster-management/multicloud-operators-foundation/pkg/webhook/serve"
hivev1 "github.com/openshift/hive/apis/hive/v1"
hiveclient "github.com/openshift/hive/pkg/client/clientset/versioned"
v1 "k8s.io/api/admission/v1"

authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
clusterv1 "open-cluster-management.io/api/cluster/v1"
)

Expand All @@ -28,68 +24,6 @@ type AdmissionHandler struct {
KubeClient kubernetes.Interface
}

// toAdmissionResponse is a helper function to create an AdmissionResponse
// with an embedded error
func toAdmissionResponse(err error) *v1.AdmissionResponse {
return &v1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
},
}
}

// admitFunc is the type we use for all of our validators and mutators
type admitFunc func(request *v1.AdmissionRequest) *v1.AdmissionResponse

// serve handles the http portion of a request prior to handing to an admit
// function
func (a *AdmissionHandler) serve(w io.Writer, r *http.Request, admit admitFunc) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
klog.Errorf("contentType=%s, expect application/json", contentType)
return
}

klog.V(2).Info(fmt.Sprintf("handling request: %s", body))

// The AdmissionReview that was sent to the webhook
requestedAdmissionReview := v1.AdmissionReview{}

// The AdmissionReview that will be returned
responseAdmissionReview := v1.AdmissionReview{}

deserializer := options.Codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
klog.Error(err)
responseAdmissionReview.Response = toAdmissionResponse(err)
} else {
// pass to admitFunc
responseAdmissionReview.Response = admit(requestedAdmissionReview.Request)
}

responseAdmissionReview.Kind = requestedAdmissionReview.Kind
responseAdmissionReview.APIVersion = requestedAdmissionReview.APIVersion
// Return the same UID
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID

klog.V(2).Info(fmt.Sprintf("sending response: %+v", responseAdmissionReview))

respBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
klog.Error(err)
}
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
}

var managedClustersGVR = metav1.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Expand Down Expand Up @@ -337,5 +271,5 @@ func (a *AdmissionHandler) allowUpdateClusterSet(userInfo authenticationv1.UserI
}

func (a *AdmissionHandler) ServerValidateResource(w http.ResponseWriter, r *http.Request) {
a.serve(w, r, a.validateResource)
serve.Serve(w, r, a.validateResource)
}
107 changes: 107 additions & 0 deletions pkg/webhook/serve/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package serve

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"github.com/open-cluster-management/multicloud-operators-foundation/cmd/webhook/app/options"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
)

// toAdmissionResponse is a helper function to create an AdmissionResponse
// with an embedded error
func ToAdmissionResponse(err error) *v1.AdmissionResponse {
return &v1.AdmissionResponse{
Result: &metav1.Status{
Message: err.Error(),
Reason: metav1.StatusReasonBadRequest,
},
}
}

// admitFunc is the type we use for all of our validators and mutators
type admitFunc func(request *v1.AdmissionRequest) *v1.AdmissionResponse

// serve handles the http portion of a request prior to handing to an admit
// function
func Serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
var body []byte
var errmsg string
// The AdmissionReview that was sent to the webhook
requestedAdmissionReview := v1.AdmissionReview{}

// The AdmissionReview that will be returned
responseAdmissionReview := v1.AdmissionReview{}

if r.Body == nil {
errmsg = "Request Body is null"
writerErrorResponse(errmsg, w)
return
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
errmsg = fmt.Sprintf("Can not read request body, err: %v", err)
writerErrorResponse(errmsg, w)
return
}

body = data

// verify the content type is accurate
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
errmsg = fmt.Sprintf("contentType=%s, expect application/json", contentType)
writerErrorResponse(errmsg, w)
return
}

klog.V(2).Info(fmt.Sprintf("handling request: %s", body))

deserializer := options.Codecs.UniversalDeserializer()
_, _, err = deserializer.Decode(body, nil, &requestedAdmissionReview)
if err != nil {
errmsg = fmt.Sprintf("Decode body error: %v", err)
writerErrorResponse(errmsg, w)
return
} else {
// pass to admitFunc
responseAdmissionReview.Response = admit(requestedAdmissionReview.Request)
}

responseAdmissionReview.Kind = requestedAdmissionReview.Kind
responseAdmissionReview.APIVersion = requestedAdmissionReview.APIVersion
// Return the same UID
if requestedAdmissionReview.Request == nil {
errmsg = fmt.Sprintf("requestedAdmissionReview is nil")
writerErrorResponse(errmsg, w)
return
}
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID

klog.V(2).Info(fmt.Sprintf("sending response: %+v", responseAdmissionReview))

respBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
errmsg = fmt.Sprintf("Decode responseAdmissionReview error: %v", err)
writerErrorResponse(errmsg, w)
return
}
_, err = w.Write(respBytes)
if err != nil {
errmsg = fmt.Sprintf("Write responsebyte error: %v", err)
writerErrorResponse(errmsg, w)
}
return
}

func writerErrorResponse(errmsg string, httpWriter http.ResponseWriter) {
var buffer bytes.Buffer
buffer.WriteString(errmsg)
httpWriter.WriteHeader(http.StatusBadRequest)
httpWriter.Write(buffer.Bytes())
}
86 changes: 86 additions & 0 deletions pkg/webhook/serve/serve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package serve

import (
"context"
"net/http"
"strconv"
"strings"
"testing"

v1 "k8s.io/api/admission/v1"
)

var statusCode = "statusCode"

func fakeadmit(request *v1.AdmissionRequest) *v1.AdmissionResponse {
status := &v1.AdmissionResponse{
Allowed: true,
}
return status
}
func TestServe(t *testing.T) {
cases := []struct {
name string
request *http.Request
responseWriter *fakeWriter
}{
{
name: "nil body in request",
request: func() *http.Request {
r, _ := http.NewRequest(http.MethodOptions, "url", nil)
return r
}(),

responseWriter: &fakeWriter{},
},
{
name: "error header format in request",
request: func() *http.Request {
r, _ := http.NewRequest(http.MethodOptions, "url", strings.NewReader("{\"foo\":\"bar\"}"))
return r
}(),

responseWriter: &fakeWriter{},
},
{
name: "requestedAdmissionReview is not right",
request: func() *http.Request {
ctx := context.TODO()
r, _ := http.NewRequestWithContext(ctx, http.MethodHead, "url", strings.NewReader("{\"foo\":\"bar\"}"))
r.Header.Add("Content-Type", "application/json")
return r
}(),
responseWriter: &fakeWriter{},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Serve(c.responseWriter, c.request, fakeadmit)
if len(c.responseWriter.Header()[statusCode]) == 0 {
t.Errorf("response error:%v", c.responseWriter.Header())
}
})
}
}

type fakeWriter struct {
header http.Header
}

func (fw *fakeWriter) Header() http.Header {
if fw.header == nil {
fw.header = http.Header{}
}
return fw.header
}

func (fw *fakeWriter) WriteHeader(status int) {
tempHead := make(map[string][]string)
tempHead[statusCode] = []string{strconv.Itoa(status)}
fw.header = tempHead
}

func (fw *fakeWriter) Write(data []byte) (int, error) {
return len(data), nil
}
Loading

0 comments on commit 74667b7

Please sign in to comment.