mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 23:37:01 +00:00
Add a third party controller that creates/deletes third party apis
This commit is contained in:
parent
e7666814e2
commit
0a3aa2242b
122
pkg/master/thirdparty_controller.go
Normal file
122
pkg/master/thirdparty_controller.go
Normal file
@ -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
|
||||||
|
}
|
182
pkg/master/thirdparty_controller_test.go
Normal file
182
pkg/master/thirdparty_controller_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user