diff --git a/pkg/cmd/server/kubernetes/master.go b/pkg/cmd/server/kubernetes/master.go index e89d01100bc8..1fe35853e413 100644 --- a/pkg/cmd/server/kubernetes/master.go +++ b/pkg/cmd/server/kubernetes/master.go @@ -24,8 +24,7 @@ import ( latestschedulerapi "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/api/latest" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory" - // Namespace controller will be added - _ "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" + "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" ) const ( @@ -83,6 +82,12 @@ func (c *MasterConfig) InstallAPI(container *restful.Container) []string { } } +func (c *MasterConfig) RunNamespaceController() { + namespaceController := namespace.NewNamespaceManager(c.KubeClient) + namespaceController.Run(1 * time.Minute) + glog.Infof("Started Kubernetes Namespace Manager") +} + // RunReplicationController starts the Kubernetes replication controller sync loop func (c *MasterConfig) RunReplicationController() { controllerManager := controller.NewReplicationManager(c.KubeClient) diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index dc36010498d7..7883476b127d 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -75,7 +75,7 @@ import ( clientauthorizationregistry "github.com/openshift/origin/pkg/oauth/registry/clientauthorization" oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd" projectcontroller "github.com/openshift/origin/pkg/project/controller" - projectregistry "github.com/openshift/origin/pkg/project/registry/project" + projectproxy "github.com/openshift/origin/pkg/project/registry/project/proxy" routeallocationcontroller "github.com/openshift/origin/pkg/route/controller/allocation" routeetcd "github.com/openshift/origin/pkg/route/registry/etcd" routeregistry "github.com/openshift/origin/pkg/route/registry/route" @@ -224,7 +224,7 @@ func (c *MasterConfig) InstallProtectedAPI(container *restful.Container) []strin "routes": routeregistry.NewREST(routeEtcd, routeAllocator), - "projects": projectregistry.NewREST(kclient.Namespaces(), c.ProjectAuthorizationCache), + "projects": projectproxy.NewREST(kclient.Namespaces(), c.ProjectAuthorizationCache), "users": userStorage, "identities": identityStorage, diff --git a/pkg/cmd/server/start/start_master.go b/pkg/cmd/server/start/start_master.go index 8f15c047785d..34e07501ea26 100644 --- a/pkg/cmd/server/start/start_master.go +++ b/pkg/cmd/server/start/start_master.go @@ -343,6 +343,7 @@ func StartMaster(openshiftMasterConfig *configapi.MasterConfig) error { kubeConfig.RunEndpointController() kubeConfig.RunMinionController() kubeConfig.RunResourceQuotaManager() + kubeConfig.RunNamespaceController() } else { _, kubeConfig, err := configapi.GetKubeClient(openshiftMasterConfig.MasterClients.KubernetesKubeConfig) diff --git a/pkg/project/api/types.go b/pkg/project/api/types.go index 0cccadede3cc..53bbea24df3f 100644 --- a/pkg/project/api/types.go +++ b/pkg/project/api/types.go @@ -11,8 +11,15 @@ type ProjectList struct { Items []Project } +// These are internal finalizer values to Origin +const ( + FinalizerProject kapi.FinalizerName = "openshift.com/project" +) + // ProjectSpec describes the attributes on a Project type ProjectSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []kapi.FinalizerName } // ProjectStatus is information about the current status of a Project diff --git a/pkg/project/api/v1beta1/types.go b/pkg/project/api/v1beta1/types.go index 5e4c45408848..e631d71c3cbc 100644 --- a/pkg/project/api/v1beta1/types.go +++ b/pkg/project/api/v1beta1/types.go @@ -11,8 +11,15 @@ type ProjectList struct { Items []Project `json:"items"` } +// These are internal finalizer values to Origin +const ( + FinalizerProject kapi.FinalizerName = "openshift.com/project" +) + // ProjectSpec describes the attributes on a Project type ProjectSpec struct { + // Finalizers is an opaque list of values that must be empty to permanently remove object from storage + Finalizers []kapi.FinalizerName `json:"finalizers,omitempty" description:"an opaque list of values that must be empty to permanently remove object from storage"` } // ProjectStatus is information about the current status of a Project diff --git a/pkg/project/api/validation/validation.go b/pkg/project/api/validation/validation.go index 0e6c489820ad..108b4a27ee5f 100644 --- a/pkg/project/api/validation/validation.go +++ b/pkg/project/api/validation/validation.go @@ -3,6 +3,7 @@ package validation import ( "strings" + kvalidation "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" "github.com/openshift/origin/pkg/project/api" @@ -29,3 +30,12 @@ func ValidateProject(project *api.Project) fielderrors.ValidationErrorList { func validateNoNewLineOrTab(s string) bool { return !(strings.Contains(s, "\n") || strings.Contains(s, "\t")) } + +// ValidateProjectUpdate tests to make sure a project update can be applied. Modifies newProject with immutable fields. +func ValidateProjectUpdate(newProject *api.Project, oldProject *api.Project) fielderrors.ValidationErrorList { + allErrs := fielderrors.ValidationErrorList{} + allErrs = append(allErrs, kvalidation.ValidateObjectMetaUpdate(&oldProject.ObjectMeta, &newProject.ObjectMeta).Prefix("metadata")...) + newProject.Spec.Finalizers = oldProject.Spec.Finalizers + newProject.Status = oldProject.Status + return allErrs +} diff --git a/pkg/project/controller/controller.go b/pkg/project/controller/controller.go index 80ef379c16c0..e98f23ded159 100644 --- a/pkg/project/controller/controller.go +++ b/pkg/project/controller/controller.go @@ -5,7 +5,9 @@ import ( kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" osclient "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/project/api" ) // NamespaceController is responsible for participating in Kubernetes Namespace termination @@ -23,17 +25,61 @@ type fatalError string func (e fatalError) Error() string { return "fatal error handling namespace: " + string(e) } // Handle processes a namespace and deletes content in origin if its terminating -func (c *NamespaceController) Handle(namespace *kapi.Namespace) error { - // ignore namespaces that are not terminating +func (c *NamespaceController) Handle(namespace *kapi.Namespace) (err error) { + // if namespace is not terminating, ignore it if namespace.Status.Phase != kapi.NamespaceTerminating { return nil } - deleteAllContent(c.Client, namespace.Name) - // TODO: finalize namespace (remove openshift.com/origin) + // if we already processed this namespace, ignore it + if finalized(namespace) { + return nil + } + + // there may still be content for us to remove + err = deleteAllContent(c.Client, namespace.Name) + if err != nil { + return err + } + + // we have removed content, so mark it finalized by us + err = finalize(c.KubeClient, namespace) + if err != nil { + return err + } + return nil } +// finalized returns true if the spec.finalizers does not contain the project finalizer +func finalized(namespace *kapi.Namespace) bool { + for i := range namespace.Spec.Finalizers { + if api.FinalizerProject == namespace.Spec.Finalizers[i] { + return false + } + } + return true +} + +// finalize will finalize the namespace for kubernetes +func finalize(kubeClient kclient.Interface, namespace *kapi.Namespace) error { + namespaceFinalize := kapi.Namespace{} + namespaceFinalize.ObjectMeta = namespace.ObjectMeta + namespaceFinalize.Spec = namespace.Spec + finalizerSet := util.NewStringSet() + for i := range namespace.Spec.Finalizers { + if namespace.Spec.Finalizers[i] != api.FinalizerProject { + finalizerSet.Insert(string(namespace.Spec.Finalizers[i])) + } + } + namespaceFinalize.Spec.Finalizers = make([]kapi.FinalizerName, 0, len(finalizerSet)) + for _, value := range finalizerSet.List() { + namespaceFinalize.Spec.Finalizers = append(namespaceFinalize.Spec.Finalizers, kapi.FinalizerName(value)) + } + _, err := kubeClient.Namespaces().Finalize(&namespaceFinalize) + return err +} + // deleteAllContent will purge all content in openshift in the specified namespace func deleteAllContent(client osclient.Interface, namespace string) (err error) { err = deleteBuildConfigs(client, namespace) diff --git a/pkg/project/controller/controller_test.go b/pkg/project/controller/controller_test.go index 16e48f649d42..eeda2b1a9050 100644 --- a/pkg/project/controller/controller_test.go +++ b/pkg/project/controller/controller_test.go @@ -3,11 +3,11 @@ package controller import ( "testing" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - osclient "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/project/api" ) func TestSyncNamespaceThatIsTerminating(t *testing.T) { @@ -17,18 +17,18 @@ func TestSyncNamespaceThatIsTerminating(t *testing.T) { KubeClient: mockKubeClient, Client: mockOriginClient, } - //now := util.Now() - testNamespace := &api.Namespace{ - ObjectMeta: api.ObjectMeta{ - Name: "test", - ResourceVersion: "1", - // DeletionTimestamp: &now, + now := util.Now() + testNamespace := &kapi.Namespace{ + ObjectMeta: kapi.ObjectMeta{ + Name: "test", + ResourceVersion: "1", + DeletionTimestamp: &now, }, - // Spec: api.NamespaceSpec{ - // Finalizers: []api.FinalizerName{"kubernetes"}, - // }, - Status: api.NamespaceStatus{ - Phase: api.NamespaceTerminating, + Spec: kapi.NamespaceSpec{ + Finalizers: []kapi.FinalizerName{kapi.FinalizerKubernetes, api.FinalizerProject}, + }, + Status: kapi.NamespaceStatus{ + Phase: kapi.NamespaceTerminating, }, } err := nm.Handle(testNamespace) @@ -67,18 +67,16 @@ func TestSyncNamespaceThatIsActive(t *testing.T) { KubeClient: mockKubeClient, Client: mockOriginClient, } - //now := util.Now() - testNamespace := &api.Namespace{ - ObjectMeta: api.ObjectMeta{ + testNamespace := &kapi.Namespace{ + ObjectMeta: kapi.ObjectMeta{ Name: "test", ResourceVersion: "1", - // DeletionTimestamp: &now, }, - // Spec: api.NamespaceSpec{ - // Finalizers: []api.FinalizerName{"kubernetes"}, - // }, - Status: api.NamespaceStatus{ - Phase: api.NamespaceActive, + Spec: kapi.NamespaceSpec{ + Finalizers: []kapi.FinalizerName{kapi.FinalizerKubernetes, api.FinalizerProject}, + }, + Status: kapi.NamespaceStatus{ + Phase: kapi.NamespaceActive, }, } err := nm.Handle(testNamespace) diff --git a/pkg/project/registry/project/proxy/proxy.go b/pkg/project/registry/project/proxy/proxy.go new file mode 100644 index 000000000000..ee7e04bd911b --- /dev/null +++ b/pkg/project/registry/project/proxy/proxy.go @@ -0,0 +1,135 @@ +package proxy + +import ( + "fmt" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + "github.com/openshift/origin/pkg/project/api" + projectauth "github.com/openshift/origin/pkg/project/auth" + projectregistry "github.com/openshift/origin/pkg/project/registry/project" +) + +type REST struct { + // client can modify Kubernetes namespaces + client kclient.NamespaceInterface + // lister can enumerate project lists that enforce policy + lister projectauth.Lister + // Allows extended behavior during creation, required + createStrategy rest.RESTCreateStrategy + // Allows extended behavior during updates, required + updateStrategy rest.RESTUpdateStrategy +} + +// NewREST returns a RESTStorage object that will work against Project resources +func NewREST(client kclient.NamespaceInterface, lister projectauth.Lister) *REST { + return &REST{ + client: client, + lister: lister, + createStrategy: projectregistry.Strategy, + updateStrategy: projectregistry.Strategy, + } +} + +// New returns a new Project +func (s *REST) New() runtime.Object { + return &api.Project{} +} + +// NewList returns a new ProjectList +func (*REST) NewList() runtime.Object { + return &api.ProjectList{} +} + +// convertNamespace transforms a Namespace into a Project +func convertNamespace(namespace *kapi.Namespace) *api.Project { + displayName := namespace.Annotations["displayname"] + return &api.Project{ + ObjectMeta: namespace.ObjectMeta, + DisplayName: displayName, + Spec: api.ProjectSpec{ + Finalizers: namespace.Spec.Finalizers, + }, + Status: api.ProjectStatus{ + Phase: namespace.Status.Phase, + }, + } +} + +// convertProject transforms a Project into a Namespace +func convertProject(project *api.Project) *kapi.Namespace { + namespace := &kapi.Namespace{ + ObjectMeta: project.ObjectMeta, + Spec: kapi.NamespaceSpec{ + Finalizers: project.Spec.Finalizers, + }, + Status: kapi.NamespaceStatus{ + Phase: project.Status.Phase, + }, + } + if namespace.Annotations == nil { + namespace.Annotations = map[string]string{} + } + namespace.Annotations["displayname"] = project.DisplayName + return namespace +} + +// convertNamespaceList transforms a NamespaceList into a ProjectList +func convertNamespaceList(namespaceList *kapi.NamespaceList) *api.ProjectList { + projects := &api.ProjectList{} + for _, n := range namespaceList.Items { + projects.Items = append(projects.Items, *convertNamespace(&n)) + } + return projects +} + +// List retrieves a list of Projects that match label. +func (s *REST) List(ctx kapi.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { + user, ok := kapi.UserFrom(ctx) + if !ok { + return nil, kerrors.NewForbidden("Project", "", fmt.Errorf("Unable to list projects without a user on the context")) + } + namespaceList, err := s.lister.List(user) + if err != nil { + return nil, err + } + return convertNamespaceList(namespaceList), nil +} + +// Get retrieves a Project by name +func (s *REST) Get(ctx kapi.Context, name string) (runtime.Object, error) { + namespace, err := s.client.Get(name) + if err != nil { + return nil, err + } + return convertNamespace(namespace), nil +} + +// Create registers the given Project. +func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { + project, ok := obj.(*api.Project) + if !ok { + return nil, fmt.Errorf("not a project: %#v", obj) + } + kapi.FillObjectMetaSystemFields(ctx, &project.ObjectMeta) + s.createStrategy.PrepareForCreate(obj) + if errs := s.createStrategy.Validate(ctx, obj); len(errs) > 0 { + return nil, kerrors.NewInvalid("project", project.Name, errs) + } + namespace, err := s.client.Create(convertProject(project)) + if err != nil { + return nil, err + } + return convertNamespace(namespace), nil +} + +// Delete deletes a Project specified by its name +func (s *REST) Delete(ctx kapi.Context, name string) (runtime.Object, error) { + return &kapi.Status{Status: kapi.StatusSuccess}, s.client.Delete(name) +} diff --git a/pkg/project/registry/project/proxy/proxy_test.go b/pkg/project/registry/project/proxy/proxy_test.go new file mode 100644 index 000000000000..c1cc82ded275 --- /dev/null +++ b/pkg/project/registry/project/proxy/proxy_test.go @@ -0,0 +1,141 @@ +package proxy + +import ( + // "fmt" + "strings" + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/openshift/origin/pkg/project/api" +) + +// mockLister returns the namespaces in the list +type mockLister struct { + namespaceList *kapi.NamespaceList +} + +func (ml *mockLister) List(user user.Info) (*kapi.NamespaceList, error) { + return ml.namespaceList, nil +} + +func TestListProjects(t *testing.T) { + namespaceList := kapi.NamespaceList{ + Items: []kapi.Namespace{ + { + ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + }, + }, + } + mockClient := &kclient.Fake{ + NamespacesList: namespaceList, + } + storage := REST{ + client: mockClient.Namespaces(), + lister: &mockLister{&namespaceList}, + } + user := &user.DefaultInfo{ + Name: "test-user", + UID: "test-uid", + Groups: []string{"test-groups"}, + } + ctx := kapi.WithUser(kapi.NewContext(), user) + response, err := storage.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("%#v should be nil.", err) + } + projects := response.(*api.ProjectList) + if len(projects.Items) != 1 { + t.Errorf("%#v projects.Items should have len 1.", projects.Items) + } + responseProject := projects.Items[0] + if e, r := responseProject.Name, "foo"; e != r { + t.Errorf("%#v != %#v.", e, r) + } +} + +func TestCreateProjectBadObject(t *testing.T) { + storage := REST{} + + obj, err := storage.Create(kapi.NewContext(), &api.ProjectList{}) + if obj != nil { + t.Errorf("Expected nil, got %v", obj) + } + if strings.Index(err.Error(), "not a project:") == -1 { + t.Errorf("Expected 'not an project' error, got %v", err) + } +} + +func TestCreateInvalidProject(t *testing.T) { + mockClient := &kclient.Fake{} + storage := NewREST(mockClient.Namespaces(), &mockLister{}) + _, err := storage.Create(nil, &api.Project{ + DisplayName: "h\t\ni", + }) + if !errors.IsInvalid(err) { + t.Errorf("Expected 'invalid' error, got %v", err) + } +} + +func TestCreateProjectOK(t *testing.T) { + mockClient := &kclient.Fake{} + storage := NewREST(mockClient.Namespaces(), &mockLister{}) + _, err := storage.Create(kapi.NewContext(), &api.Project{ + ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + }) + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + if len(mockClient.Actions) != 1 { + t.Errorf("Expected client action for create") + } + if mockClient.Actions[0].Action != "create-namespace" { + t.Errorf("Expected call to create-namespace") + } +} + +func TestGetProjectOK(t *testing.T) { + mockClient := &kclient.Fake{} + storage := NewREST(mockClient.Namespaces(), &mockLister{}) + project, err := storage.Get(kapi.NewContext(), "foo") + if project == nil { + t.Error("Unexpected nil project") + } + if err != nil { + t.Errorf("Unexpected non-nil error: %v", err) + } + if project.(*api.Project).Name != "foo" { + t.Errorf("Unexpected project: %#v", project) + } +} + +func TestDeleteProject(t *testing.T) { + mockClient := &kclient.Fake{} + storage := REST{ + client: mockClient.Namespaces(), + } + obj, err := storage.Delete(kapi.NewContext(), "foo") + if obj == nil { + t.Error("Unexpected nil obj") + } + if err != nil { + t.Errorf("Unexpected non-nil error: %#v", err) + } + status, ok := obj.(*kapi.Status) + if !ok { + t.Errorf("Expected status type, got: %#v", obj) + } + if status.Status != kapi.StatusSuccess { + t.Errorf("Expected status=success, got: %#v", status) + } + if len(mockClient.Actions) != 1 { + t.Errorf("Expected client action for delete") + } + if mockClient.Actions[0].Action != "delete-namespace" { + t.Errorf("Expected call to delete-namespace") + } +} diff --git a/pkg/project/registry/project/rest.go b/pkg/project/registry/project/rest.go index 07cf2b56dddd..bebb7a57f28e 100644 --- a/pkg/project/registry/project/rest.go +++ b/pkg/project/registry/project/rest.go @@ -1,114 +1,68 @@ package project import ( - "fmt" - kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" - kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" "github.com/openshift/origin/pkg/project/api" "github.com/openshift/origin/pkg/project/api/validation" - projectauth "github.com/openshift/origin/pkg/project/auth" ) -type REST struct { - client kclient.NamespaceInterface - lister projectauth.Lister -} - -// NewREST returns a RESTStorage object that will work against Project resources -func NewREST(client kclient.NamespaceInterface, lister projectauth.Lister) *REST { - return &REST{client: client, lister: lister} -} - -// New returns a new Project -func (s *REST) New() runtime.Object { - return &api.Project{} +// projectStrategy implements behavior for projects +type projectStrategy struct { + runtime.ObjectTyper + kapi.NameGenerator } -// NewList returns a new ProjectList -func (*REST) NewList() runtime.Object { - return &api.ProjectList{} -} +// Strategy is the default logic that applies when creating and updating Project +// objects via the REST API. +var Strategy = projectStrategy{kapi.Scheme, kapi.SimpleNameGenerator} -// convertNamespace transforms a Namespace into a Project -func convertNamespace(namespace *kapi.Namespace) *api.Project { - displayName := namespace.Annotations["displayname"] - return &api.Project{ - ObjectMeta: namespace.ObjectMeta, - DisplayName: displayName, - Spec: api.ProjectSpec{}, - Status: api.ProjectStatus{ - Phase: namespace.Status.Phase, - }, - } +// NamespaceScoped is false for projects. +func (projectStrategy) NamespaceScoped() bool { + return false } -// convertProject transforms a Project into a Namespace -func convertProject(project *api.Project) *kapi.Namespace { - namespace := &kapi.Namespace{ - ObjectMeta: project.ObjectMeta, - } - if namespace.Annotations == nil { - namespace.Annotations = map[string]string{} +// PrepareForCreate clears fields that are not allowed to be set by end users on creation. +func (projectStrategy) PrepareForCreate(obj runtime.Object) { + project := obj.(*api.Project) + hasProjectFinalizer := false + for i := range project.Spec.Finalizers { + if project.Spec.Finalizers[i] == api.FinalizerProject { + hasProjectFinalizer = true + break + } } - namespace.Annotations["displayname"] = project.DisplayName - return namespace -} - -// convertNamespaceList transforms a NamespaceList into a ProjectList -func convertNamespaceList(namespaceList *kapi.NamespaceList) *api.ProjectList { - projects := &api.ProjectList{} - for _, n := range namespaceList.Items { - projects.Items = append(projects.Items, *convertNamespace(&n)) + if !hasProjectFinalizer { + if len(project.Spec.Finalizers) == 0 { + project.Spec.Finalizers = []kapi.FinalizerName{api.FinalizerProject} + } else { + project.Spec.Finalizers = append(project.Spec.Finalizers, api.FinalizerProject) + } } - return projects } -// List retrieves a list of Projects that match label. -func (s *REST) List(ctx kapi.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { - user, ok := kapi.UserFrom(ctx) - if !ok { - return nil, kerrors.NewForbidden("Project", "", fmt.Errorf("Unable to list projects without a user on the context")) - } - namespaceList, err := s.lister.List(user) - if err != nil { - return nil, err - } - return convertNamespaceList(namespaceList), nil +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (projectStrategy) PrepareForUpdate(obj, old runtime.Object) { + newProject := obj.(*api.Project) + oldProject := old.(*api.Project) + newProject.Spec.Finalizers = oldProject.Spec.Finalizers + newProject.Status = oldProject.Status } -// Get retrieves a Project by name -func (s *REST) Get(ctx kapi.Context, name string) (runtime.Object, error) { - namespace, err := s.client.Get(name) - if err != nil { - return nil, err - } - return convertNamespace(namespace), nil +// Validate validates a new project. +func (projectStrategy) Validate(ctx kapi.Context, obj runtime.Object) fielderrors.ValidationErrorList { + project := obj.(*api.Project) + return validation.ValidateProject(project) } -// Create registers the given Project. -func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) { - project, ok := obj.(*api.Project) - if !ok { - return nil, fmt.Errorf("not a project: %#v", obj) - } - kapi.FillObjectMetaSystemFields(ctx, &project.ObjectMeta) - if errs := validation.ValidateProject(project); len(errs) > 0 { - return nil, kerrors.NewInvalid("project", project.Name, errs) - } - namespace, err := s.client.Create(convertProject(project)) - if err != nil { - return nil, err - } - return convertNamespace(namespace), nil +// AllowCreateOnUpdate is false for project. +func (projectStrategy) AllowCreateOnUpdate() bool { + return false } -// Delete deletes a Project specified by its name -func (s *REST) Delete(ctx kapi.Context, name string) (runtime.Object, error) { - return &kapi.Status{Status: kapi.StatusSuccess}, s.client.Delete(name) +// ValidateUpdate is the default update validation for an end user. +func (projectStrategy) ValidateUpdate(ctx kapi.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateProjectUpdate(obj.(*api.Project), old.(*api.Project)) } diff --git a/pkg/project/registry/project/rest_test.go b/pkg/project/registry/project/rest_test.go index 7d0792b42b6c..e4381a16bc5d 100644 --- a/pkg/project/registry/project/rest_test.go +++ b/pkg/project/registry/project/rest_test.go @@ -1,170 +1,61 @@ +/* +Copyright 2014 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 project import ( - // "fmt" - "strings" "testing" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" - "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" - kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/openshift/origin/pkg/project/api" ) -// mockLister returns the namespaces in the list -type mockLister struct { - namespaceList *kapi.NamespaceList -} - -func (ml *mockLister) List(user user.Info) (*kapi.NamespaceList, error) { - return ml.namespaceList, nil -} - -func TestListProjects(t *testing.T) { - namespaceList := kapi.NamespaceList{ - Items: []kapi.Namespace{ - { - ObjectMeta: kapi.ObjectMeta{Name: "foo"}, - }, - }, - } - mockClient := &kclient.Fake{ - NamespacesList: namespaceList, - } - storage := REST{ - client: mockClient.Namespaces(), - lister: &mockLister{&namespaceList}, +func TestProjectStrategy(t *testing.T) { + ctx := kapi.NewDefaultContext() + if Strategy.NamespaceScoped() { + t.Errorf("Projects should not be namespace scoped") } - user := &user.DefaultInfo{ - Name: "test-user", - UID: "test-uid", - Groups: []string{"test-groups"}, + if Strategy.AllowCreateOnUpdate() { + t.Errorf("Projects should not allow create on update") } - ctx := kapi.WithUser(kapi.NewContext(), user) - response, err := storage.List(ctx, labels.Everything(), fields.Everything()) - if err != nil { - t.Errorf("%#v should be nil.", err) - } - projects := response.(*api.ProjectList) - if len(projects.Items) != 1 { - t.Errorf("%#v projects.Items should have len 1.", projects.Items) - } - responseProject := projects.Items[0] - if e, r := responseProject.Name, "foo"; e != r { - t.Errorf("%#v != %#v.", e, r) - } -} - -func TestCreateProjectBadObject(t *testing.T) { - storage := REST{} - - obj, err := storage.Create(nil, &api.ProjectList{}) - if obj != nil { - t.Errorf("Expected nil, got %v", obj) - } - if strings.Index(err.Error(), "not a project:") == -1 { - t.Errorf("Expected 'not an project' error, got %v", err) - } -} - -func TestCreateProjectMissingID(t *testing.T) { - storage := REST{} - - obj, err := storage.Create(nil, &api.Project{}) - if obj != nil { - t.Errorf("Expected nil obj, got %v", obj) - } - if !errors.IsInvalid(err) { - t.Errorf("Expected 'invalid' error, got %v", err) - } -} - -func TestCreateProjectOK(t *testing.T) { - mockClient := &kclient.Fake{} - storage := REST{ - client: mockClient.Namespaces(), - } - _, err := storage.Create(nil, &api.Project{ - ObjectMeta: kapi.ObjectMeta{Name: "foo"}, - }) - if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) - } - if len(mockClient.Actions) != 1 { - t.Errorf("Expected client action for create") - } - if mockClient.Actions[0].Action != "create-namespace" { - t.Errorf("Expected call to create-namespace") - } - - /* - TODO: Need upstream change to fake_namespaces so create returns the object passed in on client and not nil object - project, ok := obj.(*api.Project) - if !ok { - t.Errorf("Expected project type, got: %#v", obj) - } - if project.Name != "foo" { - t.Errorf("Unexpected project: %#v", project) - }*/ -} - -func TestGetProjectError(t *testing.T) { - // TODO: Need upstream change to fake_namespaces so get returns the error on Fake - /* - mockRegistry := test.NewProjectRegistry() - mockRegistry.Err = fmt.Errorf("bad") - storage := REST{registry: mockRegistry} - - project, err := storage.Get(nil, "foo") - if project != nil { - t.Errorf("Unexpected non-nil project: %#v", project) - } - if err != mockRegistry.Err { - t.Errorf("Expected %#v, got %#v", mockRegistry.Err, err) - }*/ -} - -func TestGetProjectOK(t *testing.T) { - mockClient := &kclient.Fake{} - storage := REST{client: mockClient.Namespaces()} - project, err := storage.Get(nil, "foo") - if project == nil { - t.Error("Unexpected nil project") - } - if err != nil { - t.Errorf("Unexpected non-nil error: %v", err) - } - if project.(*api.Project).Name != "foo" { - t.Errorf("Unexpected project: %#v", project) - } -} - -func TestDeleteProject(t *testing.T) { - mockClient := &kclient.Fake{} - storage := REST{ - client: mockClient.Namespaces(), + project := &api.Project{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "10"}, } - obj, err := storage.Delete(nil, "foo") - if obj == nil { - t.Error("Unexpected nil obj") + Strategy.PrepareForCreate(project) + if len(project.Spec.Finalizers) != 1 || project.Spec.Finalizers[0] != api.FinalizerProject { + t.Errorf("Prepare For Create should have added project finalizer") } - if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + errs := Strategy.Validate(ctx, project) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) } - status, ok := obj.(*kapi.Status) - if !ok { - t.Errorf("Expected status type, got: %#v", obj) + invalidProject := &api.Project{ + ObjectMeta: kapi.ObjectMeta{Name: "bar", ResourceVersion: "4"}, } - if status.Status != kapi.StatusSuccess { - t.Errorf("Expected status=success, got: %#v", status) + // ensure we copy spec.finalizers from old to new + Strategy.PrepareForUpdate(invalidProject, project) + if len(invalidProject.Spec.Finalizers) != 1 || invalidProject.Spec.Finalizers[0] != api.FinalizerProject { + t.Errorf("PrepareForUpdate should have preserved old.spec.finalizers") } - if len(mockClient.Actions) != 1 { - t.Errorf("Expected client action for delete") + errs = Strategy.ValidateUpdate(ctx, invalidProject, project) + if len(errs) == 0 { + t.Errorf("Expected a validation error") } - if mockClient.Actions[0].Action != "delete-namespace" { - t.Errorf("Expected call to delete-namespace") + if invalidProject.ResourceVersion != "4" { + t.Errorf("Incoming resource version on update should not be mutated") } } diff --git a/test/integration/project_test.go b/test/integration/project_test.go index 61261fe7d6dc..16f4a53f65d6 100644 --- a/test/integration/project_test.go +++ b/test/integration/project_test.go @@ -21,7 +21,7 @@ import ( "github.com/openshift/origin/pkg/api/v1beta1" "github.com/openshift/origin/pkg/client" projectapi "github.com/openshift/origin/pkg/project/api" - projectregistry "github.com/openshift/origin/pkg/project/registry/project" + projectregistry "github.com/openshift/origin/pkg/project/registry/project/proxy" testutil "github.com/openshift/origin/test/util" )