diff --git a/pkg/master/thirdparty_controller.go b/pkg/master/thirdparty_controller.go new file mode 100644 index 00000000000..6101b7f7c12 --- /dev/null +++ b/pkg/master/thirdparty_controller.go @@ -0,0 +1,122 @@ +/* +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 master + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + thirdpartyresourceetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresource/etcd" + "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/sets" +) + +const thirdpartyprefix = "/thirdparty/" + +func makeThirdPartyPath(group string) string { + return thirdpartyprefix + group +} + +// resourceInterface is the interface for the parts of the master than know how to add/remove +// third party resources. Extracted into an interface for injection for testing. +type resourceInterface interface { + // Remove a third party resource based on the RESTful path for that resource + RemoveThirdPartyResource(path string) error + // Install a third party resource described by 'rsrc' + InstallThirdPartyResource(rsrc *expapi.ThirdPartyResource) error + // Is a particular third party resource currently installed? + HasThirdPartyResource(rsrc *expapi.ThirdPartyResource) (bool, error) + // List all currently installed third party resources + ListThirdPartyResources() []string +} + +// ThirdPartyController is a control loop that knows how to synchronize ThirdPartyResource objects with +// RESTful resources which are present in the API server. +type ThirdPartyController struct { + master resourceInterface + thirdPartyResourceRegistry *thirdpartyresourceetcd.REST +} + +// Synchronize a single resource with RESTful resources on the master +func (t *ThirdPartyController) SyncOneResource(rsrc *expapi.ThirdPartyResource) error { + hasResource, err := t.master.HasThirdPartyResource(rsrc) + if err != nil { + return err + } + if !hasResource { + return t.master.InstallThirdPartyResource(rsrc) + } + return nil +} + +// Synchronize all resources with RESTful resources on the master +func (t *ThirdPartyController) SyncResources() error { + list, err := t.thirdPartyResourceRegistry.List(api.NewDefaultContext(), labels.Everything(), fields.Everything()) + if err != nil { + return err + } + return t.syncResourceList(list) +} + +func (t *ThirdPartyController) syncResourceList(list runtime.Object) error { + existing := sets.String{} + switch list := list.(type) { + case *expapi.ThirdPartyResourceList: + // Loop across all schema objects for third party resources + for ix := range list.Items { + item := &list.Items[ix] + // extract the api group and resource kind from the schema + _, group, err := thirdpartyresourcedata.ExtractApiGroupAndKind(item) + if err != nil { + return err + } + // place it in the set of resources that we expect, so that we don't delete it in the delete pass + existing.Insert(makeThirdPartyPath(group)) + // ensure a RESTful resource for this schema exists on the master + if err := t.SyncOneResource(item); err != nil { + return err + } + } + default: + return fmt.Errorf("expected a *ThirdPartyResourceList, got %#v", list) + } + // deletion phase, get all installed RESTful resources + installed := t.master.ListThirdPartyResources() + for _, installedAPI := range installed { + found := false + // search across the expected restful resources to see if this resource belongs to one of the expected ones + for _, apiPath := range existing.List() { + if strings.HasPrefix(installedAPI, apiPath) { + found = true + break + } + } + // not expected, delete the resource + if !found { + if err := t.master.RemoveThirdPartyResource(installedAPI); err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/master/thirdparty_controller_test.go b/pkg/master/thirdparty_controller_test.go new file mode 100644 index 00000000000..920124f7f44 --- /dev/null +++ b/pkg/master/thirdparty_controller_test.go @@ -0,0 +1,182 @@ +/* +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 master + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" + "k8s.io/kubernetes/pkg/util/sets" +) + +type FakeAPIInterface struct { + removed []string + installed []*expapi.ThirdPartyResource + apis []string + t *testing.T +} + +func (f *FakeAPIInterface) RemoveThirdPartyResource(path string) error { + f.removed = append(f.removed, path) + return nil +} + +func (f *FakeAPIInterface) InstallThirdPartyResource(rsrc *expapi.ThirdPartyResource) error { + f.installed = append(f.installed, rsrc) + _, group, _ := thirdpartyresourcedata.ExtractApiGroupAndKind(rsrc) + f.apis = append(f.apis, makeThirdPartyPath(group)) + return nil +} + +func (f *FakeAPIInterface) HasThirdPartyResource(rsrc *expapi.ThirdPartyResource) (bool, error) { + if f.apis == nil { + return false, nil + } + _, group, _ := thirdpartyresourcedata.ExtractApiGroupAndKind(rsrc) + path := makeThirdPartyPath(group) + for _, api := range f.apis { + if api == path { + return true, nil + } + } + return false, nil +} + +func (f *FakeAPIInterface) ListThirdPartyResources() []string { + return f.apis +} + +func TestSyncAPIs(t *testing.T) { + tests := []struct { + list *expapi.ThirdPartyResourceList + apis []string + expectedInstalled []string + expectedRemoved []string + name string + }{ + { + list: &expapi.ThirdPartyResourceList{ + Items: []expapi.ThirdPartyResource{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo.example.com", + }, + }, + }, + }, + expectedInstalled: []string{"foo.example.com"}, + name: "simple add", + }, + { + list: &expapi.ThirdPartyResourceList{ + Items: []expapi.ThirdPartyResource{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo.example.com", + }, + }, + }, + }, + apis: []string{ + "/thirdparty/example.com", + "/thirdparty/example.com/v1", + }, + name: "does nothing", + }, + { + list: &expapi.ThirdPartyResourceList{ + Items: []expapi.ThirdPartyResource{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo.example.com", + }, + }, + { + ObjectMeta: api.ObjectMeta{ + Name: "foo.company.com", + }, + }, + }, + }, + apis: []string{ + "/thirdparty/company.com", + "/thirdparty/company.com/v1", + }, + expectedInstalled: []string{"foo.example.com"}, + name: "adds with existing", + }, + { + list: &expapi.ThirdPartyResourceList{ + Items: []expapi.ThirdPartyResource{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo.example.com", + }, + }, + }, + }, + apis: []string{ + "/thirdparty/company.com", + "/thirdparty/company.com/v1", + }, + expectedInstalled: []string{"foo.example.com"}, + expectedRemoved: []string{"/thirdparty/company.com", "/thirdparty/company.com/v1"}, + name: "removes with existing", + }, + } + + for _, test := range tests { + fake := FakeAPIInterface{ + apis: test.apis, + t: t, + } + + cntrl := ThirdPartyController{master: &fake} + + if err := cntrl.syncResourceList(test.list); err != nil { + t.Errorf("[%s] unexpected error: %v", test.name) + } + if len(test.expectedInstalled) != len(fake.installed) { + t.Errorf("[%s] unexpected installed APIs: %d, expected %d (%#v)", test.name, len(fake.installed), len(test.expectedInstalled), fake.installed[0]) + continue + } else { + names := sets.String{} + for ix := range fake.installed { + names.Insert(fake.installed[ix].Name) + } + for _, name := range test.expectedInstalled { + if !names.Has(name) { + t.Errorf("[%s] missing installed API: %s", test.name, name) + } + } + } + if len(test.expectedRemoved) != len(fake.removed) { + t.Errorf("[%s] unexpected installed APIs: %d, expected %d", test.name, len(fake.removed), len(test.expectedRemoved)) + continue + } else { + names := sets.String{} + names.Insert(fake.removed...) + for _, name := range test.expectedRemoved { + if !names.Has(name) { + t.Errorf("[%s] missing removed API: %s (%s)", test.name, name, names) + } + } + } + } +}