From 416f85982fecaa3199bb97392fcc7e83c3d8bcda Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Tue, 20 Apr 2021 17:34:02 +0800 Subject: [PATCH] add admissionreview v1 --- pkg/apiserver/apiserver.go | 133 ++++++++-- pkg/apiserver/apiserver_test.go | 245 ++++++++++++++++++ pkg/cmd/server/start.go | 6 +- .../admissionreview/admission_review_v1.go | 47 ++++ 4 files changed, 411 insertions(+), 20 deletions(-) create mode 100644 pkg/apiserver/apiserver_test.go create mode 100644 pkg/registry/admissionreview/admission_review_v1.go diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index fa6df5dd2..218c3ba78 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + admissionv1 "k8s.io/api/admission/v1" admissionv1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -34,12 +35,24 @@ type ValidatingAdmissionHook interface { // MutatingAdmissionHook as well, the two resources for validating and mutating admission must be different. // Note: this is (usually) not the same as the payload resource! ValidatingResource() (plural schema.GroupVersionResource, singular string) +} + +type ValidatingAdmissionHookV1Beta1 interface { + ValidatingAdmissionHook // Validate is called to decide whether to accept the admission request. The returned AdmissionResponse // must not use the Patch field. Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse } +type ValidatingAdmissionHookV1 interface { + ValidatingAdmissionHook + + // Validate is called to decide whether to accept the v1 admission request. The returned AdmissionResponse + // must not use the Patch field. + Validate(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse +} + type MutatingAdmissionHook interface { AdmissionHook @@ -47,13 +60,26 @@ type MutatingAdmissionHook interface { // ValidatingAdmissionHook as well, the two resources for validating and mutating admission must be different. // Note: this is (usually) not the same as the payload resource! MutatingResource() (plural schema.GroupVersionResource, singular string) +} + +type MutatingAdmissionHookV1Beta1 interface { + MutatingAdmissionHook // Admit is called to decide whether to accept the admission request. The returned AdmissionResponse may // use the Patch field to mutate the object from the passed AdmissionRequest. Admit(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse } +type MutatingAdmissionHookV1 interface { + MutatingAdmissionHook + + // Admit is called to decide whether to accept the v1 admission request. The returned AdmissionResponse may + // use the Patch field to mutate the object from the passed AdmissionRequest. + Admit(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse +} + func init() { + admissionv1.AddToScheme(Scheme) admissionv1beta1.AddToScheme(Scheme) // we need to add the options to empty v1 @@ -74,6 +100,7 @@ func init() { type Config struct { GenericConfig *genericapiserver.RecommendedConfig ExtraConfig ExtraConfig + RestConfig *restclient.Config } type ExtraConfig struct { @@ -88,6 +115,7 @@ type AdmissionServer struct { type completedConfig struct { GenericConfig genericapiserver.CompletedConfig ExtraConfig *ExtraConfig + RestConfig *restclient.Config } type CompletedConfig struct { @@ -100,6 +128,7 @@ func (c *Config) Complete() CompletedConfig { completedCfg := completedConfig{ c.GenericConfig.Complete(), &c.ExtraConfig, + c.RestConfig, } completedCfg.GenericConfig.Version = &version.Info{ @@ -121,9 +150,12 @@ func (c completedConfig) New() (*AdmissionServer, error) { GenericAPIServer: genericServer, } - inClusterConfig, err := restclient.InClusterConfig() - if err != nil { - return nil, err + restConfig := c.RestConfig + if restConfig == nil { + restConfig, err = restclient.InClusterConfig() + if err != nil { + return nil, err + } } for _, versionMap := range admissionHooksByGroupThenVersion(c.ExtraConfig.AdmissionHooks...) { @@ -147,7 +179,10 @@ func (c completedConfig) New() (*AdmissionServer, error) { // just overwrite the groupversion with a random one. We don't really care or know. apiGroupInfo.PrioritizedVersions = appendUniqueGroupVersion(apiGroupInfo.PrioritizedVersions, admissionVersion) - admissionReview := admissionreview.NewREST(admissionHook.Admission) + admissionReview := getAdmissionRest(admissionHook) + if admissionReview == nil { + continue + } v1alpha1storage, ok := apiGroupInfo.VersionedResourcesStorageMap[admissionVersion.Version] if !ok { v1alpha1storage = map[string]rest.Storage{} @@ -170,7 +205,7 @@ func (c completedConfig) New() (*AdmissionServer, error) { } s.GenericAPIServer.AddPostStartHookOrDie(postStartName, func(context genericapiserver.PostStartHookContext) error { - return admissionHook.Initialize(inClusterConfig, context.StopCh) + return admissionHook.Initialize(restConfig, context.StopCh) }, ) } @@ -213,55 +248,119 @@ func admissionHooksByGroupThenVersion(admissionHooks ...AdmissionHook) map[strin ret := map[string]map[string][]admissionHookWrapper{} for i := range admissionHooks { - if mutatingHook, ok := admissionHooks[i].(MutatingAdmissionHook); ok { + if mutatingHook, ok := admissionHooks[i].(MutatingAdmissionHookV1Beta1); ok { gvr, _ := mutatingHook.MutatingResource() group, ok := ret[gvr.Group] if !ok { group = map[string][]admissionHookWrapper{} ret[gvr.Group] = group } - group[gvr.Version] = append(group[gvr.Version], mutatingAdmissionHookWrapper{mutatingHook}) + group[gvr.Version] = append(group[gvr.Version], mutatingAdmissionHookV1Beta1Wrapper{hook: mutatingHook}) } - if validatingHook, ok := admissionHooks[i].(ValidatingAdmissionHook); ok { + if validatingHook, ok := admissionHooks[i].(ValidatingAdmissionHookV1Beta1); ok { gvr, _ := validatingHook.ValidatingResource() group, ok := ret[gvr.Group] if !ok { group = map[string][]admissionHookWrapper{} ret[gvr.Group] = group } - group[gvr.Version] = append(group[gvr.Version], validatingAdmissionHookWrapper{validatingHook}) + group[gvr.Version] = append(group[gvr.Version], validatingAdmissionHookV1Beta1Wrapper{hook: validatingHook}) + } + if mutatingHook, ok := admissionHooks[i].(MutatingAdmissionHookV1); ok { + gvr, _ := mutatingHook.MutatingResource() + group, ok := ret[gvr.Group] + if !ok { + group = map[string][]admissionHookWrapper{} + ret[gvr.Group] = group + } + group[gvr.Version] = append(group[gvr.Version], mutatingAdmissionHookV1Wrapper{hook: mutatingHook}) + } + if validatingHook, ok := admissionHooks[i].(ValidatingAdmissionHookV1); ok { + gvr, _ := validatingHook.ValidatingResource() + group, ok := ret[gvr.Group] + if !ok { + group = map[string][]admissionHookWrapper{} + ret[gvr.Group] = group + } + group[gvr.Version] = append(group[gvr.Version], validatingAdmissionHookV1Wrapper{hook: validatingHook}) } } return ret } +func getAdmissionRest(wrapper admissionHookWrapper) rest.Storage { + switch t := wrapper.(type) { + case admissionHookWrapperV1Alpha1: + return admissionreview.NewREST(t.Admission) + case admissionHookWrapperV1: + return admissionreview.NewV1REST(t.Admission) + } + + return nil +} + // admissionHookWrapper wraps either a validating or mutating admission hooks, calling the respective resource and admission method. type admissionHookWrapper interface { Resource() (plural schema.GroupVersionResource, singular string) +} + +type admissionHookWrapperV1Alpha1 interface { + admissionHookWrapper Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse } -type mutatingAdmissionHookWrapper struct { - hook MutatingAdmissionHook +type admissionHookWrapperV1 interface { + admissionHookWrapper + Admission(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse +} + +// v1beta1 wrappers +type mutatingAdmissionHookV1Beta1Wrapper struct { + hook MutatingAdmissionHookV1Beta1 +} + +func (h mutatingAdmissionHookV1Beta1Wrapper) Resource() (plural schema.GroupVersionResource, singular string) { + return h.hook.MutatingResource() +} + +func (h mutatingAdmissionHookV1Beta1Wrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { + return h.hook.Admit(admissionSpec) +} + +type validatingAdmissionHookV1Beta1Wrapper struct { + hook ValidatingAdmissionHookV1Beta1 +} + +func (h validatingAdmissionHookV1Beta1Wrapper) Resource() (plural schema.GroupVersionResource, singular string) { + return h.hook.ValidatingResource() +} + +func (h validatingAdmissionHookV1Beta1Wrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { + return h.hook.Validate(admissionSpec) +} + +// v1 wrappers +type mutatingAdmissionHookV1Wrapper struct { + hook MutatingAdmissionHookV1 } -func (h mutatingAdmissionHookWrapper) Resource() (plural schema.GroupVersionResource, singular string) { +func (h mutatingAdmissionHookV1Wrapper) Resource() (plural schema.GroupVersionResource, singular string) { return h.hook.MutatingResource() } -func (h mutatingAdmissionHookWrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { +func (h mutatingAdmissionHookV1Wrapper) Admission(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { return h.hook.Admit(admissionSpec) } -type validatingAdmissionHookWrapper struct { - hook ValidatingAdmissionHook +type validatingAdmissionHookV1Wrapper struct { + hook ValidatingAdmissionHookV1 } -func (h validatingAdmissionHookWrapper) Resource() (plural schema.GroupVersionResource, singular string) { +func (h validatingAdmissionHookV1Wrapper) Resource() (plural schema.GroupVersionResource, singular string) { return h.hook.ValidatingResource() } -func (h validatingAdmissionHookWrapper) Admission(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { +func (h validatingAdmissionHookV1Wrapper) Admission(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { return h.hook.Validate(admissionSpec) } diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go new file mode 100644 index 000000000..623614216 --- /dev/null +++ b/pkg/apiserver/apiserver_test.go @@ -0,0 +1,245 @@ +package apiserver + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "testing" + + "net/http" + "net/http/httptest" + + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/client-go/rest" + restclient "k8s.io/client-go/rest" +) + +const ( + validatorPath = "/apis/admission.openshift.io/v1/testvalidators" + mutatorPath = "/apis/admission.openshift.io/v1/testmutators" +) + +type testWebhook struct { +} + +// Initialize is called by generic-admission-server on startup to setup initialization that webhook needs. +func (a *testWebhook) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { + // do nothing + return nil +} + +func (a *testWebhook) MutatingResource() (schema.GroupVersionResource, string) { + return schema.GroupVersionResource{ + Group: "admission.openshift.io", + Version: "v1", + Resource: "testmutators", + }, + "testmutators" +} + +func (a *testWebhook) ValidatingResource() (plural schema.GroupVersionResource, singular string) { + return schema.GroupVersionResource{ + Group: "admission.openshift.io", + Version: "v1", + Resource: "testvalidators", + }, + "testvalidators" +} + +type testWebhookV1Beta1 struct { + testWebhook +} + +func (a *testWebhookV1Beta1) Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { + return &admissionv1beta1.AdmissionResponse{Allowed: true} +} + +func (a *testWebhookV1Beta1) Admit(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { + return &admissionv1beta1.AdmissionResponse{Allowed: true, Patch: []byte("{}")} +} + +type testWebhookV1 struct { + testWebhook +} + +func (a *testWebhookV1) Validate(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { + return &admissionv1.AdmissionResponse{Allowed: true} +} + +func (a *testWebhookV1) Admit(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { + return &admissionv1.AdmissionResponse{Allowed: true, Patch: []byte("{}")} +} + +func TestV1Beta1Webhook(t *testing.T) { + testHook := &testWebhookV1Beta1{} + server := newTestServer(t, testHook) + defer server.Close() + + reviewRequest := &admissionv1beta1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admission.k8s.io/v1beta1", + Kind: "AdmissionReview", + }, + Request: &admissionv1beta1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: "TestKind"}, + }, + } + payload, _ := json.Marshal(reviewRequest) + + cases := []struct { + name string + path string + validateResponse func(t *testing.T, response *admissionv1beta1.AdmissionResponse) + }{ + { + name: "test validator", + path: validatorPath, + validateResponse: func(t *testing.T, response *admissionv1beta1.AdmissionResponse) { + if response == nil { + t.Errorf("expect review response but get nil") + } + if response.Allowed != true { + t.Errorf("expect validation is allowed") + } + }, + }, + { + name: "test mutator", + path: mutatorPath, + validateResponse: func(t *testing.T, response *admissionv1beta1.AdmissionResponse) { + if response == nil { + t.Errorf("expect review response but get nil") + } + if string(response.Patch) != "{}" { + t.Errorf("unexpected mutator response; %v", response) + } + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + url := fmt.Sprintf("%s%s", server.URL, c.path) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(payload)) + if err != nil { + t.Errorf("unexpected error when calling webhook, but got %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("unexpected error reading body at url %q: %v", url, err) + } + + reviewResponse := &admissionv1beta1.AdmissionReview{} + err = json.Unmarshal(body, reviewResponse) + if err != nil { + t.Errorf("unexpected error parsing json body at path %q: %v", url, err) + } + + c.validateResponse(t, reviewResponse.Response) + }) + } +} + +func TestV1Webhook(t *testing.T) { + testHook := &testWebhookV1{} + server := newTestServer(t, testHook) + defer server.Close() + + reviewRequest := &admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admission.k8s.io/v1", + Kind: "AdmissionReview", + }, + Request: &admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: "TestKind"}, + }, + } + payload, _ := json.Marshal(reviewRequest) + + cases := []struct { + name string + path string + validateResponse func(t *testing.T, response *admissionv1.AdmissionResponse) + }{ + { + name: "test validator", + path: validatorPath, + validateResponse: func(t *testing.T, response *admissionv1.AdmissionResponse) { + if response == nil { + t.Errorf("expect review response but get nil") + } + if response.Allowed != true { + t.Errorf("expect validation is allowed") + } + }, + }, + { + name: "test mutator", + path: mutatorPath, + validateResponse: func(t *testing.T, response *admissionv1.AdmissionResponse) { + if response == nil { + t.Errorf("expect review response but get nil") + } + if string(response.Patch) != "{}" { + t.Errorf("unexpected mutator response; %v", response) + } + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + url := fmt.Sprintf("%s%s", server.URL, c.path) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(payload)) + if err != nil { + t.Errorf("unexpected error when calling webhook, but got %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("unexpected error reading body at url %q: %v", url, err) + } + + fmt.Printf("body is %v\n", string(body)) + + reviewResponse := &admissionv1.AdmissionReview{} + err = json.Unmarshal(body, reviewResponse) + if err != nil { + t.Errorf("unexpected error parsing json body at path %q: %v", url, err) + } + + c.validateResponse(t, reviewResponse.Response) + }) + } +} + +func newTestServer(t *testing.T, webhook AdmissionHook) *httptest.Server { + serverConfig := genericapiserver.NewRecommendedConfig(Codecs) + serverConfig.ExternalAddress = "192.168.10.4:443" + serverConfig.PublicAddress = net.ParseIP("192.168.10.4") + serverConfig.LegacyAPIGroupPrefixes = sets.NewString("/api") + serverConfig.LoopbackClientConfig = &restclient.Config{} + + config := &Config{ + GenericConfig: serverConfig, + ExtraConfig: ExtraConfig{ + []AdmissionHook{webhook}, + }, + RestConfig: &restclient.Config{}, + } + + addmissionServer, err := config.Complete().New() + if err != nil { + t.Errorf("unexpected error building server: %v", err) + } + server := httptest.NewServer(addmissionServer.GenericAPIServer.Handler) + return server +} diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 624d92431..5d2614c30 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" + admissionv1 "k8s.io/api/admission/v1" admissionv1beta1 "k8s.io/api/admission/v1beta1" genericapiserver "k8s.io/apiserver/pkg/server" genericoptions "k8s.io/apiserver/pkg/server/options" @@ -30,7 +31,7 @@ func NewAdmissionServerOptions(out, errOut io.Writer, admissionHooks ...apiserve // TODO we will nil out the etcd storage options. This requires a later level of k8s.io/apiserver RecommendedOptions: genericoptions.NewRecommendedOptions( defaultEtcdPathPrefix, - apiserver.Codecs.LegacyCodec(admissionv1beta1.SchemeGroupVersion), + apiserver.Codecs.LegacyCodec(admissionv1.SchemeGroupVersion, admissionv1beta1.SchemeGroupVersion), ), AdmissionHooks: admissionHooks, @@ -45,8 +46,7 @@ func NewAdmissionServerOptions(out, errOut io.Writer, admissionHooks ...apiserve // delegating authorizer now allows this. o.RecommendedOptions.Authorization = o.RecommendedOptions.Authorization. WithAlwaysAllowPaths("/healthz", "/readyz", "/livez"). // this allows the kubelet to always get health and readiness without causing an access check - WithAlwaysAllowGroups("system:masters") // in a kube cluster, system:masters can take any action, so there is no need to ask for an authz check - + WithAlwaysAllowGroups("system:masters") // in a kube cluster, system:masters can take any action, so there is no need to ask for an authz check return o } diff --git a/pkg/registry/admissionreview/admission_review_v1.go b/pkg/registry/admissionreview/admission_review_v1.go new file mode 100644 index 000000000..d032f1d78 --- /dev/null +++ b/pkg/registry/admissionreview/admission_review_v1.go @@ -0,0 +1,47 @@ +package admissionreview + +import ( + "context" + + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" +) + +type AdmissionV1HookFunc func(admissionSpec *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse + +type V1REST struct { + hookFn AdmissionV1HookFunc +} + +var _ rest.Creater = &REST{} +var _ rest.Scoper = &REST{} +var _ rest.GroupVersionKindProvider = &REST{} + +func NewV1REST(hookFn AdmissionV1HookFunc) *V1REST { + return &V1REST{ + hookFn: hookFn, + } +} + +func (r *V1REST) New() runtime.Object { + return &admissionv1.AdmissionReview{} +} + +func (r *V1REST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind { + return admissionv1.SchemeGroupVersion.WithKind("AdmissionReview") +} + +func (r *V1REST) NamespaceScoped() bool { + return false +} + +func (r *V1REST) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { + admissionReview := obj.(*admissionv1.AdmissionReview) + admissionReview.Response = r.hookFn(admissionReview.Request) + // Copey request uid to response + admissionReview.Response.UID = admissionReview.Request.UID + return admissionReview, nil +}