From a291846cd1ba856e32907d62b6e6be2d2f463dc9 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Sun, 28 Aug 2016 10:20:44 -0700 Subject: [PATCH] Revert "Remove deprecated Namespace admission plug-ins" --- cluster/centos/config-default.sh | 2 +- cluster/centos/master/scripts/apiserver.sh | 2 +- cmd/kube-apiserver/app/plugins.go | 2 + .../namespace/autoprovision/admission.go | 100 ++++++++++ .../namespace/autoprovision/admission_test.go | 172 ++++++++++++++++++ .../admission/namespace/exists/admission.go | 107 +++++++++++ .../namespace/exists/admission_test.go | 118 ++++++++++++ 7 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 plugin/pkg/admission/namespace/autoprovision/admission.go create mode 100644 plugin/pkg/admission/namespace/autoprovision/admission_test.go create mode 100644 plugin/pkg/admission/namespace/exists/admission.go create mode 100644 plugin/pkg/admission/namespace/exists/admission_test.go diff --git a/cluster/centos/config-default.sh b/cluster/centos/config-default.sh index 615de5d31bf..2fd596e2e6f 100755 --- a/cluster/centos/config-default.sh +++ b/cluster/centos/config-default.sh @@ -42,7 +42,7 @@ export FLANNEL_NET=${FLANNEL_NET:-"172.16.0.0/16"} # Admission Controllers to invoke prior to persisting objects in cluster # If we included ResourceQuota, we should keep it at the end of the list to prevent incremeting quota usage prematurely. -export ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,DefaultStorageClass,ResourceQuota +export ADMISSION_CONTROL=NamespaceLifecycle,NamespaceExists,LimitRanger,ServiceAccount,SecurityContextDeny,DefaultStorageClass,ResourceQuota # Extra options to set on the Docker command line. # This is useful for setting --insecure-registry for local registries. diff --git a/cluster/centos/master/scripts/apiserver.sh b/cluster/centos/master/scripts/apiserver.sh index e3fe5d45719..29bcc985bbf 100755 --- a/cluster/centos/master/scripts/apiserver.sh +++ b/cluster/centos/master/scripts/apiserver.sh @@ -54,7 +54,7 @@ KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=${SERVICE_CLUSTER_IP_RANGE}" # --admission-control="AlwaysAdmit": Ordered list of plug-ins # to do admission control of resources into cluster. # Comma-delimited list of: -# LimitRanger, AlwaysDeny, SecurityContextDeny, +# LimitRanger, AlwaysDeny, SecurityContextDeny, NamespaceExists, # NamespaceLifecycle, NamespaceAutoProvision, # AlwaysAdmit, ServiceAccount, ResourceQuota, DefaultStorageClass KUBE_ADMISSION_CONTROL="--admission-control=${ADMISSION_CONTROL}" diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index 9721d1db527..c685428fbee 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -32,6 +32,8 @@ import ( _ "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy" _ "k8s.io/kubernetes/plugin/pkg/admission/initialresources" _ "k8s.io/kubernetes/plugin/pkg/admission/limitranger" + _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision" + _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists" _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label" _ "k8s.io/kubernetes/plugin/pkg/admission/resourcequota" diff --git a/plugin/pkg/admission/namespace/autoprovision/admission.go b/plugin/pkg/admission/namespace/autoprovision/admission.go new file mode 100644 index 00000000000..4b06f63df19 --- /dev/null +++ b/plugin/pkg/admission/namespace/autoprovision/admission.go @@ -0,0 +1,100 @@ +/* +Copyright 2014 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 autoprovision + +import ( + "io" + + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + + "fmt" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/controller/framework" + "k8s.io/kubernetes/pkg/controller/framework/informers" +) + +func init() { + admission.RegisterPlugin("NamespaceAutoProvision", func(client clientset.Interface, config io.Reader) (admission.Interface, error) { + return NewProvision(client), nil + }) +} + +// provision is an implementation of admission.Interface. +// It looks at all incoming requests in a namespace context, and if the namespace does not exist, it creates one. +// It is useful in deployments that do not want to restrict creation of a namespace prior to its usage. +type provision struct { + *admission.Handler + client clientset.Interface + namespaceInformer framework.SharedIndexInformer +} + +var _ = admission.WantsInformerFactory(&provision{}) + +func (p *provision) Admit(a admission.Attributes) (err error) { + // if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do + // if we're here, then the API server has found a route, which means that if we have a non-empty namespace + // its a namespaced resource. + if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") { + return nil + } + // we need to wait for our caches to warm + if !p.WaitForReady() { + return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := p.namespaceInformer.GetStore().Get(namespace) + if err != nil { + return admission.NewForbidden(a, err) + } + if exists { + return nil + } + _, err = p.client.Core().Namespaces().Create(namespace) + if err != nil && !errors.IsAlreadyExists(err) { + return admission.NewForbidden(a, err) + } + return nil +} + +// NewProvision creates a new namespace provision admission control handler +func NewProvision(c clientset.Interface) admission.Interface { + return &provision{ + Handler: admission.NewHandler(admission.Create), + client: c, + } +} + +func (p *provision) SetInformerFactory(f informers.SharedInformerFactory) { + p.namespaceInformer = f.Namespaces().Informer() + p.SetReadyFunc(p.namespaceInformer.HasSynced) +} + +func (p *provision) Validate() error { + if p.namespaceInformer == nil { + return fmt.Errorf("missing namespaceInformer") + } + return nil +} diff --git a/plugin/pkg/admission/namespace/autoprovision/admission_test.go b/plugin/pkg/admission/namespace/autoprovision/admission_test.go new file mode 100644 index 00000000000..3b96b77b30a --- /dev/null +++ b/plugin/pkg/admission/namespace/autoprovision/admission_test.go @@ -0,0 +1,172 @@ +/* +Copyright 2014 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 autoprovision + +import ( + "fmt" + "testing" + "time" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + "k8s.io/kubernetes/pkg/client/testing/core" + "k8s.io/kubernetes/pkg/controller/framework/informers" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/wait" +) + +// newHandlerForTest returns the admission controller configured for testing. +func newHandlerForTest(c clientset.Interface) (admission.Interface, informers.SharedInformerFactory, error) { + f := informers.NewSharedInformerFactory(c, 5*time.Minute) + handler := NewProvision(c) + plugins := []admission.Interface{handler} + pluginInitializer := admission.NewPluginInitializer(f) + pluginInitializer.Initialize(plugins) + err := admission.Validate(plugins) + return handler, f, err +} + +// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces. +func newMockClientForTest(namespaces []string) *fake.Clientset { + mockClient := &fake.Clientset{} + mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) { + namespaceList := &api.NamespaceList{ + ListMeta: unversioned.ListMeta{ + ResourceVersion: fmt.Sprintf("%d", len(namespaces)), + }, + } + for i, ns := range namespaces { + namespaceList.Items = append(namespaceList.Items, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: ns, + ResourceVersion: fmt.Sprintf("%d", i), + }, + }) + } + return true, namespaceList, nil + }) + return mockClient +} + +// newPod returns a new pod for the specified namespace +func newPod(namespace string) api.Pod { + return api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } +} + +// hasCreateNamespaceAction returns true if it has the create namespace action +func hasCreateNamespaceAction(mockClient *fake.Clientset) bool { + for _, action := range mockClient.Actions() { + if action.GetVerb() == "create" && action.GetResource().Resource == "namespaces" { + return true + } + } + return false +} + +// TestAdmission verifies a namespace is created on create requests for namespace managed resources +func TestAdmission(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{}) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + + pod := newPod(namespace) + err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) + if err != nil { + t.Errorf("unexpected error returned from admission handler") + } + if !hasCreateNamespaceAction(mockClient) { + t.Errorf("expected create namespace action") + } +} + +// TestAdmissionNamespaceExists verifies that no client call is made when a namespace already exists +func TestAdmissionNamespaceExists(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{namespace}) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + + pod := newPod(namespace) + err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) + if err != nil { + t.Errorf("unexpected error returned from admission handler") + } + if hasCreateNamespaceAction(mockClient) { + t.Errorf("unexpected create namespace action") + } +} + +// TestIgnoreAdmission validates that a request is ignored if its not a create +func TestIgnoreAdmission(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{}) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + chainHandler := admission.NewChainHandler(handler) + + pod := newPod(namespace) + err = chainHandler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, nil)) + if err != nil { + t.Errorf("unexpected error returned from admission handler") + } + if hasCreateNamespaceAction(mockClient) { + t.Errorf("unexpected create namespace action") + } +} + +func TestAdmissionWithLatentCache(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{}) + mockClient.AddReactor("create", "namespaces", func(action core.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewAlreadyExists(api.Resource("namespaces"), namespace) + }) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + + pod := newPod(namespace) + err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) + if err != nil { + t.Errorf("unexpected error returned from admission handler") + } + + if !hasCreateNamespaceAction(mockClient) { + t.Errorf("expected create namespace action") + } +} diff --git a/plugin/pkg/admission/namespace/exists/admission.go b/plugin/pkg/admission/namespace/exists/admission.go new file mode 100644 index 00000000000..e20e3b4ea79 --- /dev/null +++ b/plugin/pkg/admission/namespace/exists/admission.go @@ -0,0 +1,107 @@ +/* +Copyright 2014 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 exists + +import ( + "io" + + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + + "fmt" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/controller/framework" + "k8s.io/kubernetes/pkg/controller/framework/informers" +) + +func init() { + admission.RegisterPlugin("NamespaceExists", func(client clientset.Interface, config io.Reader) (admission.Interface, error) { + return NewExists(client), nil + }) +} + +// exists is an implementation of admission.Interface. +// It rejects all incoming requests in a namespace context if the namespace does not exist. +// It is useful in deployments that want to enforce pre-declaration of a Namespace resource. +type exists struct { + *admission.Handler + client clientset.Interface + namespaceInformer framework.SharedIndexInformer +} + +var _ = admission.WantsInformerFactory(&exists{}) + +func (e *exists) Admit(a admission.Attributes) (err error) { + // if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do + // if we're here, then the API server has found a route, which means that if we have a non-empty namespace + // its a namespaced resource. + if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") { + return nil + } + + // we need to wait for our caches to warm + if !e.WaitForReady() { + return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := e.namespaceInformer.GetStore().Get(namespace) + if err != nil { + return errors.NewInternalError(err) + } + if exists { + return nil + } + + // in case of latency in our caches, make a call direct to storage to verify that it truly exists or not + _, err = e.client.Core().Namespaces().Get(a.GetNamespace()) + if err != nil { + if errors.IsNotFound(err) { + return err + } + return errors.NewInternalError(err) + } + + return nil +} + +// NewExists creates a new namespace exists admission control handler +func NewExists(c clientset.Interface) admission.Interface { + return &exists{ + client: c, + Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), + } +} + +func (e *exists) SetInformerFactory(f informers.SharedInformerFactory) { + e.namespaceInformer = f.Namespaces().Informer() + e.SetReadyFunc(e.namespaceInformer.HasSynced) +} + +func (e *exists) Validate() error { + if e.namespaceInformer == nil { + return fmt.Errorf("missing namespaceInformer") + } + return nil +} diff --git a/plugin/pkg/admission/namespace/exists/admission_test.go b/plugin/pkg/admission/namespace/exists/admission_test.go new file mode 100644 index 00000000000..f2b832b65fc --- /dev/null +++ b/plugin/pkg/admission/namespace/exists/admission_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2014 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 exists + +import ( + "fmt" + "testing" + "time" + + "k8s.io/kubernetes/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + "k8s.io/kubernetes/pkg/client/testing/core" + "k8s.io/kubernetes/pkg/controller/framework/informers" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/wait" +) + +// newHandlerForTest returns the admission controller configured for testing. +func newHandlerForTest(c clientset.Interface) (admission.Interface, informers.SharedInformerFactory, error) { + f := informers.NewSharedInformerFactory(c, 5*time.Minute) + handler := NewExists(c) + plugins := []admission.Interface{handler} + pluginInitializer := admission.NewPluginInitializer(f) + pluginInitializer.Initialize(plugins) + err := admission.Validate(plugins) + return handler, f, err +} + +// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces. +func newMockClientForTest(namespaces []string) *fake.Clientset { + mockClient := &fake.Clientset{} + mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) { + namespaceList := &api.NamespaceList{ + ListMeta: unversioned.ListMeta{ + ResourceVersion: fmt.Sprintf("%d", len(namespaces)), + }, + } + for i, ns := range namespaces { + namespaceList.Items = append(namespaceList.Items, api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: ns, + ResourceVersion: fmt.Sprintf("%d", i), + }, + }) + } + return true, namespaceList, nil + }) + return mockClient +} + +// newPod returns a new pod for the specified namespace +func newPod(namespace string) api.Pod { + return api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } +} + +// TestAdmissionNamespaceExists verifies pod is admitted only if namespace exists. +func TestAdmissionNamespaceExists(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{namespace}) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + + pod := newPod(namespace) + err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) + if err != nil { + t.Errorf("unexpected error returned from admission handler") + } +} + +// TestAdmissionNamespaceDoesNotExist verifies pod is not admitted if namespace does not exist. +func TestAdmissionNamespaceDoesNotExist(t *testing.T) { + namespace := "test" + mockClient := newMockClientForTest([]string{}) + mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("nope, out of luck") + }) + handler, informerFactory, err := newHandlerForTest(mockClient) + if err != nil { + t.Errorf("unexpected error initializing handler: %v", err) + } + informerFactory.Start(wait.NeverStop) + + pod := newPod(namespace) + err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) + if err == nil { + actions := "" + for _, action := range mockClient.Actions() { + actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", " + } + t.Errorf("expected error returned from admission handler: %v", actions) + } +}