diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index aa85b25d94..5b1c429667 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -256,6 +256,7 @@ func (s *CMServer) Run(_ []string) error { return fmt.Sprintf("serviceaccount:%s:%s:%s:%s", serviceAccount.Namespace, serviceAccount.Name, serviceAccount.UID, secret.Name), nil }) serviceaccount.NewTokensController(kubeClient, serviceaccount.DefaultTokenControllerOptions(tokenGenerator)).Run() + serviceaccount.NewServiceAccountsController(kubeClient, serviceaccount.DefaultServiceAccountControllerOptions()).Run() select {} return nil diff --git a/pkg/serviceaccount/serviceaccounts_controller.go b/pkg/serviceaccount/serviceaccounts_controller.go new file mode 100644 index 0000000000..1f4e114b14 --- /dev/null +++ b/pkg/serviceaccount/serviceaccounts_controller.go @@ -0,0 +1,230 @@ +/* +Copyright 2014 The Kubernetes Authors 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 serviceaccount + +import ( + "fmt" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "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/controller/framework" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/golang/glog" +) + +// nameIndexFunc is an index function that indexes based on an object's name +func nameIndexFunc(obj interface{}) (string, error) { + meta, err := meta.Accessor(obj) + if err != nil { + return "", fmt.Errorf("object has no meta: %v", err) + } + return meta.Name(), nil +} + +type ServiceAccountControllerOptions struct { + Name string + ServiceAccountResync time.Duration + NamespaceResync time.Duration +} + +func DefaultServiceAccountControllerOptions() ServiceAccountControllerOptions { + return ServiceAccountControllerOptions{Name: "default"} +} + +// NewServiceAccountsController returns a new *ServiceAccountsController. +func NewServiceAccountsController(cl *client.Client, options ServiceAccountControllerOptions) *ServiceAccountsController { + e := &ServiceAccountsController{ + client: cl, + name: options.Name, + } + + accountSelector := fields.SelectorFromSet(map[string]string{client.ObjectNameField: options.Name}) + e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), accountSelector) + }, + WatchFunc: func(rv string) (watch.Interface, error) { + return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), accountSelector, rv) + }, + }, + &api.ServiceAccount{}, + options.ServiceAccountResync, + framework.ResourceEventHandlerFuncs{ + DeleteFunc: e.serviceAccountDeleted, + }, + cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}, + ) + + e.namespaces, e.namespaceController = framework.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return e.client.Namespaces().List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(rv string) (watch.Interface, error) { + return e.client.Namespaces().Watch(labels.Everything(), fields.Everything(), rv) + }, + }, + &api.Namespace{}, + options.NamespaceResync, + framework.ResourceEventHandlerFuncs{ + AddFunc: e.namespaceAdded, + UpdateFunc: e.namespaceUpdated, + }, + cache.Indexers{"name": nameIndexFunc}, + ) + + return e +} + +// ServiceAccountsController manages ServiceAccount objects inside Namespaces +type ServiceAccountsController struct { + stopChan chan struct{} + + client *client.Client + name string + + serviceAccounts cache.Indexer + namespaces cache.Indexer + + // Since we join two objects, we'll watch both of them with controllers. + serviceAccountController *framework.Controller + namespaceController *framework.Controller +} + +// Runs controller loops and returns immediately +func (e *ServiceAccountsController) Run() { + if e.stopChan == nil { + e.stopChan = make(chan struct{}) + go e.serviceAccountController.Run(e.stopChan) + go e.namespaceController.Run(e.stopChan) + } +} + +// Stop gracefully shuts down this controller +func (e *ServiceAccountsController) Stop() { + if e.stopChan != nil { + close(e.stopChan) + e.stopChan = nil + } +} + +// serviceAccountDeleted reacts to a ServiceAccount deletion by recreating a default ServiceAccount in the namespace if needed +func (e *ServiceAccountsController) serviceAccountDeleted(obj interface{}) { + serviceAccount, ok := obj.(*api.ServiceAccount) + if !ok { + // Unknown type. If we missed a ServiceAccount deletion, the + // corresponding secrets will be cleaned up during the Secret re-list + return + } + e.createDefaultServiceAccountIfNeeded(serviceAccount.Namespace) +} + +// namespaceAdded reacts to a Namespace creation by creating a default ServiceAccount object +func (e *ServiceAccountsController) namespaceAdded(obj interface{}) { + namespace := obj.(*api.Namespace) + e.createDefaultServiceAccountIfNeeded(namespace.Name) +} + +// namespaceUpdated reacts to a Namespace update (or re-list) by creating a default ServiceAccount in the namespace if needed +func (e *ServiceAccountsController) namespaceUpdated(oldObj interface{}, newObj interface{}) { + newNamespace := newObj.(*api.Namespace) + e.createDefaultServiceAccountIfNeeded(newNamespace.Name) +} + +// createDefaultServiceAccountIfNeeded creates a default ServiceAccount in the given namespace if: +// * it default ServiceAccount does not already exist +// * the specified namespace exists +// * the specified namespace is in the ACTIVE phase +func (e *ServiceAccountsController) createDefaultServiceAccountIfNeeded(namespace string) { + serviceAccount, err := e.getDefaultServiceAccount(namespace) + if err != nil { + glog.Error(err) + return + } + if serviceAccount != nil { + // If service account already exists, it doesn't need to be created + return + } + + namespaceObj, err := e.getNamespace(namespace) + if err != nil { + glog.Error(err) + return + } + if namespaceObj == nil { + // If namespace does not exist, no service account is needed + return + } + if namespaceObj.Status.Phase != api.NamespaceActive { + // If namespace is not active, we shouldn't try to create anything + return + } + + e.createDefaultServiceAccount(namespace) +} + +// createDefaultServiceAccount creates a default ServiceAccount in the specified namespace +func (e *ServiceAccountsController) createDefaultServiceAccount(namespace string) { + serviceAccount := &api.ServiceAccount{} + serviceAccount.Name = e.name + serviceAccount.Namespace = namespace + if _, err := e.client.ServiceAccounts(namespace).Create(serviceAccount); err != nil { + glog.Error(err) + } +} + +// getDefaultServiceAccount returns the default ServiceAccount for the given namespace +func (e *ServiceAccountsController) getDefaultServiceAccount(namespace string) (*api.ServiceAccount, error) { + + key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}} + accounts, err := e.serviceAccounts.Index("namespace", key) + if err != nil { + return nil, err + } + + for _, obj := range accounts { + serviceAccount := obj.(*api.ServiceAccount) + if e.name == serviceAccount.Name { + return serviceAccount, nil + } + } + return nil, nil +} + +// getNamespace returns the Namespace with the given name +func (e *ServiceAccountsController) getNamespace(name string) (*api.Namespace, error) { + key := &api.Namespace{ObjectMeta: api.ObjectMeta{Name: name}} + namespaces, err := e.namespaces.Index("name", key) + if err != nil { + return nil, err + } + + if len(namespaces) == 0 { + return nil, nil + } + if len(namespaces) == 1 { + return namespaces[0].(*api.Namespace), nil + } + return nil, fmt.Errorf("%d namespaces with the name %s indexed", len(namespaces), name) +} diff --git a/pkg/serviceaccount/serviceaccounts_controller_test.go b/pkg/serviceaccount/serviceaccounts_controller_test.go new file mode 100644 index 0000000000..4db08bb7e6 --- /dev/null +++ b/pkg/serviceaccount/serviceaccounts_controller_test.go @@ -0,0 +1,165 @@ +/* +Copyright 2014 The Kubernetes Authors 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 serviceaccount + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +type serverResponse struct { + statusCode int + obj interface{} +} + +func makeTestServer(t *testing.T, namespace string, serviceAccountResponse serverResponse) (*httptest.Server, *util.FakeHandler) { + fakeServiceAccountsHandler := util.FakeHandler{ + StatusCode: serviceAccountResponse.statusCode, + ResponseBody: runtime.EncodeOrDie(testapi.Codec(), serviceAccountResponse.obj.(runtime.Object)), + } + + mux := http.NewServeMux() + mux.Handle(testapi.ResourcePath("serviceAccounts", namespace, ""), &fakeServiceAccountsHandler) + mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + t.Errorf("unexpected request: %v", req.RequestURI) + res.WriteHeader(http.StatusNotFound) + }) + return httptest.NewServer(mux), &fakeServiceAccountsHandler +} + +func TestServiceAccountCreation(t *testing.T) { + ns := api.NamespaceDefault + + activeNS := &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: ns}, + Status: api.NamespaceStatus{ + Phase: api.NamespaceActive, + }, + } + terminatingNS := &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: ns}, + Status: api.NamespaceStatus{ + Phase: api.NamespaceTerminating, + }, + } + serviceAccount := &api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: "default", + Namespace: ns, + ResourceVersion: "1", + }, + } + + testcases := map[string]struct { + ExistingNamespace *api.Namespace + ExistingServiceAccount *api.ServiceAccount + + AddedNamespace *api.Namespace + UpdatedNamespace *api.Namespace + DeletedServiceAccount *api.ServiceAccount + + ExpectCreatedServiceAccount bool + }{ + "new active namespace missing serviceaccount": { + AddedNamespace: activeNS, + ExpectCreatedServiceAccount: true, + }, + "new active namespace with serviceaccount": { + ExistingServiceAccount: serviceAccount, + AddedNamespace: activeNS, + ExpectCreatedServiceAccount: false, + }, + "new terminating namespace": { + AddedNamespace: terminatingNS, + ExpectCreatedServiceAccount: false, + }, + + "updated active namespace missing serviceaccount": { + UpdatedNamespace: activeNS, + ExpectCreatedServiceAccount: true, + }, + "updated active namespace with serviceaccount": { + ExistingServiceAccount: serviceAccount, + UpdatedNamespace: activeNS, + ExpectCreatedServiceAccount: false, + }, + "updated terminating namespace": { + UpdatedNamespace: terminatingNS, + ExpectCreatedServiceAccount: false, + }, + + "deleted serviceaccount without namespace": { + DeletedServiceAccount: serviceAccount, + ExpectCreatedServiceAccount: false, + }, + "deleted serviceaccount with active namespace": { + ExistingNamespace: activeNS, + DeletedServiceAccount: serviceAccount, + ExpectCreatedServiceAccount: true, + }, + "deleted serviceaccount with terminating namespace": { + ExistingNamespace: terminatingNS, + DeletedServiceAccount: serviceAccount, + ExpectCreatedServiceAccount: false, + }, + } + + for k, tc := range testcases { + + testServer, handler := makeTestServer(t, ns, serverResponse{http.StatusOK, serviceAccount}) + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) + controller := NewServiceAccountsController(client, DefaultServiceAccountControllerOptions()) + + if tc.ExistingNamespace != nil { + controller.namespaces.Add(tc.ExistingNamespace) + } + if tc.ExistingServiceAccount != nil { + controller.serviceAccounts.Add(tc.ExistingServiceAccount) + } + + if tc.AddedNamespace != nil { + controller.namespaces.Add(tc.AddedNamespace) + controller.namespaceAdded(tc.AddedNamespace) + } + if tc.UpdatedNamespace != nil { + controller.namespaces.Add(tc.UpdatedNamespace) + controller.namespaceUpdated(nil, tc.UpdatedNamespace) + } + if tc.DeletedServiceAccount != nil { + controller.serviceAccountDeleted(tc.DeletedServiceAccount) + } + + if tc.ExpectCreatedServiceAccount { + if !handler.ValidateRequestCount(t, 1) { + t.Errorf("%s: Expected a single creation call", k) + } + } else { + if !handler.ValidateRequestCount(t, 0) { + t.Errorf("%s: Expected no creation calls", k) + } + } + + testServer.Close() + } +} diff --git a/pkg/util/fake_handler.go b/pkg/util/fake_handler.go index ca4bdff2a1..dfed31a021 100644 --- a/pkg/util/fake_handler.go +++ b/pkg/util/fake_handler.go @@ -73,13 +73,16 @@ func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Requ f.RequestBody = string(bodyReceived) } -func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) { +func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) bool { + ok := true f.lock.Lock() defer f.lock.Unlock() if f.requestCount != count { + ok = false t.Logf("Expected %d call, but got %d. Only the last call is recorded and checked.", count, f.requestCount) } f.hasBeenChecked = true + return ok } // ValidateRequest verifies that FakeHandler received a request with expected path, method, and body.