Skip to content

Commit

Permalink
Add support for admission review v1beta1
Browse files Browse the repository at this point in the history
  • Loading branch information
aledbf committed Oct 2, 2020
1 parent 2feb43b commit 9c94d77
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 167 deletions.
90 changes: 90 additions & 0 deletions internal/admission/controller/convert.go
@@ -0,0 +1,90 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"unsafe"

admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

// these conversions are copied from https://github.com/kubernetes/kubernetes/blob/4db3a096ce8ac730b2280494422e1c4cf5fe875e/pkg/apis/admission/v1beta1/zz_generated.conversion.go
// to avoid copying in kubernetes/kubernetes
// they are sightly modified to remove complexity

func convertV1beta1AdmissionReviewToAdmissionAdmissionReview(in *admissionv1beta1.AdmissionReview, out *admissionv1.AdmissionReview) {
if in.Request != nil {
if out.Request == nil {
out.Request = &admissionv1.AdmissionRequest{}
}
in, out := &in.Request, &out.Request
*out = new(admissionv1.AdmissionRequest)
convertV1beta1AdmissionRequestToAdmissionAdmissionRequest(*in, *out)
} else {
out.Request = nil
}
out.Response = (*admissionv1.AdmissionResponse)(unsafe.Pointer(in.Response))
}

func convertV1beta1AdmissionRequestToAdmissionAdmissionRequest(in *admissionv1beta1.AdmissionRequest, out *admissionv1.AdmissionRequest) {
out.UID = types.UID(in.UID)
out.Kind = in.Kind
out.Resource = in.Resource
out.SubResource = in.SubResource
out.RequestKind = (*metav1.GroupVersionKind)(unsafe.Pointer(in.RequestKind))
out.RequestResource = (*metav1.GroupVersionResource)(unsafe.Pointer(in.RequestResource))
out.RequestSubResource = in.RequestSubResource
out.Name = in.Name
out.Namespace = in.Namespace
out.Operation = admissionv1.Operation(in.Operation)
out.Object = in.Object
out.OldObject = in.OldObject
out.Options = in.Options
}

func convertAdmissionAdmissionReviewToV1beta1AdmissionReview(in *admissionv1.AdmissionReview, out *admissionv1beta1.AdmissionReview) {
if in.Request != nil {
if out.Request == nil {
out.Request = &admissionv1beta1.AdmissionRequest{}
}
in, out := &in.Request, &out.Request
*out = new(admissionv1beta1.AdmissionRequest)
convertAdmissionAdmissionRequestToV1beta1AdmissionRequest(*in, *out)
} else {
out.Request = nil
}
out.Response = (*admissionv1beta1.AdmissionResponse)(unsafe.Pointer(in.Response))
}

func convertAdmissionAdmissionRequestToV1beta1AdmissionRequest(in *admissionv1.AdmissionRequest, out *admissionv1beta1.AdmissionRequest) {
out.UID = types.UID(in.UID)
out.Kind = in.Kind
out.Resource = in.Resource
out.SubResource = in.SubResource
out.RequestKind = (*metav1.GroupVersionKind)(unsafe.Pointer(in.RequestKind))
out.RequestResource = (*metav1.GroupVersionResource)(unsafe.Pointer(in.RequestResource))
out.RequestSubResource = in.RequestSubResource
out.Name = in.Name
out.Namespace = in.Namespace
out.Operation = admissionv1beta1.Operation(in.Operation)
out.Object = in.Object
out.OldObject = in.OldObject
out.Options = in.Options
}
103 changes: 60 additions & 43 deletions internal/admission/controller/main.go
Expand Up @@ -18,13 +18,16 @@ package controller

import (
"fmt"
"net/http"

admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
networking "k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/klog/v2"

"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
)

