diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index 2d6d365fcee..6d60773a93b 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -95,3 +95,10 @@ func IsServiceIPSet(service *Service) bool { func IsServiceIPRequested(service *Service) bool { return service.Spec.PortalIP == "" } + +var standardFinalizers = util.NewStringSet( + string(FinalizerKubernetes)) + +func IsStandardFinalizerName(str string) bool { + return standardFinalizers.Has(str) +} diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 3aaa06cdbcc..05645bb44ad 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -198,6 +198,9 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { c.FuzzNoCustom(s) // fuzz self without calling this function again s.Type = api.SecretTypeOpaque }, + func(s *api.NamespaceSpec, c fuzz.Continue) { + s.Finalizers = []api.FinalizerName{api.FinalizerKubernetes} + }, func(s *api.NamespaceStatus, c fuzz.Continue) { s.Phase = api.NamespaceActive }, diff --git a/pkg/api/types.go b/pkg/api/types.go index 759c33586cb..65800e9467b 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -966,8 +966,17 @@ type NodeList struct { // NamespaceSpec describes the attributes on a Namespace type NamespaceSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []FinalizerName } +type FinalizerName string + +// These are internal finalizer values to Kubernetes, must be qualified name unless defined here +const ( + FinalizerKubernetes FinalizerName = "kubernetes" +) + // NamespaceStatus is information about the current status of a Namespace. type NamespaceStatus struct { // Phase is the current lifecycle phase of the namespace. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 1ecceeacc39..796ca0058db 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1426,4 +1426,17 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + func(label, value string) (string, string, error) { + switch label { + case "status.phase": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index b3ad64b0471..21fa47678fd 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -782,8 +782,17 @@ type MinionList struct { Items []Minion `json:"items" description:"list of nodes"` } +type FinalizerName string + +// These are internal finalizer values to Kubernetes, must be qualified name unless defined here +const ( + FinalizerKubernetes FinalizerName = "kubernetes" +) + // NamespaceSpec describes the attributes on a Namespace type NamespaceSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []FinalizerName `json:"finalizers,omitempty" description:"an opaque list of values that must be empty to permanently remove object from storage"` } // NamespaceStatus is information about the current status of a Namespace. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 249b25a6d6f..432af9fe760 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -1354,4 +1354,17 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + func(label, value string) (string, string, error) { + switch label { + case "status.phase": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index f7815297efc..b0974725cee 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -798,8 +798,17 @@ type MinionList struct { Items []Minion `json:"items" description:"list of nodes"` } +type FinalizerName string + +// These are internal finalizer values to Kubernetes, must be qualified name unless defined here +const ( + FinalizerKubernetes FinalizerName = "kubernetes" +) + // NamespaceSpec describes the attributes on a Namespace type NamespaceSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []FinalizerName `json:"finalizers,omitempty" description:"an opaque list of values that must be empty to permanently remove object from storage"` } // NamespaceStatus is information about the current status of a Namespace. diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index 40245f812fb..c40fb780fee 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -60,4 +60,17 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "namespaces", + func(label, value string) (string, string, error) { + switch label { + case "status.phase": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 15e369104af..391c928776b 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -950,8 +950,17 @@ type NodeList struct { Items []Node `json:"items" description:"list of nodes"` } +type FinalizerName string + +// These are internal finalizer values to Kubernetes, must be qualified name unless defined here +const ( + FinalizerKubernetes FinalizerName = "kubernetes" +) + // NamespaceSpec describes the attributes on a Namespace type NamespaceSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []FinalizerName `json:"finalizers,omitempty" description:"an opaque list of values that must be empty to permanently remove object from storage"` } // NamespaceStatus is information about the current status of a Namespace. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index f85b398cc4f..36a3b3d9368 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1011,9 +1011,28 @@ func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.R func ValidateNamespace(namespace *api.Namespace) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMeta(&namespace.ObjectMeta, false, ValidateNamespaceName).Prefix("metadata")...) + for i := range namespace.Spec.Finalizers { + allErrs = append(allErrs, validateFinalizerName(string(namespace.Spec.Finalizers[i]))...) + } return allErrs } +// Validate finalizer names +func validateFinalizerName(stringValue string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if !util.IsQualifiedName(stringValue) { + return append(allErrs, fmt.Errorf("finalizer name: %v, %v", stringValue, qualifiedNameErrorMsg)) + } + + if len(strings.Split(stringValue, "/")) == 1 { + if !api.IsStandardFinalizerName(stringValue) { + return append(allErrs, fmt.Errorf("finalizer name: %v is neither a standard finalizer name nor is it fully qualified", stringValue)) + } + } + + return errs.ValidationErrorList{} +} + // ValidateNamespaceUpdate tests to make sure a mamespace update can be applied. Modifies oldNamespace. func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespace) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -1022,6 +1041,7 @@ func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespa // TODO: move reset function to its own location // Ignore metadata changes now that they have been tested oldNamespace.ObjectMeta = namespace.ObjectMeta + oldNamespace.Spec.Finalizers = namespace.Spec.Finalizers // TODO: Add a 'real' ValidationError type for this error and provide print actual diffs. if !api.Semantic.DeepEqual(oldNamespace, namespace) { @@ -1036,9 +1056,20 @@ func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespa func ValidateNamespaceStatusUpdate(newNamespace, oldNamespace *api.Namespace) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldNamespace.ObjectMeta, &newNamespace.ObjectMeta).Prefix("metadata")...) - if newNamespace.Status.Phase != oldNamespace.Status.Phase { - allErrs = append(allErrs, errs.NewFieldInvalid("status.phase", newNamespace.Status.Phase, "namespace phase cannot be changed directly")) - } newNamespace.Spec = oldNamespace.Spec return allErrs } + +// ValidateNamespaceFinalizeUpdate tests to see if the update is legal for an end user to make. newNamespace is updated with fields +// that cannot be changed. +func ValidateNamespaceFinalizeUpdate(newNamespace, oldNamespace *api.Namespace) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldNamespace.ObjectMeta, &newNamespace.ObjectMeta).Prefix("metadata")...) + for i := range newNamespace.Spec.Finalizers { + allErrs = append(allErrs, validateFinalizerName(string(newNamespace.Spec.Finalizers[i]))...) + } + newNamespace.ObjectMeta = oldNamespace.ObjectMeta + newNamespace.Status = oldNamespace.Status + fmt.Printf("NEW NAMESPACE FINALIZERS : %v\n", newNamespace.Spec.Finalizers) + return allErrs +} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index c656c548a87..4da3973abad 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2206,6 +2206,90 @@ func TestValidateNamespace(t *testing.T) { } } +func TestValidateNamespaceFinalizeUpdate(t *testing.T) { + tests := []struct { + oldNamespace api.Namespace + namespace api.Namespace + valid bool + }{ + {api.Namespace{}, api.Namespace{}, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}}, + api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}, + Spec: api.NamespaceSpec{ + Finalizers: []api.FinalizerName{"Foo"}, + }, + }, false}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}, + Spec: api.NamespaceSpec{ + Finalizers: []api.FinalizerName{"foo.com/bar"}, + }, + }, + api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}, + Spec: api.NamespaceSpec{ + Finalizers: []api.FinalizerName{"foo.com/bar", "what.com/bar"}, + }, + }, true}, + } + for i, test := range tests { + errs := ValidateNamespaceFinalizeUpdate(&test.namespace, &test.oldNamespace) + if test.valid && len(errs) > 0 { + t.Errorf("%d: Unexpected error: %v", i, errs) + t.Logf("%#v vs %#v", test.oldNamespace, test.namespace) + } + if !test.valid && len(errs) == 0 { + t.Errorf("%d: Unexpected non-error", i) + } + } +} + +func TestValidateNamespaceStatusUpdate(t *testing.T) { + tests := []struct { + oldNamespace api.Namespace + namespace api.Namespace + valid bool + }{ + {api.Namespace{}, api.Namespace{}, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}}, + api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}, + Status: api.NamespaceStatus{ + Phase: api.NamespaceTerminating, + }, + }, true}, + {api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo"}}, + api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "bar"}, + Status: api.NamespaceStatus{ + Phase: api.NamespaceTerminating, + }, + }, false}, + } + for i, test := range tests { + errs := ValidateNamespaceStatusUpdate(&test.oldNamespace, &test.namespace) + if test.valid && len(errs) > 0 { + t.Errorf("%d: Unexpected error: %v", i, errs) + t.Logf("%#v vs %#v", test.oldNamespace.ObjectMeta, test.namespace.ObjectMeta) + } + if !test.valid && len(errs) == 0 { + t.Errorf("%d: Unexpected non-error", i) + } + } +} + func TestValidateNamespaceUpdate(t *testing.T) { tests := []struct { oldNamespace api.Namespace diff --git a/pkg/client/fake_namespaces.go b/pkg/client/fake_namespaces.go index 197b56767f1..055bd2a2403 100644 --- a/pkg/client/fake_namespaces.go +++ b/pkg/client/fake_namespaces.go @@ -29,7 +29,7 @@ type FakeNamespaces struct { Fake *Fake } -func (c *FakeNamespaces) List(selector labels.Selector) (*api.NamespaceList, error) { +func (c *FakeNamespaces) List(labels labels.Selector, field fields.Selector) (*api.NamespaceList, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-namespaces"}) return api.Scheme.CopyOrDie(&c.Fake.NamespacesList).(*api.NamespaceList), nil } @@ -58,3 +58,13 @@ func (c *FakeNamespaces) Watch(label labels.Selector, field fields.Selector, res c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-namespaces", Value: resourceVersion}) return c.Fake.Watch, nil } + +func (c *FakeNamespaces) Finalize(namespace *api.Namespace) (*api.Namespace, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "finalize-namespace", Value: namespace.Name}) + return &api.Namespace{}, nil +} + +func (c *FakeNamespaces) Status(namespace *api.Namespace) (*api.Namespace, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "status-namespace", Value: namespace.Name}) + return &api.Namespace{}, nil +} diff --git a/pkg/client/namespaces.go b/pkg/client/namespaces.go index 8285f80b7c0..04ad150a3d3 100644 --- a/pkg/client/namespaces.go +++ b/pkg/client/namespaces.go @@ -33,10 +33,12 @@ type NamespacesInterface interface { type NamespaceInterface interface { Create(item *api.Namespace) (*api.Namespace, error) Get(name string) (result *api.Namespace, err error) - List(selector labels.Selector) (*api.NamespaceList, error) + List(label labels.Selector, field fields.Selector) (*api.NamespaceList, error) Delete(name string) error Update(item *api.Namespace) (*api.Namespace, error) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + Finalize(item *api.Namespace) (*api.Namespace, error) + Status(item *api.Namespace) (*api.Namespace, error) } // namespaces implements NamespacesInterface @@ -57,9 +59,13 @@ func (c *namespaces) Create(namespace *api.Namespace) (*api.Namespace, error) { } // List lists all the namespaces in the cluster. -func (c *namespaces) List(selector labels.Selector) (*api.NamespaceList, error) { +func (c *namespaces) List(label labels.Selector, field fields.Selector) (*api.NamespaceList, error) { result := &api.NamespaceList{} - err := c.r.Get().Resource("namespaces").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) + err := c.r.Get(). + Resource("namespaces"). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). + Do().Into(result) return result, err } @@ -74,6 +80,28 @@ func (c *namespaces) Update(namespace *api.Namespace) (result *api.Namespace, er return } +// Finalize takes the representation of a namespace to update. Returns the server's representation of the namespace, and an error, if it occurs. +func (c *namespaces) Finalize(namespace *api.Namespace) (result *api.Namespace, err error) { + result = &api.Namespace{} + if len(namespace.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", namespace) + return + } + err = c.r.Put().Resource("namespaces").Name(namespace.Name).SubResource("finalize").Body(namespace).Do().Into(result) + return +} + +// Status takes the representation of a namespace to update. Returns the server's representation of the namespace, and an error, if it occurs. +func (c *namespaces) Status(namespace *api.Namespace) (result *api.Namespace, err error) { + result = &api.Namespace{} + if len(namespace.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", namespace) + return + } + err = c.r.Put().Resource("namespaces").Name(namespace.Name).SubResource("status").Body(namespace).Do().Into(result) + return +} + // Get gets an existing namespace func (c *namespaces) Get(name string) (*api.Namespace, error) { if len(name) == 0 { diff --git a/pkg/client/namespaces_test.go b/pkg/client/namespaces_test.go index 7777c65581f..01ccd86591b 100644 --- a/pkg/client/namespaces_test.go +++ b/pkg/client/namespaces_test.go @@ -91,7 +91,7 @@ func TestNamespaceList(t *testing.T) { }, Response: Response{StatusCode: 200, Body: namespaceList}, } - response, err := c.Setup().Namespaces().List(labels.Everything()) + response, err := c.Setup().Namespaces().List(labels.Everything(), fields.Everything()) if err != nil { t.Errorf("%#v should be nil.", err) @@ -117,6 +117,9 @@ func TestNamespaceUpdate(t *testing.T) { "name": "baz", }, }, + Spec: api.NamespaceSpec{ + Finalizers: []api.FinalizerName{api.FinalizerKubernetes}, + }, } c := &testClient{ Request: testRequest{Method: "PUT", Path: "/namespaces/foo"}, @@ -126,6 +129,28 @@ func TestNamespaceUpdate(t *testing.T) { c.Validate(t, receivedNamespace, err) } +func TestNamespaceFinalize(t *testing.T) { + requestNamespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + ResourceVersion: "1", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: api.NamespaceSpec{ + Finalizers: []api.FinalizerName{api.FinalizerKubernetes}, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: "/namespaces/foo/finalize"}, + Response: Response{StatusCode: 200, Body: requestNamespace}, + } + receivedNamespace, err := c.Setup().Namespaces().Finalize(requestNamespace) + c.Validate(t, receivedNamespace, err) +} + func TestNamespaceDelete(t *testing.T) { c := &testClient{ Request: testRequest{Method: "DELETE", Path: "/namespaces/foo"}, diff --git a/pkg/master/master.go b/pkg/master/master.go index 432cb55ce33..3633ed3e659 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -358,7 +358,7 @@ func (m *Master) init(c *Config) { resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper) secretRegistry := secret.NewEtcdRegistry(c.EtcdHelper) - namespaceStorage := namespaceetcd.NewStorage(c.EtcdHelper) + namespaceStorage, namespaceStatusStorage, namespaceFinalizeStorage := namespaceetcd.NewStorage(c.EtcdHelper) m.namespaceRegistry = namespace.NewRegistry(namespaceStorage) // TODO: split me up into distinct storage registries @@ -402,6 +402,8 @@ func (m *Master) init(c *Config) { "resourceQuotas": resourceQuotaStorage, "resourceQuotas/status": resourceQuotaStatusStorage, "namespaces": namespaceStorage, + "namespaces/status": namespaceStatusStorage, + "namespaces/finalize": namespaceFinalizeStorage, "secrets": secret.NewStorage(secretRegistry), } diff --git a/pkg/registry/namespace/etcd/etcd.go b/pkg/registry/namespace/etcd/etcd.go index 5931ab82ab8..03c618d064e 100644 --- a/pkg/registry/namespace/etcd/etcd.go +++ b/pkg/registry/namespace/etcd/etcd.go @@ -17,6 +17,8 @@ limitations under the License. package etcd import ( + "fmt" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -25,15 +27,27 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) // rest implements a RESTStorage for namespaces against etcd type REST struct { *etcdgeneric.Etcd + status *etcdgeneric.Etcd +} + +// StatusREST implements the REST endpoint for changing the status of a namespace. +type StatusREST struct { + store *etcdgeneric.Etcd +} + +// FinalizeREST implements the REST endpoint for finalizing a namespace. +type FinalizeREST struct { + store *etcdgeneric.Etcd } // NewStorage returns a RESTStorage object that will work against namespaces -func NewStorage(h tools.EtcdHelper) *REST { +func NewStorage(h tools.EtcdHelper) (*REST, *StatusREST, *FinalizeREST) { store := &etcdgeneric.Etcd{ NewFunc: func() runtime.Object { return &api.Namespace{} }, NewListFunc: func() runtime.Object { return &api.NamespaceList{} }, @@ -56,5 +70,54 @@ func NewStorage(h tools.EtcdHelper) *REST { store.UpdateStrategy = namespace.Strategy store.ReturnDeletedObject = true - return &REST{Etcd: store} + statusStore := *store + statusStore.UpdateStrategy = namespace.StatusStrategy + + finalizeStore := *store + finalizeStore.UpdateStrategy = namespace.FinalizeStrategy + + return &REST{Etcd: store, status: &statusStore}, &StatusREST{store: &statusStore}, &FinalizeREST{store: &finalizeStore} +} + +// Delete enforces life-cycle rules for namespace termination +func (r *REST) Delete(ctx api.Context, name string, options *api.DeleteOptions) (runtime.Object, error) { + nsObj, err := r.Get(ctx, name) + if err != nil { + return nil, err + } + + namespace := nsObj.(*api.Namespace) + + // upon first request to delete, we switch the phase to start namespace termination + if namespace.DeletionTimestamp == nil { + now := util.Now() + namespace.DeletionTimestamp = &now + namespace.Status.Phase = api.NamespaceTerminating + result, _, err := r.status.Update(ctx, namespace) + return result, err + } + + // prior to final deletion, we must ensure that finalizers is empty + if len(namespace.Spec.Finalizers) != 0 { + err = fmt.Errorf("Unable to delete namespace %v because finalizers is not empty %v", namespace.Name, namespace.Spec.Finalizers) + } + return r.Etcd.Delete(ctx, name, nil) +} + +func (r *StatusREST) New() runtime.Object { + return &api.Namespace{} +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) +} + +func (r *FinalizeREST) New() runtime.Object { + return &api.Namespace{} +} + +// Update alters the status finalizers subset of an object. +func (r *FinalizeREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) } diff --git a/pkg/registry/namespace/etcd/etcd_test.go b/pkg/registry/namespace/etcd/etcd_test.go index 4bb44974d03..f3df2d1562e 100644 --- a/pkg/registry/namespace/etcd/etcd_test.go +++ b/pkg/registry/namespace/etcd/etcd_test.go @@ -41,7 +41,7 @@ func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient, tools.EtcdHelper) { fakeEtcdClient, h := newHelper(t) - storage := NewStorage(h) + storage, _, _ := NewStorage(h) return storage, fakeEtcdClient, h } @@ -69,7 +69,7 @@ func TestStorage(t *testing.T) { func TestCreate(t *testing.T) { fakeEtcdClient, helper := newHelper(t) - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) test := resttest.New(t, storage, fakeEtcdClient.SetError) namespace := validNewNamespace() namespace.ObjectMeta = api.ObjectMeta{} @@ -94,7 +94,7 @@ func expectNamespace(t *testing.T, out runtime.Object) (*api.Namespace, bool) { func TestCreateSetsFields(t *testing.T) { fakeEtcdClient, helper := newHelper(t) - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) namespace := validNewNamespace() _, err := storage.Create(api.NewDefaultContext(), namespace) if err != fakeEtcdClient.Err { @@ -124,7 +124,7 @@ func TestListEmptyNamespaceList(t *testing.T) { E: fakeEtcdClient.NewError(tools.EtcdErrorCodeNotFound), } - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) namespaces, err := storage.List(api.NewContext(), labels.Everything(), fields.Everything()) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -157,7 +157,7 @@ func TestListNamespaceList(t *testing.T) { }, }, } - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) namespacesObj, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything()) namespaces := namespacesObj.(*api.NamespaceList) if err != nil { @@ -204,7 +204,7 @@ func TestListNamespaceListSelection(t *testing.T) { }, }, } - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) ctx := api.NewDefaultContext() table := []struct { label, field string @@ -252,9 +252,11 @@ func TestListNamespaceListSelection(t *testing.T) { } func TestNamespaceDecode(t *testing.T) { - storage := NewStorage(tools.EtcdHelper{}) + _, helper := newHelper(t) + storage, _, _ := NewStorage(helper) expected := validNewNamespace() expected.Status.Phase = api.NamespaceActive + expected.Spec.Finalizers = []api.FinalizerName{api.FinalizerKubernetes} body, err := latest.Codec.Encode(expected) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -281,7 +283,7 @@ func TestGet(t *testing.T) { }, }, } - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) obj, err := storage.Get(api.NewContext(), "foo") namespace := obj.(*api.Namespace) if err != nil { @@ -311,8 +313,9 @@ func TestDeleteNamespace(t *testing.T) { }, }, } - storage := NewStorage(helper) + storage, _, _ := NewStorage(helper) _, err := storage.Delete(api.NewDefaultContext(), "foo", nil) + if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/registry/namespace/rest.go b/pkg/registry/namespace/rest.go index e2b9f6dadc0..371524991ed 100644 --- a/pkg/registry/namespace/rest.go +++ b/pkg/registry/namespace/rest.go @@ -45,10 +45,27 @@ func (namespaceStrategy) NamespaceScoped() bool { // ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. func (namespaceStrategy) ResetBeforeCreate(obj runtime.Object) { + // on create, status is active namespace := obj.(*api.Namespace) namespace.Status = api.NamespaceStatus{ Phase: api.NamespaceActive, } + // on create, we require the kubernetes value + // we cannot use this in defaults conversion because we let it get removed over life of object + hasKubeFinalizer := false + for i := range namespace.Spec.Finalizers { + if namespace.Spec.Finalizers[i] == api.FinalizerKubernetes { + hasKubeFinalizer = true + break + } + } + if !hasKubeFinalizer { + if len(namespace.Spec.Finalizers) == 0 { + namespace.Spec.Finalizers = []api.FinalizerName{api.FinalizerKubernetes} + } else { + namespace.Spec.Finalizers = append(namespace.Spec.Finalizers, api.FinalizerKubernetes) + } + } } // Validate validates a new namespace. @@ -74,10 +91,19 @@ type namespaceStatusStrategy struct { var StatusStrategy = namespaceStatusStrategy{Strategy} func (namespaceStatusStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { - // TODO: merge valid fields after update return validation.ValidateNamespaceStatusUpdate(obj.(*api.Namespace), old.(*api.Namespace)) } +type namespaceFinalizeStrategy struct { + namespaceStrategy +} + +var FinalizeStrategy = namespaceFinalizeStrategy{Strategy} + +func (namespaceFinalizeStrategy) ValidateUpdate(obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateNamespaceFinalizeUpdate(obj.(*api.Namespace), old.(*api.Namespace)) +} + // MatchNamespace returns a generic matcher for a given label and field selector. func MatchNamespace(label labels.Selector, field fields.Selector) generic.Matcher { return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { diff --git a/plugin/pkg/admission/namespace/autoprovision/admission.go b/plugin/pkg/admission/namespace/autoprovision/admission.go index 37f1528d316..c54b819265c 100644 --- a/plugin/pkg/admission/namespace/autoprovision/admission.go +++ b/plugin/pkg/admission/namespace/autoprovision/admission.go @@ -87,7 +87,7 @@ func NewProvision(c client.Interface) admission.Interface { reflector := cache.NewReflector( &cache.ListWatch{ ListFunc: func() (runtime.Object, error) { - return c.Namespaces().List(labels.Everything()) + return c.Namespaces().List(labels.Everything(), fields.Everything()) }, WatchFunc: func(resourceVersion string) (watch.Interface, error) { return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion) diff --git a/plugin/pkg/admission/namespace/exists/admission.go b/plugin/pkg/admission/namespace/exists/admission.go index f3d4942dc37..10f6a12d675 100644 --- a/plugin/pkg/admission/namespace/exists/admission.go +++ b/plugin/pkg/admission/namespace/exists/admission.go @@ -86,7 +86,7 @@ func NewExists(c client.Interface) admission.Interface { reflector := cache.NewReflector( &cache.ListWatch{ ListFunc: func() (runtime.Object, error) { - return c.Namespaces().List(labels.Everything()) + return c.Namespaces().List(labels.Everything(), fields.Everything()) }, WatchFunc: func(resourceVersion string) (watch.Interface, error) { return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion) diff --git a/plugin/pkg/admission/namespace/lifecycle/admission.go b/plugin/pkg/admission/namespace/lifecycle/admission.go index e571a509421..23696201533 100644 --- a/plugin/pkg/admission/namespace/lifecycle/admission.go +++ b/plugin/pkg/admission/namespace/lifecycle/admission.go @@ -91,7 +91,7 @@ func NewLifecycle(c client.Interface) admission.Interface { reflector := cache.NewReflector( &cache.ListWatch{ ListFunc: func() (runtime.Object, error) { - return c.Namespaces().List(labels.Everything()) + return c.Namespaces().List(labels.Everything(), fields.Everything()) }, WatchFunc: func(resourceVersion string) (watch.Interface, error) { return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion)