add hook handler interface

Signed-off-by: changzhen <changzhen5@huawei.com>
This commit is contained in:
changzhen 2021-11-16 15:30:03 +08:00
parent 30803c9b8c
commit 0acd626d1b
8 changed files with 300 additions and 29 deletions

View File

@ -260,5 +260,12 @@ func (e *CustomizedExplorer) callHook(ctx context.Context, hook configmanager.We
}
}
if !res.Successful {
return nil, &webhookutil.ErrCallingWebhook{
WebhookName: hook.GetUID(),
Reason: fmt.Errorf("webhook call failed, get status code: %d, msg: %s", res.Status.Code, res.Status.Message),
}
}
return res, nil
}

View File

@ -19,6 +19,8 @@ type RequestAttributes struct {
// ResponseAttributes contains the attributes that response by the webhook.
type ResponseAttributes struct {
Successful bool
Status configv1alpha1.RequestStatus
Replicas int32
ReplicaRequirements *workv1alpha2.ReplicaRequirements
Dependencies []configv1alpha1.DependentObjectReference

View File

@ -50,7 +50,7 @@ func CreateV1alpha1ExploreReview(uid types.UID, attributes *RequestAttributes) *
}
// VerifyExploreReview checks the validity of the provided explore review object, and returns ResponseAttributes,
// or an error if the provided admission review was not valid.
// or an error if the provided explore review was not valid.
func VerifyExploreReview(uid types.UID, operation configv1alpha1.OperationType, review runtime.Object) (response *ResponseAttributes, err error) {
switch r := review.(type) {
case *configv1alpha1.ExploreReview:
@ -62,41 +62,59 @@ func VerifyExploreReview(uid types.UID, operation configv1alpha1.OperationType,
return nil, fmt.Errorf("expected response.uid %q, got %q", uid, r.Response.UID)
}
v1alpha1GVK := configv1alpha1.SchemeGroupVersion.WithKind("ExploreReview")
if r.GroupVersionKind() != v1alpha1GVK {
return nil, fmt.Errorf("expected webhook response of %v, got %v", v1alpha1GVK.String(), r.GroupVersionKind().String())
}
if err := verifyExploreResponse(operation, r.Response); err != nil {
res, err := verifyExploreResponse(operation, r.Response)
if err != nil {
return nil, err
}
return extractResponseFromV1alpha1ExploreReview(r.Response), nil
return res, nil
default:
return nil, fmt.Errorf("unexpected response type %T", review)
}
}
func verifyExploreResponse(operation configv1alpha1.OperationType, response *configv1alpha1.ExploreResponse) error {
func verifyExploreResponse(operation configv1alpha1.OperationType, response *configv1alpha1.ExploreResponse) (*ResponseAttributes, error) {
res := &ResponseAttributes{}
res.Successful = response.Successful
if !response.Successful {
if response.Status == nil {
return nil, fmt.Errorf("webhook require response.status when response.successful is false")
}
res.Status = *response.Status
return res, nil
}
switch operation {
case configv1alpha1.ExploreReplica:
if response.Replicas == nil {
return fmt.Errorf("webhook returned nil response.replicas")
return nil, fmt.Errorf("webhook returned nil response.replicas")
}
return nil
res.Replicas = *response.Replicas
res.ReplicaRequirements = response.ReplicaRequirements
return res, nil
case configv1alpha1.ExploreDependencies:
return nil
res.Dependencies = response.Dependencies
return res, nil
case configv1alpha1.ExplorePacking, configv1alpha1.ExploreReplicaRevising,
configv1alpha1.ExploreRetaining, configv1alpha1.ExploreStatusAggregating:
return verifyExploreResponseWithPatch(response)
err := verifyExploreResponseWithPatch(response)
if err != nil {
return nil, err
}
res.Patch = response.Patch
res.PatchType = *response.PatchType
return res, nil
case configv1alpha1.ExploreStatus:
return nil
res.RawStatus = *response.RawStatus
return res, nil
case configv1alpha1.ExploreHealthy:
if response.Healthy == nil {
return fmt.Errorf("webhook returned nil response.healthy")
return nil, fmt.Errorf("webhook returned nil response.healthy")
}
return nil
res.Healthy = *response.Healthy
return res, nil
default:
return fmt.Errorf("input wrong operation type: %s", operation)
return nil, fmt.Errorf("input wrong operation type: %s", operation)
}
}
@ -116,15 +134,3 @@ func verifyExploreResponseWithPatch(response *configv1alpha1.ExploreResponse) er
}
return nil
}
func extractResponseFromV1alpha1ExploreReview(response *configv1alpha1.ExploreResponse) *ResponseAttributes {
return &ResponseAttributes{
Replicas: *response.Replicas,
ReplicaRequirements: response.ReplicaRequirements,
Dependencies: response.Dependencies,
Patch: response.Patch,
PatchType: *response.PatchType,
RawStatus: *response.RawStatus,
Healthy: *response.Healthy,
}
}

View File

@ -0,0 +1,38 @@
package explorer
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json"
)
// Decoder knows how to decode the contents of an explorer
// request into a concrete object.
type Decoder struct {
codecs serializer.CodecFactory
}
// NewDecoder creates a Decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) *Decoder {
return &Decoder{
codecs: serializer.NewCodecFactory(scheme),
}
}
// Decode decodes the inlined object in the ExploreRequest into the passed-in runtime.Object.
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
func (d *Decoder) Decode(req Request, into runtime.Object) error {
if len(req.Object.Raw) == 0 {
return fmt.Errorf("there is no context to decode")
}
if unstructuredInto, isUnstructured := into.(*unstructured.Unstructured); isUnstructured {
// unmarshal into unstructured's underlying object to avoid calling the decoder
return json.Unmarshal(req.Object.Raw, &unstructuredInto.Object)
}
deserializer := d.codecs.UniversalDeserializer()
return runtime.DecodeInto(deserializer, req.Object.Raw, into)
}

