123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383 |
- /*
- Copyright 2019 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 admissionwebhook
- import (
- "crypto/tls"
- "crypto/x509"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "sort"
- "strings"
- "sync"
- "testing"
- "time"
- "k8s.io/api/admission/v1beta1"
- admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
- appsv1beta1 "k8s.io/api/apps/v1beta1"
- corev1 "k8s.io/api/core/v1"
- v1 "k8s.io/api/core/v1"
- extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
- policyv1beta1 "k8s.io/api/policy/v1beta1"
- apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
- "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/apimachinery/pkg/util/sets"
- "k8s.io/apimachinery/pkg/util/wait"
- dynamic "k8s.io/client-go/dynamic"
- clientset "k8s.io/client-go/kubernetes"
- "k8s.io/client-go/rest"
- "k8s.io/client-go/util/retry"
- kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
- "k8s.io/kubernetes/test/integration/etcd"
- "k8s.io/kubernetes/test/integration/framework"
- )
- const (
- testNamespace = "webhook-integration"
- testClientUsername = "webhook-integration-client"
- mutation = "mutation"
- validation = "validation"
- )
- type testContext struct {
- t *testing.T
- admissionHolder *holder
- client dynamic.Interface
- clientset clientset.Interface
- verb string
- gvr schema.GroupVersionResource
- resource metav1.APIResource
- resources map[schema.GroupVersionResource]metav1.APIResource
- }
- type testFunc func(*testContext)
- var (
- // defaultResourceFuncs holds the default test functions.
- // may be overridden for specific resources by customTestFuncs.
- defaultResourceFuncs = map[string]testFunc{
- "create": testResourceCreate,
- "update": testResourceUpdate,
- "patch": testResourcePatch,
- "delete": testResourceDelete,
- "deletecollection": testResourceDeletecollection,
- }
- // defaultSubresourceFuncs holds default subresource test functions.
- // may be overridden for specific resources by customTestFuncs.
- defaultSubresourceFuncs = map[string]testFunc{
- "update": testSubresourceUpdate,
- "patch": testSubresourcePatch,
- }
- // customTestFuncs holds custom test functions by resource and verb.
- customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
- gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
- gvr("apps", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
- gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
- gvr("", "v1", "pods/attach"): {"create": testPodConnectSubresource},
- gvr("", "v1", "pods/exec"): {"create": testPodConnectSubresource},
- gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource},
- gvr("", "v1", "bindings"): {"create": testPodBindingEviction},
- gvr("", "v1", "pods/binding"): {"create": testPodBindingEviction},
- gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction},
- gvr("", "v1", "nodes/proxy"): {"*": testSubresourceProxy},
- gvr("", "v1", "pods/proxy"): {"*": testSubresourceProxy},
- gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
- gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers},
- gvr("custom.fancy.com", "v2", "pants"): {"create": testNoPruningCustomFancy},
- }
- // admissionExemptResources lists objects which are exempt from admission validation/mutation,
- // only resources exempted from admission processing by API server should be listed here.
- admissionExemptResources = map[schema.GroupVersionResource]bool{
- gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
- gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
- }
- parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
- gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
- }
- // stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden.
- stubDataOverrides = map[schema.GroupVersionResource]string{
- // Non persistent Reviews resource
- gvr("authentication.k8s.io", "v1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
- gvr("authentication.k8s.io", "v1beta1", "tokenreviews"): `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
- gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
- gvr("authorization.k8s.io", "v1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
- gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
- gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
- gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
- gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
- gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
- gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
- // Other Non persistent resources
- }
- )
- type webhookOptions struct {
- // phase indicates whether this is a mutating or validating webhook
- phase string
- // converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion.
- // if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK.
- // if false, recordGVR and expectGVK are compared directly to the admission review.
- converted bool
- }
- type holder struct {
- lock sync.RWMutex
- t *testing.T
- recordGVR metav1.GroupVersionResource
- recordOperation v1beta1.Operation
- recordNamespace string
- recordName string
- expectGVK schema.GroupVersionKind
- expectObject bool
- expectOldObject bool
- expectOptionsGVK schema.GroupVersionKind
- expectOptions bool
- // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource.
- // When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook.
- gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource
- // gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource.
- // When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook.
- gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind
- recorded map[webhookOptions]*v1beta1.AdmissionRequest
- }
- func (h *holder) reset(t *testing.T) {
- h.lock.Lock()
- defer h.lock.Unlock()
- h.t = t
- h.recordGVR = metav1.GroupVersionResource{}
- h.expectGVK = schema.GroupVersionKind{}
- h.recordOperation = ""
- h.recordName = ""
- h.recordNamespace = ""
- h.expectObject = false
- h.expectOldObject = false
- h.expectOptionsGVK = schema.GroupVersionKind{}
- h.expectOptions = false
- // Set up the recorded map with nil records for all combinations
- h.recorded = map[webhookOptions]*v1beta1.AdmissionRequest{}
- for _, phase := range []string{mutation, validation} {
- for _, converted := range []bool{true, false} {
- h.recorded[webhookOptions{phase: phase, converted: converted}] = nil
- }
- }
- }
- func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
- // Special-case namespaces, since the object name shows up in request attributes for update/delete requests
- if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" && operation != v1beta1.Create {
- namespace = name
- }
- h.lock.Lock()
- defer h.lock.Unlock()
- h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
- h.expectGVK = gvk
- h.recordOperation = operation
- h.recordName = name
- h.recordNamespace = namespace
- h.expectObject = object
- h.expectOldObject = oldObject
- h.expectOptionsGVK = optionsGVK
- h.expectOptions = options
- // Set up the recorded map with nil records for all combinations
- h.recorded = map[webhookOptions]*v1beta1.AdmissionRequest{}
- for _, phase := range []string{mutation, validation} {
- for _, converted := range []bool{true, false} {
- h.recorded[webhookOptions{phase: phase, converted: converted}] = nil
- }
- }
- }
- func (h *holder) record(phase string, converted bool, request *v1beta1.AdmissionRequest) {
- h.lock.Lock()
- defer h.lock.Unlock()
- // this is useful to turn on if items aren't getting recorded and you need to figure out why
- debug := false
- if debug {
- h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource)
- }
- resource := request.Resource
- if len(request.SubResource) > 0 {
- resource.Resource += "/" + request.SubResource
- }
- // See if we should record this
- gvrToRecord := h.recordGVR
- if converted {
- // If this is a converted webhook, map to the GVR we expect the webhook to see
- gvrToRecord = h.gvrToConvertedGVR[h.recordGVR]
- }
- if resource != gvrToRecord {
- if debug {
- h.t.Log(resource, "!=", gvrToRecord)
- }
- return
- }
- if request.Operation != h.recordOperation {
- if debug {
- h.t.Log(request.Operation, "!=", h.recordOperation)
- }
- return
- }
- if request.Namespace != h.recordNamespace {
- if debug {
- h.t.Log(request.Namespace, "!=", h.recordNamespace)
- }
- return
- }
- name := request.Name
- if name == "" && request.Object.Object != nil {
- name = request.Object.Object.(*unstructured.Unstructured).GetName()
- }
- if name != h.recordName {
- if debug {
- h.t.Log(name, "!=", h.recordName)
- }
- return
- }
- if debug {
- h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource)
- }
- h.recorded[webhookOptions{phase: phase, converted: converted}] = request
- }
- func (h *holder) verify(t *testing.T) {
- h.lock.Lock()
- defer h.lock.Unlock()
- for options, value := range h.recorded {
- if err := h.verifyRequest(options.converted, value); err != nil {
- t.Errorf("phase:%v, converted:%v error: %v", options.phase, options.converted, err)
- }
- }
- }
- func (h *holder) verifyRequest(converted bool, request *v1beta1.AdmissionRequest) error {
- // Check if current resource should be exempted from Admission processing
- if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] {
- if request == nil {
- return nil
- }
- return fmt.Errorf("admission webhook was called, but not supposed to")
- }
- if request == nil {
- return fmt.Errorf("no request received")
- }
- if h.expectObject {
- if err := h.verifyObject(converted, request.Object.Object); err != nil {
- return fmt.Errorf("object error: %v", err)
- }
- } else if request.Object.Object != nil {
- return fmt.Errorf("unexpected object: %#v", request.Object.Object)
- }
- if h.expectOldObject {
- if err := h.verifyObject(converted, request.OldObject.Object); err != nil {
- return fmt.Errorf("old object error: %v", err)
- }
- } else if request.OldObject.Object != nil {
- return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object)
- }
- if h.expectOptions {
- if err := h.verifyOptions(request.Options.Object); err != nil {
- return fmt.Errorf("options error: %v", err)
- }
- } else if request.Options.Object != nil {
- return fmt.Errorf("unexpected options: %#v", request.Options.Object)
- }
- return nil
- }
- func (h *holder) verifyObject(converted bool, obj runtime.Object) error {
- if obj == nil {
- return fmt.Errorf("no object sent")
- }
- expectGVK := h.expectGVK
- if converted {
- expectGVK = h.gvrToConvertedGVK[h.recordGVR]
- }
- if obj.GetObjectKind().GroupVersionKind() != expectGVK {
- return fmt.Errorf("expected %#v, got %#v", expectGVK, obj.GetObjectKind().GroupVersionKind())
- }
- return nil
- }
- func (h *holder) verifyOptions(options runtime.Object) error {
- if options == nil {
- return fmt.Errorf("no options sent")
- }
- if options.GetObjectKind().GroupVersionKind() != h.expectOptionsGVK {
- return fmt.Errorf("expected %#v, got %#v", h.expectOptionsGVK, options.GetObjectKind().GroupVersionKind())
- }
- return nil
- }
- // TestWebhookV1beta1 tests communication between API server and webhook process.
- func TestWebhookV1beta1(t *testing.T) {
- // holder communicates expectations to webhooks, and results from webhooks
- holder := &holder{
- t: t,
- gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{},
- gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{},
- }
- // set up webhook server
- roots := x509.NewCertPool()
- if !roots.AppendCertsFromPEM(localhostCert) {
- t.Fatal("Failed to append Cert from PEM")
- }
- cert, err := tls.X509KeyPair(localhostCert, localhostKey)
- if err != nil {
- t.Fatalf("Failed to build cert with error: %+v", err)
- }
- webhookMux := http.NewServeMux()
- webhookMux.Handle("/"+mutation, newWebhookHandler(t, holder, mutation, false))
- webhookMux.Handle("/convert/"+mutation, newWebhookHandler(t, holder, mutation, true))
- webhookMux.Handle("/"+validation, newWebhookHandler(t, holder, validation, false))
- webhookMux.Handle("/convert/"+validation, newWebhookHandler(t, holder, validation, true))
- webhookServer := httptest.NewUnstartedServer(webhookMux)
- webhookServer.TLS = &tls.Config{
- RootCAs: roots,
- Certificates: []tls.Certificate{cert},
- }
- webhookServer.StartTLS()
- defer webhookServer.Close()
- // start API server
- etcdConfig := framework.SharedEtcd()
- server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{
- // turn off admission plugins that add finalizers
- "--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
- // force enable all resources so we can check storage.
- // TODO: drop these once we stop allowing them to be served.
- "--runtime-config=api/all=true,extensions/v1beta1/deployments=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/podsecuritypolicies=true,extensions/v1beta1/networkpolicies=true",
- }, etcdConfig)
- defer server.TearDownFn()
- // Configure a client with a distinct user name so that it is easy to distinguish requests
- // made by the client from requests made by controllers. We use this to filter out requests
- // before recording them to ensure we don't accidentally mistake requests from controllers
- // as requests made by the client.
- clientConfig := rest.CopyConfig(server.ClientConfig)
- clientConfig.Impersonate.UserName = testClientUsername
- clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
- client, err := clientset.NewForConfig(clientConfig)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- // create CRDs
- etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
- if _, err := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil {
- t.Fatal(err)
- }
- // gather resources to test
- dynamicClient, err := dynamic.NewForConfig(clientConfig)
- if err != nil {
- t.Fatal(err)
- }
- _, resources, err := client.Discovery().ServerGroupsAndResources()
- if err != nil {
- t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
- }
- gvrsToTest := []schema.GroupVersionResource{}
- resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
- for _, list := range resources {
- defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
- if err != nil {
- t.Errorf("Failed to get GroupVersion for: %+v", list)
- continue
- }
- for _, resource := range list.APIResources {
- if resource.Group == "" {
- resource.Group = defaultGroupVersion.Group
- }
- if resource.Version == "" {
- resource.Version = defaultGroupVersion.Version
- }
- gvr := defaultGroupVersion.WithResource(resource.Name)
- resourcesByGVR[gvr] = resource
- if shouldTestResource(gvr, resource) {
- gvrsToTest = append(gvrsToTest, gvr)
- }
- }
- }
- sort.SliceStable(gvrsToTest, func(i, j int) bool {
- if gvrsToTest[i].Group < gvrsToTest[j].Group {
- return true
- }
- if gvrsToTest[i].Group > gvrsToTest[j].Group {
- return false
- }
- if gvrsToTest[i].Version < gvrsToTest[j].Version {
- return true
- }
- if gvrsToTest[i].Version > gvrsToTest[j].Version {
- return false
- }
- if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
- return true
- }
- if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
- return false
- }
- return true
- })
- // map unqualified resource names to the fully qualified resource we will expect to be converted to
- // Note: this only works because there are no overlapping resource names in-process that are not co-located
- convertedResources := map[string]schema.GroupVersionResource{}
- // build the webhook rules enumerating the specific group/version/resources we want
- convertedRules := []admissionv1beta1.RuleWithOperations{}
- for _, gvr := range gvrsToTest {
- metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
- convertedGVR, ok := convertedResources[gvr.Resource]
- if !ok {
- // this is the first time we've seen this resource
- // record the fully qualified resource we expect
- convertedGVR = gvr
- convertedResources[gvr.Resource] = gvr
- // add an admission rule indicating we can receive this version
- convertedRules = append(convertedRules, admissionv1beta1.RuleWithOperations{
- Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
- Rule: admissionv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
- })
- }
- // record the expected resource and kind
- holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource}
- holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind}
- }
- if err := createV1beta1MutationWebhook(client, webhookServer.URL+"/"+mutation, webhookServer.URL+"/convert/"+mutation, convertedRules); err != nil {
- t.Fatal(err)
- }
- if err := createV1beta1ValidationWebhook(client, webhookServer.URL+"/"+validation, webhookServer.URL+"/convert/"+validation, convertedRules); err != nil {
- t.Fatal(err)
- }
- // Allow the webhook to establish
- time.Sleep(time.Second)
- // Test admission on all resources, subresources, and verbs
- for _, gvr := range gvrsToTest {
- resource := resourcesByGVR[gvr]
- t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
- for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
- if shouldTestResourceVerb(gvr, resource, verb) {
- t.Run(verb, func(t *testing.T) {
- holder.reset(t)
- testFunc := getTestFunc(gvr, verb)
- testFunc(&testContext{
- t: t,
- admissionHolder: holder,
- client: dynamicClient,
- clientset: client,
- verb: verb,
- gvr: gvr,
- resource: resource,
- resources: resourcesByGVR,
- })
- holder.verify(t)
- })
- }
- }
- })
- }
- }
- //
- // generic resource testing
- //
- func testResourceCreate(c *testContext) {
- stubObj, err := getStubObj(c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- ns := ""
- if c.resource.Namespaced {
- ns = testNamespace
- }
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true)
- _, err = c.client.Resource(c.gvr).Namespace(ns).Create(stubObj, metav1.CreateOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- func testResourceUpdate(c *testContext) {
- if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
- obj, err := createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- return err
- }
- obj.SetAnnotations(map[string]string{"update": "true"})
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(obj, metav1.UpdateOptions{})
- return err
- }); err != nil {
- c.t.Error(err)
- return
- }
- }
- func testResourcePatch(c *testContext) {
- obj, err := createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
- obj.GetName(),
- types.MergePatchType,
- []byte(`{"metadata":{"annotations":{"patch":"true"}}}`),
- metav1.PatchOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- func testResourceDelete(c *testContext) {
- // Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject.
- obj, err := createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- background := metav1.DeletePropagationBackground
- zero := int64(0)
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
- err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
- if err != nil {
- c.t.Error(err)
- return
- }
- c.admissionHolder.verify(c.t)
- // wait for the item to be gone
- err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
- obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if errors.IsNotFound(err) {
- return true, nil
- }
- if err == nil {
- c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
- return false, nil
- }
- return false, err
- })
- if err != nil {
- c.t.Error(err)
- return
- }
- // Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject.
- obj, err = createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- // Adding finalizer to the object, then deleting it.
- // We don't add finalizers by setting DeleteOptions.PropagationPolicy
- // because some resource (e.g., events) do not support garbage
- // collector finalizers.
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
- obj.GetName(),
- types.MergePatchType,
- []byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`),
- metav1.PatchOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
- err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
- if err != nil {
- c.t.Error(err)
- return
- }
- c.admissionHolder.verify(c.t)
- // wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed.
- err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
- obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if err != nil {
- return false, err
- }
- finalizers := obj.GetFinalizers()
- if len(finalizers) != 1 {
- c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers())
- return false, nil
- }
- if finalizers[0] != "test/k8s.io" {
- return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers())
- }
- return true, nil
- })
- // remove the finalizer
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
- obj.GetName(),
- types.MergePatchType,
- []byte(`{"metadata":{"finalizers":[]}}`),
- metav1.PatchOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- // wait for the item to be gone
- err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
- obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if errors.IsNotFound(err) {
- return true, nil
- }
- if err == nil {
- c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
- return false, nil
- }
- return false, err
- })
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- func testResourceDeletecollection(c *testContext) {
- obj, err := createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- background := metav1.DeletePropagationBackground
- zero := int64(0)
- // update the object with a label that matches our selector
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
- obj.GetName(),
- types.MergePatchType,
- []byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`),
- metav1.PatchOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- // set expectations
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true)
- // delete
- err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(&metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"})
- if err != nil {
- c.t.Error(err)
- return
- }
- // wait for the item to be gone
- err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
- obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if errors.IsNotFound(err) {
- return true, nil
- }
- if err == nil {
- c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
- return false, nil
- }
- return false, err
- })
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource {
- parentGVR, found := parentResources[gvr]
- // if no special override is found, just drop the subresource
- if !found {
- parentGVR = gvr
- parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0]
- }
- return parentGVR
- }
- func testSubresourceUpdate(c *testContext) {
- if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
- parentGVR := getParentGVR(c.gvr)
- parentResource := c.resources[parentGVR]
- obj, err := createOrGetResource(c.client, parentGVR, parentResource)
- if err != nil {
- return err
- }
- // Save the parent object as what we submit
- submitObj := obj
- gvrWithoutSubresources := c.gvr
- gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
- subresources := strings.Split(c.gvr.Resource, "/")[1:]
- // If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc)
- if sets.NewString(c.resource.Verbs...).Has("get") {
- submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{}, subresources...)
- if err != nil {
- return err
- }
- }
- // Modify the object
- submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"})
- // set expectations
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
- _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update(
- submitObj,
- metav1.UpdateOptions{},
- subresources...,
- )
- return err
- }); err != nil {
- c.t.Error(err)
- }
- }
- func testSubresourcePatch(c *testContext) {
- parentGVR := getParentGVR(c.gvr)
- parentResource := c.resources[parentGVR]
- obj, err := createOrGetResource(c.client, parentGVR, parentResource)
- if err != nil {
- c.t.Error(err)
- return
- }
- gvrWithoutSubresources := c.gvr
- gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
- subresources := strings.Split(c.gvr.Resource, "/")[1:]
- // set expectations
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
- _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch(
- obj.GetName(),
- types.MergePatchType,
- []byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`),
- metav1.PatchOptions{},
- subresources...,
- )
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- func unimplemented(c *testContext) {
- c.t.Errorf("Test function for %+v has not been implemented...", c.gvr)
- }
- //
- // custom methods
- //
- // testNamespaceDelete verifies namespace-specific delete behavior:
- // - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state)
- // - removes finalizer from namespace
- // - ensures admission is called on final delete once finalizers are removed
- func testNamespaceDelete(c *testContext) {
- obj, err := createOrGetResource(c.client, c.gvr, c.resource)
- if err != nil {
- c.t.Error(err)
- return
- }
- background := metav1.DeletePropagationBackground
- zero := int64(0)
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
- err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(obj.GetName(), &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
- if err != nil {
- c.t.Error(err)
- return
- }
- c.admissionHolder.verify(c.t)
- // do the finalization so the namespace can be deleted
- obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers")
- if err != nil {
- c.t.Error(err)
- return
- }
- _, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(obj, metav1.UpdateOptions{}, "finalize")
- if err != nil {
- c.t.Error(err)
- return
- }
- // verify namespace is gone
- obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(obj.GetName(), metav1.GetOptions{})
- if err == nil || !errors.IsNotFound(err) {
- c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err)
- }
- }
- // testDeploymentRollback verifies rollback-specific behavior:
- // - creates a parent deployment
- // - creates a rollback object and posts it
- func testDeploymentRollback(c *testContext) {
- deploymentGVR := gvr("apps", "v1", "deployments")
- obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR])
- if err != nil {
- c.t.Error(err)
- return
- }
- gvrWithoutSubresources := c.gvr
- gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
- subresources := strings.Split(c.gvr.Resource, "/")[1:]
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false, true)
- var rollbackObj runtime.Object
- switch c.gvr {
- case gvr("apps", "v1beta1", "deployments/rollback"):
- rollbackObj = &appsv1beta1.DeploymentRollback{
- TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"},
- Name: obj.GetName(),
- RollbackTo: appsv1beta1.RollbackConfig{Revision: 0},
- }
- case gvr("extensions", "v1beta1", "deployments/rollback"):
- rollbackObj = &extensionsv1beta1.DeploymentRollback{
- TypeMeta: metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"},
- Name: obj.GetName(),
- RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0},
- }
- default:
- c.t.Errorf("unknown rollback resource %#v", c.gvr)
- return
- }
- rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj)
- if err != nil {
- c.t.Errorf("ToUnstructured failed: %v", err)
- return
- }
- rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody}
- rollbackUnstructuredObj.SetName(obj.GetName())
- _, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...)
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- // testPodConnectSubresource verifies connect subresources
- func testPodConnectSubresource(c *testContext) {
- podGVR := gvr("", "v1", "pods")
- pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
- if err != nil {
- c.t.Error(err)
- return
- }
- // check all upgradeable verbs
- for _, httpMethod := range []string{"GET", "POST"} {
- c.t.Logf("verifying %v", httpMethod)
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, pod.GetName(), pod.GetNamespace(), true, false, false)
- var err error
- switch c.gvr {
- case gvr("", "v1", "pods/exec"):
- err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do().Error()
- case gvr("", "v1", "pods/attach"):
- err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do().Error()
- case gvr("", "v1", "pods/portforward"):
- err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do().Error()
- default:
- c.t.Errorf("unknown subresource %#v", c.gvr)
- return
- }
- if err != nil {
- c.t.Logf("debug: result of subresource connect: %v", err)
- }
- c.admissionHolder.verify(c.t)
- }
- }
- // testPodBindingEviction verifies pod binding and eviction admission
- func testPodBindingEviction(c *testContext) {
- podGVR := gvr("", "v1", "pods")
- pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
- if err != nil {
- c.t.Error(err)
- return
- }
- background := metav1.DeletePropagationBackground
- zero := int64(0)
- forceDelete := &metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}
- defer func() {
- err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(pod.GetName(), forceDelete)
- if err != nil && !errors.IsNotFound(err) {
- c.t.Error(err)
- return
- }
- }()
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, pod.GetName(), pod.GetNamespace(), true, false, true)
- switch c.gvr {
- case gvr("", "v1", "bindings"):
- err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{
- ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
- Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
- }).Do().Error()
- case gvr("", "v1", "pods/binding"):
- err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{
- ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
- Target: corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
- }).Do().Error()
- case gvr("", "v1", "pods/eviction"):
- err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1beta1.Eviction{
- ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
- DeleteOptions: forceDelete,
- }).Do().Error()
- default:
- c.t.Errorf("unhandled resource %#v", c.gvr)
- return
- }
- if err != nil {
- c.t.Error(err)
- return
- }
- }
- // testSubresourceProxy verifies proxy subresources
- func testSubresourceProxy(c *testContext) {
- parentGVR := getParentGVR(c.gvr)
- parentResource := c.resources[parentGVR]
- obj, err := createOrGetResource(c.client, parentGVR, parentResource)
- if err != nil {
- c.t.Error(err)
- return
- }
- gvrWithoutSubresources := c.gvr
- gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
- subresources := strings.Split(c.gvr.Resource, "/")[1:]
- verbToHTTPMethods := map[string][]string{
- "create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission
- "update": {"PUT"},
- "patch": {"PATCH"},
- "delete": {"DELETE"},
- }
- httpMethodsToTest, ok := verbToHTTPMethods[c.verb]
- if !ok {
- c.t.Errorf("unknown verb %v", c.verb)
- return
- }
- for _, httpMethod := range httpMethodsToTest {
- c.t.Logf("testing %v", httpMethod)
- request := c.clientset.CoreV1().RESTClient().Verb(httpMethod)
- // add the namespace if required
- if len(obj.GetNamespace()) > 0 {
- request = request.Namespace(obj.GetNamespace())
- }
- // set expectations
- c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, obj.GetName(), obj.GetNamespace(), true, false, false)
- // run the request. we don't actually care if the request is successful, just that admission gets called as expected
- err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do().Error()
- if err != nil {
- c.t.Logf("debug: result of subresource proxy (error expected): %v", err)
- }
- // verify the result
- c.admissionHolder.verify(c.t)
- }
- }
- func testPruningRandomNumbers(c *testContext) {
- testResourceCreate(c)
- cr2pant, err := c.client.Resource(c.gvr).Get("fortytwo", metav1.GetOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- foo, found, err := unstructured.NestedString(cr2pant.Object, "foo")
- if err != nil {
- c.t.Error(err)
- return
- }
- if found {
- c.t.Errorf("expected .foo to be pruned, but got: %s", foo)
- }
- }
- func testNoPruningCustomFancy(c *testContext) {
- testResourceCreate(c)
- cr2pant, err := c.client.Resource(c.gvr).Get("cr2pant", metav1.GetOptions{})
- if err != nil {
- c.t.Error(err)
- return
- }
- foo, _, err := unstructured.NestedString(cr2pant.Object, "foo")
- if err != nil {
- c.t.Error(err)
- return
- }
- // check that no pruning took place
- if expected, got := "test", foo; expected != got {
- c.t.Errorf("expected /foo to be %q, got: %q", expected, got)
- }
- }
- //
- // utility methods
- //
- func newWebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
- data, err := ioutil.ReadAll(r.Body)
- if err != nil {
- t.Error(err)
- return
- }
- if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
- t.Errorf("contentType=%s, expect application/json", contentType)
- return
- }
- review := v1beta1.AdmissionReview{}
- if err := json.Unmarshal(data, &review); err != nil {
- t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
- http.Error(w, err.Error(), 400)
- return
- }
- if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1beta1", "AdmissionReview") {
- t.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
- http.Error(w, err.Error(), 400)
- return
- }
- if len(review.Request.Object.Raw) > 0 {
- u := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
- t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
- http.Error(w, err.Error(), 400)
- return
- }
- review.Request.Object.Object = u
- }
- if len(review.Request.OldObject.Raw) > 0 {
- u := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
- t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
- http.Error(w, err.Error(), 400)
- return
- }
- review.Request.OldObject.Object = u
- }
- if len(review.Request.Options.Raw) > 0 {
- u := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil {
- t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err)
- http.Error(w, err.Error(), 400)
- return
- }
- review.Request.Options.Object = u
- }
- if review.Request.UserInfo.Username == testClientUsername {
- // only record requests originating from this integration test's client
- holder.record(phase, converted, review.Request)
- }
- review.Response = &v1beta1.AdmissionResponse{
- Allowed: true,
- UID: review.Request.UID,
- Result: &metav1.Status{Message: "admitted"},
- }
- // If we're mutating, and have an object, return a patch to exercise conversion
- if phase == mutation && len(review.Request.Object.Raw) > 0 {
- review.Response.Patch = []byte(`[{"op":"add","path":"/foo","value":"test"}]`)
- jsonPatch := v1beta1.PatchTypeJSONPatch
- review.Response.PatchType = &jsonPatch
- }
- w.Header().Set("Content-Type", "application/json")
- if err := json.NewEncoder(w).Encode(review); err != nil {
- t.Errorf("Marshal of response failed with error: %v", err)
- }
- })
- }
- func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
- if f, found := customTestFuncs[gvr][verb]; found {
- return f
- }
- if f, found := customTestFuncs[gvr]["*"]; found {
- return f
- }
- if strings.Contains(gvr.Resource, "/") {
- if f, found := defaultSubresourceFuncs[verb]; found {
- return f
- }
- return unimplemented
- }
- if f, found := defaultResourceFuncs[verb]; found {
- return f
- }
- return unimplemented
- }
- func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
- stub := ""
- if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
- stub = data.Stub
- }
- if data, ok := stubDataOverrides[gvr]; ok {
- stub = data
- }
- if len(stub) == 0 {
- return nil, fmt.Errorf("no stub data for %#v", gvr)
- }
- stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
- if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
- return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
- }
- return stubObj, nil
- }
- func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
- stubObj, err := getStubObj(gvr, resource)
- if err != nil {
- return nil, err
- }
- ns := ""
- if resource.Namespaced {
- ns = testNamespace
- }
- obj, err := client.Resource(gvr).Namespace(ns).Get(stubObj.GetName(), metav1.GetOptions{})
- if err == nil {
- return obj, nil
- }
- if !errors.IsNotFound(err) {
- return nil, err
- }
- return client.Resource(gvr).Namespace(ns).Create(stubObj, metav1.CreateOptions{})
- }
- func gvr(group, version, resource string) schema.GroupVersionResource {
- return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
- }
- func gvk(group, version, kind string) schema.GroupVersionKind {
- return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
- }
- var (
- gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions")
- gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions")
- gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions")
- )
- func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool {
- if !sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection") {
- return false
- }
- return true
- }
- func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool {
- if !sets.NewString(resource.Verbs...).Has(verb) {
- return false
- }
- return true
- }
- //
- // webhook registration helpers
- //
- func createV1beta1ValidationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1beta1.RuleWithOperations) error {
- fail := admissionv1beta1.Fail
- equivalent := admissionv1beta1.Equivalent
- // Attaching Admission webhook to API server
- _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
- Webhooks: []admissionv1beta1.ValidatingWebhook{
- {
- Name: "admission.integration.test",
- ClientConfig: admissionv1beta1.WebhookClientConfig{
- URL: &endpoint,
- CABundle: localhostCert,
- },
- Rules: []admissionv1beta1.RuleWithOperations{{
- Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
- Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
- }},
- FailurePolicy: &fail,
- AdmissionReviewVersions: []string{"v1beta1"},
- },
- {
- Name: "admission.integration.testconversion",
- ClientConfig: admissionv1beta1.WebhookClientConfig{
- URL: &convertedEndpoint,
- CABundle: localhostCert,
- },
- Rules: convertedRules,
- FailurePolicy: &fail,
- MatchPolicy: &equivalent,
- AdmissionReviewVersions: []string{"v1beta1"},
- },
- },
- })
- return err
- }
- func createV1beta1MutationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1beta1.RuleWithOperations) error {
- fail := admissionv1beta1.Fail
- equivalent := admissionv1beta1.Equivalent
- // Attaching Mutation webhook to API server
- _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"},
- Webhooks: []admissionv1beta1.MutatingWebhook{
- {
- Name: "mutation.integration.test",
- ClientConfig: admissionv1beta1.WebhookClientConfig{
- URL: &endpoint,
- CABundle: localhostCert,
- },
- Rules: []admissionv1beta1.RuleWithOperations{{
- Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
- Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
- }},
- FailurePolicy: &fail,
- AdmissionReviewVersions: []string{"v1beta1"},
- },
- {
- Name: "mutation.integration.testconversion",
- ClientConfig: admissionv1beta1.WebhookClientConfig{
- URL: &convertedEndpoint,
- CABundle: localhostCert,
- },
- Rules: convertedRules,
- FailurePolicy: &fail,
- MatchPolicy: &equivalent,
- AdmissionReviewVersions: []string{"v1beta1"},
- },
- },
- })
- return err
- }
- // localhostCert was generated from crypto/tls/generate_cert.go with the following command:
- // go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
- var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
- MIIBjzCCATmgAwIBAgIRAKpi2WmTcFrVjxrl5n5YDUEwDQYJKoZIhvcNAQELBQAw
- EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
- MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
- QQC9fEbRszP3t14Gr4oahV7zFObBI4TfA5i7YnlMXeLinb7MnvT4bkfOJzE6zktn
- 59zP7UiHs3l4YOuqrjiwM413AgMBAAGjaDBmMA4GA1UdDwEB/wQEAwICpDATBgNV
- HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQnMCWCC2V4
- YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUA
- A0EAUsVE6KMnza/ZbodLlyeMzdo7EM/5nb5ywyOxgIOCf0OOLHsPS9ueGLQX9HEG
- //yjTXuhNcUugExIjM/AIwAZPQ==
- -----END CERTIFICATE-----`)
- // localhostKey is the private key for localhostCert.
- var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
- MIIBOwIBAAJBAL18RtGzM/e3XgavihqFXvMU5sEjhN8DmLtieUxd4uKdvsye9Phu
- R84nMTrOS2fn3M/tSIezeXhg66quOLAzjXcCAwEAAQJBAKcRxH9wuglYLBdI/0OT
- BLzfWPZCEw1vZmMR2FF1Fm8nkNOVDPleeVGTWoOEcYYlQbpTmkGSxJ6ya+hqRi6x
- goECIQDx3+X49fwpL6B5qpJIJMyZBSCuMhH4B7JevhGGFENi3wIhAMiNJN5Q3UkL
- IuSvv03kaPR5XVQ99/UeEetUgGvBcABpAiBJSBzVITIVCGkGc7d+RCf49KTCIklv
- bGWObufAR8Ni4QIgWpILjW8dkGg8GOUZ0zaNA6Nvt6TIv2UWGJ4v5PoV98kCIQDx
- rIiZs5QbKdycsv9gQJzwQAogC8o04X3Zz3dsoX+h4A==
- -----END RSA PRIVATE KEY-----`)
|