// Checker must return an error if the ingress provided as argument
Expand Down Expand Up @@ -56,62 +59,76 @@ var (
// HandleAdmission populates the admission Response
// with Allowed=false if the Object is an ingress that would prevent nginx to reload the configuration
// with Allowed=true otherwise
func (ia *IngressAdmission) HandleAdmission(ar *admissionv1.AdmissionReview) {
if ar.Request == nil {
ar.Response = &admissionv1.AdmissionResponse{
Allowed: false,
}
func (ia *IngressAdmission) HandleAdmission(obj runtime.Object) (runtime.Object, error) {
outputVersion := admissionv1.SchemeGroupVersion

return
}
review, isV1 := obj.(*admissionv1.AdmissionReview)

if ar.Request.Resource != networkingV1Beta1Resource && ar.Request.Resource != networkingV1Resource {
err := fmt.Errorf("rejecting admission review because the request does not contains an Ingress resource but %s with name %s in namespace %s",
ar.Request.Resource.String(), ar.Request.Name, ar.Request.Namespace)
ar.Response = &admissionv1.AdmissionResponse{
UID: ar.Request.UID,
Allowed: false,
Result: &metav1.Status{Message: err.Error()},
status := &admissionv1.AdmissionResponse{}
status.UID = review.Request.UID

if !isV1 {
outputVersion = admissionv1beta1.SchemeGroupVersion
reviewv1beta1, isv1beta1 := obj.(*admissionv1beta1.AdmissionReview)
if !isv1beta1 {
return nil, fmt.Errorf("request is not of type apiextensions v1 or v1beta1")
}

return
review = &admissionv1.AdmissionReview{}
convertV1beta1AdmissionReviewToAdmissionAdmissionReview(reviewv1beta1, review)
}

if review.Request.Resource != networkingV1Beta1Resource && review.Request.Resource != networkingV1Resource {
return nil, fmt.Errorf("rejecting admission review because the request does not contains an Ingress resource but %s with name %s in namespace %s",
review.Request.Resource.String(), review.Request.Name, review.Request.Namespace)
}

ingress := networking.Ingress{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(ar.Request.Object.Raw, nil, &ingress); err != nil {
klog.ErrorS(err, "failed to decode ingress", "ingress", ar.Request.Name, "namespace", ar.Request.Namespace)

ar.Response = &admissionv1.AdmissionResponse{
UID: ar.Request.UID,
Allowed: false,

Result: &metav1.Status{Message: err.Error()},
AuditAnnotations: map[string]string{
parser.GetAnnotationWithPrefix("error"): err.Error(),
},

codec := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{
Pretty: true,
})
codec.Decode(review.Request.Object.Raw, nil, nil)
_, _, err := codec.Decode(review.Request.Object.Raw, nil, &ingress)
if err != nil {
klog.ErrorS(err, "failed to decode ingress")
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: err.Error(),
}

return
review.Response = status
return convertResponse(review, outputVersion), nil
}

if err := ia.Checker.CheckIngress(&ingress); err != nil {
klog.ErrorS(err, "failed to generate configuration for ingress", "ingress", ar.Request.Name, "namespace", ar.Request.Namespace)
ar.Response = &admissionv1.AdmissionResponse{
UID: ar.Request.UID,
Allowed: false,
Result: &metav1.Status{Message: err.Error()},
AuditAnnotations: map[string]string{
parser.GetAnnotationWithPrefix("error"): err.Error(),
},
klog.ErrorS(err, "invalid ingress configuration", "ingress", review.Request.Name, "namespace", review.Request.Namespace)
status.Allowed = false
status.Result = &metav1.Status{
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
Message: err.Error(),
}

return
review.Response = status
return convertResponse(review, outputVersion), nil
}

klog.InfoS("successfully validated configuration, accepting", "ingress", ar.Request.Name, "namespace", ar.Request.Namespace)
ar.Response = &admissionv1.AdmissionResponse{
UID: ar.Request.UID,
Allowed: true,
klog.InfoS("successfully validated configuration, accepting", "ingress", review.Request.Name, "namespace", review.Request.Namespace)
status.Allowed = true
review.Response = status

return convertResponse(review, outputVersion), nil
}

func convertResponse(review *admissionv1.AdmissionReview, outputVersion schema.GroupVersion) runtime.Object {
// reply v1
if outputVersion.Version == admissionv1.SchemeGroupVersion.Version {
return review
}

// reply v1beta1
reviewv1beta1 := &admissionv1beta1.AdmissionReview{}
convertAdmissionAdmissionReviewToV1beta1AdmissionReview(review, reviewv1beta1)
return review
}
27 changes: 20 additions & 7 deletions internal/admission/controller/main_test.go
Expand Up @@ -23,6 +23,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
networking "k8s.io/api/networking/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
)

Expand Down Expand Up @@ -53,21 +54,33 @@ func TestHandleAdmission(t *testing.T) {
adm := &IngressAdmission{
Checker: failTestChecker{t: t},
}
review := &admissionv1.AdmissionReview{

result, err := adm.HandleAdmission(&admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Resource: v1.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"},
},
})
if err == nil {
t.Fatalf("with a non ingress resource, the check should not pass")
}

adm.HandleAdmission(review)
if review.Response.Allowed {
t.Fatalf("with a non ingress resource, the check should not pass")
result, err = adm.HandleAdmission(&admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Resource: v1.GroupVersionResource{Group: networking.GroupName, Version: "v1beta1", Resource: "ingresses"},
Object: runtime.RawExtension{
Raw: []byte{0xff},
},
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

review.Request.Resource = v1.GroupVersionResource{Group: networking.GroupName, Version: "v1beta1", Resource: "ingresses"}
review.Request.Object.Raw = []byte{0xff}
review, isV1 := (result).(*admissionv1.AdmissionReview)
if !isV1 {
t.Fatalf("expected AdmissionReview V1 object but %T returned", result)
}

adm.HandleAdmission(review)
if review.Response.Allowed {
t.Fatalf("when the request object is not decodable, the request should not be allowed")
}
Expand Down
55 changes: 31 additions & 24 deletions internal/admission/controller/server.go
Expand Up @@ -17,71 +17,78 @@ limitations under the License.
package controller

import (
"io"
"io/ioutil"
"net/http"

admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/klog/v2"
)

var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)

func init() {
admissionv1beta1.AddToScheme(scheme)
admissionv1.AddToScheme(scheme)
}

// AdmissionController checks if an object
// is allowed in the cluster
type AdmissionController interface {
HandleAdmission(*admissionv1.AdmissionReview)
HandleAdmission(runtime.Object) (runtime.Object, error)
}

// AdmissionControllerServer implements an HTTP server
// for kubernetes validating webhook
// https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook
type AdmissionControllerServer struct {
AdmissionController AdmissionController
Decoder runtime.Decoder
}

// NewAdmissionControllerServer instanciates an admission controller server with
// a default codec
func NewAdmissionControllerServer(ac AdmissionController) *AdmissionControllerServer {
return &AdmissionControllerServer{
AdmissionController: ac,
Decoder: codecs.UniversalDeserializer(),
}
}

// ServeHTTP implements http.Server method
func (acs *AdmissionControllerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
review, err := parseAdmissionReview(acs.Decoder, r.Body)
func (acs *AdmissionControllerServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
defer req.Body.Close()

data, err := ioutil.ReadAll(req.Body)
if err != nil {
klog.ErrorS(err, "Unexpected error decoding request")
klog.ErrorS(err, "Failed to read request body")
w.WriteHeader(http.StatusBadRequest)
return
}

acs.AdmissionController.HandleAdmission(review)
if err := writeAdmissionReview(w, review); err != nil {
klog.ErrorS(err, "Unexpected returning admission review")
codec := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{
Pretty: true,
})

obj, _, err := codec.Decode(data, nil, nil)
if err != nil {
klog.ErrorS(err, "Failed to decode request body")
w.WriteHeader(http.StatusBadRequest)
return
}
}

func parseAdmissionReview(decoder runtime.Decoder, r io.Reader) (*admissionv1.AdmissionReview, error) {
review := &admissionv1.AdmissionReview{}
data, err := ioutil.ReadAll(r)
result, err := acs.AdmissionController.HandleAdmission(obj)
if err != nil {
return nil, err
klog.ErrorS(err, "failed to process webhook request")
w.WriteHeader(http.StatusInternalServerError)
return
}
_, _, err = decoder.Decode(data, nil, review)
return review, err
}

func writeAdmissionReview(w io.Writer, ar *admissionv1.AdmissionReview) error {
e := json.NewEncoder(w)
return e.Encode(ar)
if err := codec.Encode(result, w); err != nil {
klog.ErrorS(err, "failed to encode response body")
w.WriteHeader(http.StatusInternalServerError)
return
}
}

0 comments on commit 9c94d77

Please sign in to comment.