From 8aa807bc33352d8152e89a05f5877afdc6bbec6f Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Tue, 10 Mar 2015 14:28:12 -0400 Subject: [PATCH] NamespaceLifecycle admission control plugin --- cmd/kube-apiserver/app/plugins.go | 1 + .../namespace/lifecycle/admission.go | 109 ++++++++++++++++++ .../namespace/lifecycle/admission_test.go | 80 +++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 plugin/pkg/admission/namespace/lifecycle/admission.go create mode 100644 plugin/pkg/admission/namespace/lifecycle/admission_test.go diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index c2dbe05f90..94d1edf770 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -34,6 +34,7 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/autoprovision" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" ) diff --git a/plugin/pkg/admission/namespace/lifecycle/admission.go b/plugin/pkg/admission/namespace/lifecycle/admission.go new file mode 100644 index 0000000000..e571a50942 --- /dev/null +++ b/plugin/pkg/admission/namespace/lifecycle/admission.go @@ -0,0 +1,109 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +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 lifecycle + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +func init() { + admission.RegisterPlugin("NamespaceLifecycle", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewLifecycle(client), nil + }) +} + +// lifecycle is an implementation of admission.Interface. +// It enforces life-cycle constraints around a Namespace depending on its Phase +type lifecycle struct { + client client.Interface + store cache.Store +} + +func (l *lifecycle) Admit(a admission.Attributes) (err error) { + if a.GetOperation() != "CREATE" { + return nil + } + defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) + if err != nil { + return err + } + mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) + if err != nil { + return err + } + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + return nil + } + namespaceObj, exists, err := l.store.Get(&api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + }) + if err != nil { + return err + } + if !exists { + return nil + } + namespace := namespaceObj.(*api.Namespace) + if namespace.Status.Phase != api.NamespaceTerminating { + return nil + } + + name := "Unknown" + obj := a.GetObject() + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s is terminating", a.GetNamespace())) +} + +func NewLifecycle(c client.Interface) admission.Interface { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + reflector := cache.NewReflector( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return c.Namespaces().List(labels.Everything()) + }, + WatchFunc: func(resourceVersion string) (watch.Interface, error) { + return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion) + }, + }, + &api.Namespace{}, + store, + 0, + ) + reflector.Run() + return &lifecycle{ + client: c, + store: store, + } +} diff --git a/plugin/pkg/admission/namespace/lifecycle/admission_test.go b/plugin/pkg/admission/namespace/lifecycle/admission_test.go new file mode 100644 index 0000000000..1b18d2de67 --- /dev/null +++ b/plugin/pkg/admission/namespace/lifecycle/admission_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +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 lifecycle + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" +) + +// TestAdmission +func TestAdmission(t *testing.T) { + namespaceObj := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "test", + Namespace: "", + }, + Status: api.NamespaceStatus{ + Phase: api.NamespaceActive, + }, + } + store := cache.NewStore(cache.MetaNamespaceIndexFunc) + store.Add(namespaceObj) + mockClient := &client.Fake{} + handler := &lifecycle{ + client: mockClient, + store: store, + } + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespaceObj.Namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + err := handler.Admit(admission.NewAttributesRecord(&pod, namespaceObj.Namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler: %v", err) + } + + // change namespace state to terminating + namespaceObj.Status.Phase = api.NamespaceTerminating + store.Add(namespaceObj) + + // verify create operations in the namespace cause an error + err = handler.Admit(admission.NewAttributesRecord(&pod, namespaceObj.Namespace, "pods", "CREATE")) + if err == nil { + t.Errorf("Expected error rejecting creates in a namespace when it is terminating") + } + + // verify update operations in the namespace can proceed + err = handler.Admit(admission.NewAttributesRecord(&pod, namespaceObj.Namespace, "pods", "UPDATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler: %v", err) + } + + // verify delete operations in the namespace can proceed + err = handler.Admit(admission.NewAttributesRecord(nil, namespaceObj.Namespace, "pods", "DELETE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler: %v", err) + } + +}