View File

@ -0,0 +1,91 @@
package explorer
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/klog/v2"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
var admissionScheme = runtime.NewScheme()
var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
// ServeHTTP write reply headers and data to the ResponseWriter and then return.
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var body []byte
var err error
ctx := r.Context()
var reviewResponse Response
if r.Body == nil {
err = errors.New("request body is empty")
klog.Errorf("bad request: %w", err)
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
defer r.Body.Close()
if body, err = ioutil.ReadAll(r.Body); err != nil {
klog.Errorf("unable to read the body from the incoming request: %w", err)
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
// verify the content type is accurate
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
err = fmt.Errorf("contentType=%s, expected application/json", contentType)
klog.Errorf("unable to process a request with an unknown content type: %w", err)
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
request := Request{}
er := configv1alpha1.ExploreReview{}
// avoid an extra copy
er.Request = &request.ExploreRequest
_, _, err = admissionCodecs.UniversalDeserializer().Decode(body, nil, &er)
if err != nil {
klog.Errorf("unable to decode the request: %w", err)
reviewResponse = Errored(http.StatusBadRequest, err)
wh.writeResponse(w, reviewResponse)
return
}
klog.V(1).Infof("received request UID: %q, kind: %s", request.UID, request.Kind)
reviewResponse = wh.Handle(ctx, request)
wh.writeResponse(w, reviewResponse)
}
// writeResponse writes response to w generically, i.e. without encoding GVK information.
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
wh.writeExploreResponse(w, configv1alpha1.ExploreReview{
Response: &response.ExploreResponse,
})
}
// writeExploreResponse writes ar to w.
func (wh *Webhook) writeExploreResponse(w io.Writer, review configv1alpha1.ExploreReview) {
if err := json.NewEncoder(w).Encode(review); err != nil {
klog.Errorf("unable to encode the response: %w", err)
wh.writeResponse(w, Errored(http.StatusInternalServerError, err))
} else {
response := review.Response
if response.Successful {
klog.V(4).Infof("wrote response UID: %q, successful: %t", response.UID, response.Successful)
} else {
klog.V(4).Infof("wrote response UID: %q, successful: %t, response.status.code: %d, response.status.message: %s",
response.UID, response.Successful, response.Status.Code, response.Status.Message)
}
}
}

View File

@ -0,0 +1,16 @@
package explorer
// DecoderInjector is used by the ControllerManager to inject decoder into webhook handlers.
type DecoderInjector interface {
InjectDecoder(*Decoder)
}
// InjectDecoderInto will set decoder on i and return the result if it implements Decoder.
// Returns false if i does not implement Decoder.
func InjectDecoderInto(decoder *Decoder, i interface{}) bool {
if s, ok := i.(DecoderInjector); ok {
s.InjectDecoder(decoder)
return true
}
return false
}

View File

@ -0,0 +1,42 @@
package explorer
import (
"net/http"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// Errored creates a new Response for error-handling a request.
func Errored(code int32, err error) Response {
return Response{
ExploreResponse: configv1alpha1.ExploreResponse{
Successful: false,
Status: &configv1alpha1.RequestStatus{
Code: code,
Message: err.Error(),
},
},
}
}
// Succeeded constructs a response indicating the given operation is handled successfully.
func Succeeded(msg string) Response {
return ValidationResponse(true, msg)
}
// ValidationResponse returns a response for handle a explore request.
func ValidationResponse(successful bool, msg string) Response {
code := http.StatusForbidden
if successful {
code = http.StatusOK
}
return Response{
ExploreResponse: configv1alpha1.ExploreResponse{
Successful: successful,
Status: &configv1alpha1.RequestStatus{
Code: int32(code),
Message: msg,
},
},
}
}

View File

@ -0,0 +1,69 @@
package explorer
import (
"context"
"net/http"
"k8s.io/klog/v2"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// Request defines the input for an explorer handler.
// It contains information to identify the object in
// question (kind, name, namespace), as well as the
// operation in request(e.g. ExploreReplica, ExplorePacking,
// etc), and the object itself.
type Request struct {
configv1alpha1.ExploreRequest
}
// Response is the output of an explorer handler.
type Response struct {
configv1alpha1.ExploreResponse
}
// Complete populates any fields that are yet to be set in
// the underlying ExploreResponse, It mutates the response.
func (r *Response) Complete(req Request) {
r.UID = req.UID
// ensure that we have a valid status code
if r.Status == nil {
r.Status = &configv1alpha1.RequestStatus{}
}
if r.Status.Code == 0 {
r.Status.Code = http.StatusOK
}
}
// Handler can handle an ExploreRequest.
type Handler interface {
// Handle yields a response to an ExploreRequest.
//
// The supplied context is extracted from the received http.Request, allowing wrapping
// http.Handlers to inject values into and control cancellation of downstream request processing.
Handle(context.Context, Request) Response
}
// Webhook represents each individual webhook.
type Webhook struct {
// handler actually processes an admission request returning whether it was allowed or denied,
// and potentially patches to apply to the handler.
handler Handler
}
// NewWebhook return a Webhook
func NewWebhook(handler Handler, decoder *Decoder) *Webhook {
if !InjectDecoderInto(decoder, handler) {
klog.Errorf("Inject decoder into handler err")
}
return &Webhook{handler: handler}
}
// Handle processes ExploreRequest.
func (wh *Webhook) Handle(ctx context.Context, req Request) Response {
resp := wh.handler.Handle(ctx, req)
resp.Complete(req)
return resp
}