diff --git a/test/integration/apiserver/BUILD b/test/integration/apiserver/BUILD index 0992b699b2..d15fb7f133 100644 --- a/test/integration/apiserver/BUILD +++ b/test/integration/apiserver/BUILD @@ -83,6 +83,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//test/integration/apiserver/admissionwebhook:all-srcs", "//test/integration/apiserver/apply:all-srcs", ], tags = ["automanaged"], diff --git a/test/integration/apiserver/admissionwebhook/BUILD b/test/integration/apiserver/admissionwebhook/BUILD new file mode 100644 index 0000000000..226908f406 --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/BUILD @@ -0,0 +1,48 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "go_default_test", + srcs = [ + "admission_test.go", + "main_test.go", + ], + tags = [ + "etcd", + "integration", + ], + deps = [ + "//cmd/kube-apiserver/app/options:go_default_library", + "//staging/src/k8s.io/api/admission/v1beta1:go_default_library", + "//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", + "//staging/src/k8s.io/api/apps/v1beta1:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/api/extensions/v1beta1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/client-go/dynamic:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes:go_default_library", + "//staging/src/k8s.io/client-go/util/retry:go_default_library", + "//test/integration/etcd:go_default_library", + "//test/integration/framework:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/integration/apiserver/admissionwebhook/admission_test.go b/test/integration/apiserver/admissionwebhook/admission_test.go new file mode 100644 index 0000000000..40a0a1e267 --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/admission_test.go @@ -0,0 +1,947 @@ +/* +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" + v1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "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/util/retry" + "k8s.io/kubernetes/cmd/kube-apiserver/app/options" + "k8s.io/kubernetes/test/integration/etcd" +) + +const ( + testNamespace = "webhook-integration" + + mutation = "mutation" + validation = "validation" +) + +type testContext struct { + t *testing.T + + admissionHolder *holder + + client dynamic.Interface + 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}, + } + + // excludedResources lists resources / verb combinations that are not yet tested. this set should trend to zero. + excludedResources = map[schema.GroupVersionResource]sets.String{ + // TODO: verify non-persisted review objects work with webhook admission in place (and determine whether they should be sent to admission) + gvr("authentication.k8s.io", "v1", "tokenreviews"): sets.NewString("*"), + gvr("authentication.k8s.io", "v1beta1", "tokenreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1", "subjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"): sets.NewString("*"), + gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"): sets.NewString("*"), + + // TODO: webhook config objects are not subject to admission, verify CRUD works and webhooks do not observe them + gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): sets.NewString("*"), + gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): sets.NewString("*"), + + // TODO: implement custom subresource tests (requires special states or requests) + gvr("", "v1", "bindings"): sets.NewString("create"), + gvr("", "v1", "nodes/proxy"): sets.NewString("*"), + gvr("", "v1", "pods/attach"): sets.NewString("create"), + gvr("", "v1", "pods/binding"): sets.NewString("create"), + gvr("", "v1", "pods/eviction"): sets.NewString("create"), + gvr("", "v1", "pods/exec"): sets.NewString("create"), + gvr("", "v1", "pods/portforward"): sets.NewString("create"), + gvr("", "v1", "pods/proxy"): sets.NewString("*"), + gvr("", "v1", "services/proxy"): sets.NewString("*"), + } + + parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{ + gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"), + } +) + +type holder struct { + lock sync.RWMutex + + t *testing.T + + expectGVR metav1.GroupVersionResource + expectGVK schema.GroupVersionKind + expectOperation v1beta1.Operation + expectNamespace string + expectName string + expectObject bool + expectOldObject bool + + recorded map[string]*v1beta1.AdmissionRequest +} + +func (h *holder) reset(t *testing.T) { + h.lock.Lock() + defer h.lock.Unlock() + h.t = t + h.expectGVR = metav1.GroupVersionResource{} + h.expectGVK = schema.GroupVersionKind{} + h.expectOperation = "" + h.expectName = "" + h.expectNamespace = "" + h.expectObject = false + h.expectOldObject = false + h.recorded = map[string]*v1beta1.AdmissionRequest{ + mutation: nil, + validation: nil, + } +} +func (h *holder) expect(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject 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.expectGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource} + h.expectGVK = gvk + h.expectOperation = operation + h.expectName = name + h.expectNamespace = namespace + h.expectObject = object + h.expectOldObject = oldObject + h.recorded = map[string]*v1beta1.AdmissionRequest{ + mutation: nil, + validation: nil, + } +} +func (h *holder) record(phase string, 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 + } + if resource != h.expectGVR { + if debug { + h.t.Log(resource, "!=", h.expectGVR) + } + return + } + + if request.Operation != h.expectOperation { + if debug { + h.t.Log(request.Operation, "!=", h.expectOperation) + } + return + } + if request.Namespace != h.expectNamespace { + if debug { + h.t.Log(request.Namespace, "!=", h.expectNamespace) + } + return + } + + name := request.Name + if name == "" && request.Object.Object != nil { + name = request.Object.Object.(*unstructured.Unstructured).GetName() + } + if name != h.expectName { + if debug { + h.t.Log(name, "!=", h.expectName) + } + return + } + + h.recorded[phase] = request +} + +func (h *holder) verify(t *testing.T) { + h.lock.Lock() + defer h.lock.Unlock() + + if err := h.verifyRequest(h.recorded[mutation]); err != nil { + t.Errorf("mutation error: %v", err) + } + if err := h.verifyRequest(h.recorded[validation]); err != nil { + t.Errorf("validation error: %v", err) + } +} + +func (h *holder) verifyRequest(request *v1beta1.AdmissionRequest) error { + if request == nil { + return fmt.Errorf("no request received") + } + + if h.expectObject { + if err := h.verifyObject(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(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) + } + + return nil +} + +func (h *holder) verifyObject(obj runtime.Object) error { + if obj == nil { + return fmt.Errorf("no object sent") + } + if obj.GetObjectKind().GroupVersionKind() != h.expectGVK { + return fmt.Errorf("expected %#v, got %#v", h.expectGVK, obj.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} + + // 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)) + webhookMux.Handle("/"+validation, newWebhookHandler(t, holder, validation)) + webhookServer := httptest.NewUnstartedServer(webhookMux) + webhookServer.TLS = &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + } + webhookServer.StartTLS() + defer webhookServer.Close() + + // start API server + master := etcd.StartRealMasterOrDie(t, func(opts *options.ServerRunOptions) { + // turn off admission plugins that add finalizers + opts.Admission.GenericAdmission.DisablePlugins = []string{"ServiceAccount", "StorageObjectInUseProtection"} + + // force enable all resources so we can check storage. + // TODO: drop these once we stop allowing them to be served. + opts.APIEnablement.RuntimeConfig["extensions/v1beta1/deployments"] = "true" + opts.APIEnablement.RuntimeConfig["extensions/v1beta1/daemonsets"] = "true" + opts.APIEnablement.RuntimeConfig["extensions/v1beta1/replicasets"] = "true" + opts.APIEnablement.RuntimeConfig["extensions/v1beta1/podsecuritypolicies"] = "true" + opts.APIEnablement.RuntimeConfig["extensions/v1beta1/networkpolicies"] = "true" + }) + defer master.Cleanup() + + if _, err := master.Client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil { + t.Fatal(err) + } + if err := createV1beta1MutationWebhook(master.Client, webhookServer.URL+"/"+mutation); err != nil { + t.Fatal(err) + } + if err := createV1beta1ValidationWebhook(master.Client, webhookServer.URL+"/"+validation); err != nil { + t.Fatal(err) + } + + // gather resources to test + dynamicClient := master.Dynamic + _, resources, err := master.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 + }) + + // 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, + 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), v1beta1.Create, stubObj.GetName(), ns, true, false) + _, 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), v1beta1.Update, obj.GetName(), obj.GetNamespace(), 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), v1beta1.Update, obj.GetName(), obj.GetNamespace(), 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) { + 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), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false) + 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 + } + + // 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), v1beta1.Delete, "", obj.GetNamespace(), false, false) + + // 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), v1beta1.Update, obj.GetName(), obj.GetNamespace(), 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), v1beta1.Update, obj.GetName(), obj.GetNamespace(), 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), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false) + 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 + } + + // then run the final delete and make sure admission is called again + c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, false) + 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) + + // 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), v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false) + + 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 + } +} + +// +// utility methods +// + +func newWebhookHandler(t *testing.T, holder *holder, phase string) 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 + } + holder.record(phase, 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 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) { + data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr] + if !ok { + return nil, fmt.Errorf("no stub data for %#v", gvr) + } + + stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}} + if err := json.Unmarshal([]byte(data.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} +} + +func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool { + if !sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection") { + return false + } + return !excludedResources[gvr].Has("*") +} + +func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool { + if !sets.NewString(resource.Verbs...).Has(verb) { + return false + } + return !excludedResources[gvr].Has(verb) +} + +// +// webhook registration helpers +// + +func createV1beta1ValidationWebhook(client clientset.Interface, endpoint string) error { + fail := admissionv1beta1.Fail + // Attaching Admission webhook to API server + _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&admissionv1beta1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"}, + Webhooks: []admissionv1beta1.Webhook{{ + 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"}, + }}, + }) + return err +} + +func createV1beta1MutationWebhook(client clientset.Interface, endpoint string) error { + fail := admissionv1beta1.Fail + // Attaching Mutation webhook to API server + _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"}, + Webhooks: []admissionv1beta1.Webhook{{ + 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"}, + }}, + }) + 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-----`) diff --git a/test/integration/apiserver/admissionwebhook/main_test.go b/test/integration/apiserver/admissionwebhook/main_test.go new file mode 100644 index 0000000000..b5e38e0f6d --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/main_test.go @@ -0,0 +1,27 @@ +/* +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 ( + "testing" + + "k8s.io/kubernetes/test/integration/framework" +) + +func TestMain(m *testing.M) { + framework.EtcdMain(m.Run) +} diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index a131eb3800..918aa85ede 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -28,35 +28,43 @@ import ( // It is exported so that it can be reused across multiple tests. // It returns a new map on every invocation to prevent different tests from mutating shared state. func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { + return GetEtcdStorageDataForNamespace("etcdstoragepathtestnamespace") +} + +// GetEtcdStorageDataForNamespace returns etcd data for all persisted objects. +// It is exported so that it can be reused across multiple tests. +// It returns a new map on every invocation to prevent different tests from mutating shared state. +// Namespaced objects keys are computed for the specified namespace. +func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionResource]StorageData { etcdStorageData := map[schema.GroupVersionResource]StorageData{ // k8s.io/kubernetes/pkg/api/v1 gvr("", "v1", "configmaps"): { Stub: `{"data": {"foo": "bar"}, "metadata": {"name": "cm1"}}`, - ExpectedEtcdPath: "/registry/configmaps/etcdstoragepathtestnamespace/cm1", + ExpectedEtcdPath: "/registry/configmaps/" + namespace + "/cm1", }, gvr("", "v1", "services"): { Stub: `{"metadata": {"name": "service1"}, "spec": {"externalName": "service1name", "ports": [{"port": 10000, "targetPort": 11000}], "selector": {"test": "data"}}}`, - ExpectedEtcdPath: "/registry/services/specs/etcdstoragepathtestnamespace/service1", + ExpectedEtcdPath: "/registry/services/specs/" + namespace + "/service1", }, gvr("", "v1", "podtemplates"): { Stub: `{"metadata": {"name": "pt1name"}, "template": {"metadata": {"labels": {"pt": "01"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container9"}]}}}`, - ExpectedEtcdPath: "/registry/podtemplates/etcdstoragepathtestnamespace/pt1name", + ExpectedEtcdPath: "/registry/podtemplates/" + namespace + "/pt1name", }, gvr("", "v1", "pods"): { Stub: `{"metadata": {"name": "pod1"}, "spec": {"containers": [{"image": "fedora:latest", "name": "container7", "resources": {"limits": {"cpu": "1M"}, "requests": {"cpu": "1M"}}}]}}`, - ExpectedEtcdPath: "/registry/pods/etcdstoragepathtestnamespace/pod1", + ExpectedEtcdPath: "/registry/pods/" + namespace + "/pod1", }, gvr("", "v1", "endpoints"): { Stub: `{"metadata": {"name": "ep1name"}, "subsets": [{"addresses": [{"hostname": "bar-001", "ip": "192.168.3.1"}], "ports": [{"port": 8000}]}]}`, - ExpectedEtcdPath: "/registry/services/endpoints/etcdstoragepathtestnamespace/ep1name", + ExpectedEtcdPath: "/registry/services/endpoints/" + namespace + "/ep1name", }, gvr("", "v1", "resourcequotas"): { Stub: `{"metadata": {"name": "rq1name"}, "spec": {"hard": {"cpu": "5M"}}}`, - ExpectedEtcdPath: "/registry/resourcequotas/etcdstoragepathtestnamespace/rq1name", + ExpectedEtcdPath: "/registry/resourcequotas/" + namespace + "/rq1name", }, gvr("", "v1", "limitranges"): { Stub: `{"metadata": {"name": "lr1name"}, "spec": {"limits": [{"type": "Pod"}]}}`, - ExpectedEtcdPath: "/registry/limitranges/etcdstoragepathtestnamespace/lr1name", + ExpectedEtcdPath: "/registry/limitranges/" + namespace + "/lr1name", }, gvr("", "v1", "namespaces"): { Stub: `{"metadata": {"name": "namespace1"}, "spec": {"finalizers": ["kubernetes"]}}`, @@ -71,41 +79,41 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { ExpectedEtcdPath: "/registry/persistentvolumes/pv1name", }, gvr("", "v1", "events"): { - Stub: `{"involvedObject": {"namespace": "etcdstoragepathtestnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`, - ExpectedEtcdPath: "/registry/events/etcdstoragepathtestnamespace/event1", + Stub: `{"involvedObject": {"namespace": "` + namespace + `"}, "message": "some data here", "metadata": {"name": "event1"}}`, + ExpectedEtcdPath: "/registry/events/" + namespace + "/event1", }, gvr("", "v1", "persistentvolumeclaims"): { Stub: `{"metadata": {"name": "pvc1"}, "spec": {"accessModes": ["ReadWriteOnce"], "resources": {"limits": {"storage": "1M"}, "requests": {"storage": "2M"}}, "selector": {"matchLabels": {"pvc": "stuff"}}}}`, - ExpectedEtcdPath: "/registry/persistentvolumeclaims/etcdstoragepathtestnamespace/pvc1", + ExpectedEtcdPath: "/registry/persistentvolumeclaims/" + namespace + "/pvc1", }, gvr("", "v1", "serviceaccounts"): { Stub: `{"metadata": {"name": "sa1name"}, "secrets": [{"name": "secret00"}]}`, - ExpectedEtcdPath: "/registry/serviceaccounts/etcdstoragepathtestnamespace/sa1name", + ExpectedEtcdPath: "/registry/serviceaccounts/" + namespace + "/sa1name", }, gvr("", "v1", "secrets"): { Stub: `{"data": {"key": "ZGF0YSBmaWxl"}, "metadata": {"name": "secret1"}}`, - ExpectedEtcdPath: "/registry/secrets/etcdstoragepathtestnamespace/secret1", + ExpectedEtcdPath: "/registry/secrets/" + namespace + "/secret1", }, gvr("", "v1", "replicationcontrollers"): { Stub: `{"metadata": {"name": "rc1"}, "spec": {"selector": {"new": "stuff"}, "template": {"metadata": {"labels": {"new": "stuff"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container8"}]}}}}`, - ExpectedEtcdPath: "/registry/controllers/etcdstoragepathtestnamespace/rc1", + ExpectedEtcdPath: "/registry/controllers/" + namespace + "/rc1", }, // -- // k8s.io/kubernetes/pkg/apis/apps/v1beta1 gvr("apps", "v1beta1", "statefulsets"): { Stub: `{"metadata": {"name": "ss1"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`, - ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss1", + ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss1", ExpectedGVK: gvkP("apps", "v1", "StatefulSet"), }, gvr("apps", "v1beta1", "deployments"): { Stub: `{"metadata": {"name": "deployment2"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment2", + ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment2", ExpectedGVK: gvkP("apps", "v1", "Deployment"), }, gvr("apps", "v1beta1", "controllerrevisions"): { Stub: `{"metadata":{"name":"crs1"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`, - ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs1", + ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs1", ExpectedGVK: gvkP("apps", "v1", "ControllerRevision"), }, // -- @@ -113,27 +121,27 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/apps/v1beta2 gvr("apps", "v1beta2", "statefulsets"): { Stub: `{"metadata": {"name": "ss2"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`, - ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss2", + ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss2", ExpectedGVK: gvkP("apps", "v1", "StatefulSet"), }, gvr("apps", "v1beta2", "deployments"): { Stub: `{"metadata": {"name": "deployment3"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment3", + ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment3", ExpectedGVK: gvkP("apps", "v1", "Deployment"), }, gvr("apps", "v1beta2", "daemonsets"): { Stub: `{"metadata": {"name": "ds5"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds5", + ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds5", ExpectedGVK: gvkP("apps", "v1", "DaemonSet"), }, gvr("apps", "v1beta2", "replicasets"): { Stub: `{"metadata": {"name": "rs2"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`, - ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs2", + ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs2", ExpectedGVK: gvkP("apps", "v1", "ReplicaSet"), }, gvr("apps", "v1beta2", "controllerrevisions"): { Stub: `{"metadata":{"name":"crs2"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`, - ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs2", + ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs2", ExpectedGVK: gvkP("apps", "v1", "ControllerRevision"), }, // -- @@ -141,37 +149,37 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/apps/v1 gvr("apps", "v1", "daemonsets"): { Stub: `{"metadata": {"name": "ds6"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds6", + ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds6", }, gvr("apps", "v1", "deployments"): { Stub: `{"metadata": {"name": "deployment4"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment4", + ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment4", }, gvr("apps", "v1", "statefulsets"): { Stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`, - ExpectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss3", + ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss3", }, gvr("apps", "v1", "replicasets"): { Stub: `{"metadata": {"name": "rs3"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`, - ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs3", + ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs3", }, gvr("apps", "v1", "controllerrevisions"): { Stub: `{"metadata":{"name":"crs3"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`, - ExpectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs3", + ExpectedEtcdPath: "/registry/controllerrevisions/" + namespace + "/crs3", }, // -- // k8s.io/kubernetes/pkg/apis/autoscaling/v1 gvr("autoscaling", "v1", "horizontalpodautoscalers"): { Stub: `{"metadata": {"name": "hpa2"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`, - ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa2", + ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa2", }, // -- // k8s.io/kubernetes/pkg/apis/autoscaling/v2beta1 gvr("autoscaling", "v2beta1", "horizontalpodautoscalers"): { Stub: `{"metadata": {"name": "hpa1"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`, - ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa1", + ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa1", ExpectedGVK: gvkP("autoscaling", "v1", "HorizontalPodAutoscaler"), }, // -- @@ -179,7 +187,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/autoscaling/v2beta2 gvr("autoscaling", "v2beta2", "horizontalpodautoscalers"): { Stub: `{"metadata": {"name": "hpa3"}, "spec": {"maxReplicas": 3, "scaleTargetRef": {"kind": "something", "name": "cross"}}}`, - ExpectedEtcdPath: "/registry/horizontalpodautoscalers/etcdstoragepathtestnamespace/hpa3", + ExpectedEtcdPath: "/registry/horizontalpodautoscalers/" + namespace + "/hpa3", ExpectedGVK: gvkP("autoscaling", "v1", "HorizontalPodAutoscaler"), }, // -- @@ -187,21 +195,21 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/batch/v1 gvr("batch", "v1", "jobs"): { Stub: `{"metadata": {"name": "job1"}, "spec": {"manualSelector": true, "selector": {"matchLabels": {"controller-uid": "uid1"}}, "template": {"metadata": {"labels": {"controller-uid": "uid1"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container1"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}`, - ExpectedEtcdPath: "/registry/jobs/etcdstoragepathtestnamespace/job1", + ExpectedEtcdPath: "/registry/jobs/" + namespace + "/job1", }, // -- // k8s.io/kubernetes/pkg/apis/batch/v1beta1 gvr("batch", "v1beta1", "cronjobs"): { Stub: `{"metadata": {"name": "cjv1beta1"}, "spec": {"jobTemplate": {"spec": {"template": {"metadata": {"labels": {"controller-uid": "uid0"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container0"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}, "schedule": "* * * * *"}}`, - ExpectedEtcdPath: "/registry/cronjobs/etcdstoragepathtestnamespace/cjv1beta1", + ExpectedEtcdPath: "/registry/cronjobs/" + namespace + "/cjv1beta1", }, // -- // k8s.io/kubernetes/pkg/apis/batch/v2alpha1 gvr("batch", "v2alpha1", "cronjobs"): { Stub: `{"metadata": {"name": "cjv2alpha1"}, "spec": {"jobTemplate": {"spec": {"template": {"metadata": {"labels": {"controller-uid": "uid0"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container0"}], "dnsPolicy": "ClusterFirst", "restartPolicy": "Never"}}}}, "schedule": "* * * * *"}}`, - ExpectedEtcdPath: "/registry/cronjobs/etcdstoragepathtestnamespace/cjv2alpha1", + ExpectedEtcdPath: "/registry/cronjobs/" + namespace + "/cjv2alpha1", ExpectedGVK: gvkP("batch", "v1beta1", "CronJob"), }, // -- @@ -216,7 +224,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/coordination/v1 gvr("coordination.k8s.io", "v1", "leases"): { Stub: `{"metadata": {"name": "leasev1"}, "spec": {"holderIdentity": "holder", "leaseDurationSeconds": 5}}`, - ExpectedEtcdPath: "/registry/leases/etcdstoragepathtestnamespace/leasev1", + ExpectedEtcdPath: "/registry/leases/" + namespace + "/leasev1", ExpectedGVK: gvkP("coordination.k8s.io", "v1beta1", "Lease"), }, // -- @@ -224,14 +232,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/coordination/v1beta1 gvr("coordination.k8s.io", "v1beta1", "leases"): { Stub: `{"metadata": {"name": "leasev1beta1"}, "spec": {"holderIdentity": "holder", "leaseDurationSeconds": 5}}`, - ExpectedEtcdPath: "/registry/leases/etcdstoragepathtestnamespace/leasev1beta1", + ExpectedEtcdPath: "/registry/leases/" + namespace + "/leasev1beta1", }, // -- // k8s.io/kubernetes/pkg/apis/events/v1beta1 gvr("events.k8s.io", "v1beta1", "events"): { - Stub: `{"metadata": {"name": "event2"}, "regarding": {"namespace": "etcdstoragepathtestnamespace"}, "note": "some data here", "eventTime": "2017-08-09T15:04:05.000000Z", "reportingInstance": "node-xyz", "reportingController": "k8s.io/my-controller", "action": "DidNothing", "reason": "Laziness"}`, - ExpectedEtcdPath: "/registry/events/etcdstoragepathtestnamespace/event2", + Stub: `{"metadata": {"name": "event2"}, "regarding": {"namespace": "` + namespace + `"}, "note": "some data here", "eventTime": "2017-08-09T15:04:05.000000Z", "reportingInstance": "node-xyz", "reportingController": "k8s.io/my-controller", "action": "DidNothing", "reason": "Laziness"}`, + ExpectedEtcdPath: "/registry/events/" + namespace + "/event2", ExpectedGVK: gvkP("", "v1", "Event"), }, // -- @@ -239,7 +247,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/extensions/v1beta1 gvr("extensions", "v1beta1", "daemonsets"): { Stub: `{"metadata": {"name": "ds1"}, "spec": {"selector": {"matchLabels": {"u": "t"}}, "template": {"metadata": {"labels": {"u": "t"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container5"}]}}}}`, - ExpectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds1", + ExpectedEtcdPath: "/registry/daemonsets/" + namespace + "/ds1", ExpectedGVK: gvkP("apps", "v1", "DaemonSet"), }, gvr("extensions", "v1beta1", "podsecuritypolicies"): { @@ -249,21 +257,21 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, gvr("extensions", "v1beta1", "ingresses"): { Stub: `{"metadata": {"name": "ingress1"}, "spec": {"backend": {"serviceName": "service", "servicePort": 5000}}}`, - ExpectedEtcdPath: "/registry/ingress/etcdstoragepathtestnamespace/ingress1", + ExpectedEtcdPath: "/registry/ingress/" + namespace + "/ingress1", }, gvr("extensions", "v1beta1", "networkpolicies"): { Stub: `{"metadata": {"name": "np1"}, "spec": {"podSelector": {"matchLabels": {"e": "f"}}}}`, - ExpectedEtcdPath: "/registry/networkpolicies/etcdstoragepathtestnamespace/np1", + ExpectedEtcdPath: "/registry/networkpolicies/" + namespace + "/np1", ExpectedGVK: gvkP("networking.k8s.io", "v1", "NetworkPolicy"), }, gvr("extensions", "v1beta1", "deployments"): { Stub: `{"metadata": {"name": "deployment1"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, - ExpectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment1", + ExpectedEtcdPath: "/registry/deployments/" + namespace + "/deployment1", ExpectedGVK: gvkP("apps", "v1", "Deployment"), }, gvr("extensions", "v1beta1", "replicasets"): { Stub: `{"metadata": {"name": "rs1"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`, - ExpectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs1", + ExpectedEtcdPath: "/registry/replicasets/" + namespace + "/rs1", ExpectedGVK: gvkP("apps", "v1", "ReplicaSet"), }, // -- @@ -271,7 +279,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/networking/v1beta1 gvr("networking.k8s.io", "v1beta1", "ingresses"): { Stub: `{"metadata": {"name": "ingress2"}, "spec": {"backend": {"serviceName": "service", "servicePort": 5000}}}`, - ExpectedEtcdPath: "/registry/ingress/etcdstoragepathtestnamespace/ingress2", + ExpectedEtcdPath: "/registry/ingress/" + namespace + "/ingress2", ExpectedGVK: gvkP("extensions", "v1beta1", "Ingress"), }, // -- @@ -279,14 +287,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/networking/v1 gvr("networking.k8s.io", "v1", "networkpolicies"): { Stub: `{"metadata": {"name": "np2"}, "spec": {"podSelector": {"matchLabels": {"e": "f"}}}}`, - ExpectedEtcdPath: "/registry/networkpolicies/etcdstoragepathtestnamespace/np2", + ExpectedEtcdPath: "/registry/networkpolicies/" + namespace + "/np2", }, // -- // k8s.io/kubernetes/pkg/apis/policy/v1beta1 gvr("policy", "v1beta1", "poddisruptionbudgets"): { Stub: `{"metadata": {"name": "pdb1"}, "spec": {"selector": {"matchLabels": {"anokkey": "anokvalue"}}}}`, - ExpectedEtcdPath: "/registry/poddisruptionbudgets/etcdstoragepathtestnamespace/pdb1", + ExpectedEtcdPath: "/registry/poddisruptionbudgets/" + namespace + "/pdb1", }, gvr("policy", "v1beta1", "podsecuritypolicies"): { Stub: `{"metadata": {"name": "psp2"}, "spec": {"fsGroup": {"rule": "RunAsAny"}, "privileged": true, "runAsUser": {"rule": "RunAsAny"}, "seLinux": {"rule": "MustRunAs"}, "supplementalGroups": {"rule": "RunAsAny"}}}`, @@ -335,14 +343,14 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/settings/v1alpha1 gvr("settings.k8s.io", "v1alpha1", "podpresets"): { Stub: `{"metadata": {"name": "podpre1"}, "spec": {"env": [{"name": "FOO"}]}}`, - ExpectedEtcdPath: "/registry/podpresets/etcdstoragepathtestnamespace/podpre1", + ExpectedEtcdPath: "/registry/podpresets/" + namespace + "/podpre1", }, // -- // k8s.io/kubernetes/pkg/apis/rbac/v1alpha1 gvr("rbac.authorization.k8s.io", "v1alpha1", "roles"): { Stub: `{"metadata": {"name": "role1"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`, - ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role1", + ExpectedEtcdPath: "/registry/roles/" + namespace + "/role1", ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "Role"), }, gvr("rbac.authorization.k8s.io", "v1alpha1", "clusterroles"): { @@ -352,7 +360,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, gvr("rbac.authorization.k8s.io", "v1alpha1", "rolebindings"): { Stub: `{"metadata": {"name": "roleb1"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`, - ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb1", + ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb1", ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "RoleBinding"), }, gvr("rbac.authorization.k8s.io", "v1alpha1", "clusterrolebindings"): { @@ -365,7 +373,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/rbac/v1beta1 gvr("rbac.authorization.k8s.io", "v1beta1", "roles"): { Stub: `{"metadata": {"name": "role2"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`, - ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role2", + ExpectedEtcdPath: "/registry/roles/" + namespace + "/role2", ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "Role"), }, gvr("rbac.authorization.k8s.io", "v1beta1", "clusterroles"): { @@ -375,7 +383,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, gvr("rbac.authorization.k8s.io", "v1beta1", "rolebindings"): { Stub: `{"metadata": {"name": "roleb2"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`, - ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb2", + ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb2", ExpectedGVK: gvkP("rbac.authorization.k8s.io", "v1", "RoleBinding"), }, gvr("rbac.authorization.k8s.io", "v1beta1", "clusterrolebindings"): { @@ -388,7 +396,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { // k8s.io/kubernetes/pkg/apis/rbac/v1 gvr("rbac.authorization.k8s.io", "v1", "roles"): { Stub: `{"metadata": {"name": "role3"}, "rules": [{"apiGroups": ["v1"], "resources": ["events"], "verbs": ["watch"]}]}`, - ExpectedEtcdPath: "/registry/roles/etcdstoragepathtestnamespace/role3", + ExpectedEtcdPath: "/registry/roles/" + namespace + "/role3", }, gvr("rbac.authorization.k8s.io", "v1", "clusterroles"): { Stub: `{"metadata": {"name": "crole3"}, "rules": [{"nonResourceURLs": ["/version"], "verbs": ["get"]}]}`, @@ -396,7 +404,7 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, gvr("rbac.authorization.k8s.io", "v1", "rolebindings"): { Stub: `{"metadata": {"name": "roleb3"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`, - ExpectedEtcdPath: "/registry/rolebindings/etcdstoragepathtestnamespace/roleb3", + ExpectedEtcdPath: "/registry/rolebindings/" + namespace + "/roleb3", }, gvr("rbac.authorization.k8s.io", "v1", "clusterrolebindings"): { Stub: `{"metadata": {"name": "croleb3"}, "roleRef": {"apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "somecr"}, "subjects": [{"apiVersion": "rbac.authorization.k8s.io/v1alpha1", "kind": "Group", "name": "system:authenticated"}]}`, @@ -462,18 +470,18 @@ func GetEtcdStorageData() map[schema.GroupVersionResource]StorageData { }, gvr("cr.bar.com", "v1", "foos"): { Stub: `{"kind": "Foo", "apiVersion": "cr.bar.com/v1", "metadata": {"name": "cr1foo"}, "color": "blue"}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper - ExpectedEtcdPath: "/registry/cr.bar.com/foos/etcdstoragepathtestnamespace/cr1foo", + ExpectedEtcdPath: "/registry/cr.bar.com/foos/" + namespace + "/cr1foo", }, gvr("custom.fancy.com", "v2", "pants"): { Stub: `{"kind": "Pant", "apiVersion": "custom.fancy.com/v2", "metadata": {"name": "cr2pant"}, "isFancy": true}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper ExpectedEtcdPath: "/registry/custom.fancy.com/pants/cr2pant", }, gvr("awesome.bears.com", "v1", "pandas"): { - Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v1", "metadata": {"name": "cr3panda"}, "weight": 100}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper + Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v1", "metadata": {"name": "cr3panda"}, "spec":{"replicas": 100}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr3panda", }, gvr("awesome.bears.com", "v3", "pandas"): { - Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v3", "metadata": {"name": "cr4panda"}, "weight": 300}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper + Stub: `{"kind": "Panda", "apiVersion": "awesome.bears.com/v3", "metadata": {"name": "cr4panda"}, "spec":{"replicas": 300}}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda", ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"), }, @@ -601,6 +609,14 @@ func GetCustomResourceDefinitionData() []*apiextensionsv1beta1.CustomResourceDef Plural: "pandas", Kind: "Panda", }, + Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ + Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(), + }, + }, }, }, }