forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the default plan in admission controller
- Loading branch information
Ville Aikas
committed
Aug 10, 2017
1 parent
a6bb576
commit 6489d90
Showing
5 changed files
with
366 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
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 defaultserviceplan | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
|
||
"github.com/golang/glog" | ||
|
||
"k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apiserver/pkg/admission" | ||
|
||
informers "github.com/kubernetes-incubator/service-catalog/pkg/client/informers_generated/internalversion" | ||
internalversion "github.com/kubernetes-incubator/service-catalog/pkg/client/listers_generated/servicecatalog/internalversion" | ||
|
||
"github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" | ||
scadmission "github.com/kubernetes-incubator/service-catalog/pkg/apiserver/admission" | ||
) | ||
|
||
const ( | ||
// PluginName is name of admission plug-in | ||
PluginName = "ServicePlanDefault" | ||
) | ||
|
||
// Register registers a plugin | ||
func Register(plugins *admission.Plugins) { | ||
plugins.Register(PluginName, func(io.Reader) (admission.Interface, error) { | ||
return NewDefaultServicePlan() | ||
}) | ||
} | ||
|
||
// exists is an implementation of admission.Interface. | ||
// It checks to see if Service Instance is being created without | ||
// a Service Plan if there is only one Service Plan for the | ||
// specified Service and defaults to that value. | ||
// that the cluster actually has support for it. | ||
type defaultPlan struct { | ||
*admission.Handler | ||
scLister internalversion.ServiceClassLister | ||
} | ||
|
||
var _ = scadmission.WantsInternalServiceCatalogInformerFactory(&defaultPlan{}) | ||
|
||
func (d *defaultPlan) Admit(a admission.Attributes) error { | ||
// we need to wait for our caches to warm | ||
if !d.WaitForReady() { | ||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) | ||
} | ||
|
||
// We only care about service Instances | ||
if a.GetResource().Group != servicecatalog.GroupName || a.GetResource().GroupResource() != servicecatalog.Resource("instances") { | ||
return nil | ||
} | ||
instance, ok := a.GetObject().(*servicecatalog.Instance) | ||
if !ok { | ||
return errors.NewBadRequest("Resource was marked with kind Instance but was unable to be converted") | ||
} | ||
// If the plan is specified, let it through and have the controller | ||
// deal with finding the right plan, etc. | ||
if len(instance.Spec.PlanName) > 0 { | ||
return nil | ||
} | ||
|
||
sc, err := d.scLister.Get(instance.Spec.ServiceClassName) | ||
if err != nil { | ||
msg := fmt.Sprintf("ServiceClass %q does not exist, PlanName must be specified", instance.Spec.ServiceClassName) | ||
glog.V(4).Info(msg) | ||
return admission.NewForbidden(a, fmt.Errorf(msg)) | ||
} | ||
if len(sc.Plans) > 1 { | ||
msg := fmt.Sprintf("ServiceClass %q has more than one plan, PlanName must be specified", instance.Spec.ServiceClassName) | ||
glog.V(4).Info(msg) | ||
return admission.NewForbidden(a, fmt.Errorf(msg)) | ||
} | ||
|
||
p := sc.Plans[0] | ||
glog.V(4).Infof("Using default plan %s for Service Class %s for instance %s", | ||
p.Name, sc.Name, instance.Name) | ||
instance.Spec.PlanName = p.Name | ||
return nil | ||
} | ||
|
||
// NewDefaultServicePlan creates a new admission control handler that | ||
// fills in a default Service Plan if omitted from Service Instance | ||
// creation request and if there exists only one plan in the | ||
// specified Service Class | ||
func NewDefaultServicePlan() (admission.Interface, error) { | ||
return &defaultPlan{ | ||
Handler: admission.NewHandler(admission.Create, admission.Update), | ||
}, nil | ||
} | ||
|
||
func (d *defaultPlan) SetInternalServiceCatalogInformerFactory(f informers.SharedInformerFactory) { | ||
scInformer := f.Servicecatalog().InternalVersion().ServiceClasses() | ||
d.scLister = scInformer.Lister() | ||
d.SetReadyFunc(scInformer.Informer().HasSynced) | ||
} | ||
|
||
func (d *defaultPlan) Validate() error { | ||
if d.scLister == nil { | ||
return fmt.Errorf("missing service class lister") | ||
} | ||
return nil | ||
} |
243 changes: 243 additions & 0 deletions
243
plugin/pkg/admission/serviceplan/default/admission_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
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 defaultserviceplan | ||
|
||
import ( | ||
// "fmt" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/util/wait" | ||
"k8s.io/apiserver/pkg/admission" | ||
// "k8s.io/client-go/pkg/api/v1" | ||
core "k8s.io/client-go/testing" | ||
|
||
informers "github.com/kubernetes-incubator/service-catalog/pkg/client/informers_generated/internalversion" | ||
// internalversion "github.com/kubernetes-incubator/service-catalog/pkg/client/listers_generated/servicecatalog/internalversion" | ||
|
||
"github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" | ||
"github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/internalclientset" | ||
"github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/internalclientset/fake" | ||
|
||
scadmission "github.com/kubernetes-incubator/service-catalog/pkg/apiserver/admission" | ||
) | ||
|
||
// newHandlerForTest returns a configured handler for testing. | ||
func newHandlerForTest(internalClient internalclientset.Interface) (admission.Interface, informers.SharedInformerFactory, error) { | ||
f := informers.NewSharedInformerFactory(internalClient, 5*time.Minute) | ||
handler, err := NewDefaultServicePlan() | ||
if err != nil { | ||
return nil, f, err | ||
} | ||
pluginInitializer := scadmission.NewPluginInitializer(internalClient, f, nil, nil) | ||
pluginInitializer.Initialize(handler) | ||
err = admission.Validate(handler) | ||
return handler, f, err | ||
} | ||
|
||
// newMockServiceCatalogClientForTest creates a mock client that returns a client | ||
// configured for the specified list of namespaces with the specified phase. | ||
func newMockServiceCatalogClientForTest(sc *servicecatalog.ServiceClass) *fake.Clientset { | ||
mockClient := &fake.Clientset{} | ||
mockClient.AddReactor("get", "serviceclasses", func(action core.Action) (bool, runtime.Object, error) { | ||
return true, sc, nil | ||
}) | ||
return mockClient | ||
} | ||
|
||
// newMockClientForTest creates a mock client. | ||
func newMockClientForTest() *fake.Clientset { | ||
mockClient := &fake.Clientset{} | ||
return mockClient | ||
} | ||
|
||
// newBroker returns a new broker for testing. | ||
func newBroker() servicecatalog.Broker { | ||
return servicecatalog.Broker{ | ||
ObjectMeta: metav1.ObjectMeta{Name: "broker"}, | ||
} | ||
} | ||
|
||
// newInstance returns a new instance for the specified namespace. | ||
func newInstance(namespace string) servicecatalog.Instance { | ||
return servicecatalog.Instance{ | ||
ObjectMeta: metav1.ObjectMeta{Name: "instance", Namespace: namespace}, | ||
} | ||
} | ||
|
||
// newServiceClass returns a new instance with the specified plans. | ||
func newServiceClass(name string, plans ...string) *servicecatalog.ServiceClass { | ||
sc := &servicecatalog.ServiceClass{ObjectMeta: metav1.ObjectMeta{Name: name}} | ||
for _, plan := range plans { | ||
sc.Plans = append(sc.Plans, servicecatalog.ServicePlan{Name: plan}) | ||
} | ||
return sc | ||
} | ||
|
||
func TestWithPlanWorks(t *testing.T) { | ||
mockSCClient := newMockServiceCatalogClientForTest(&servicecatalog.ServiceClass{}) | ||
handler, informerFactory, err := newHandlerForTest(mockSCClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
|
||
instance := newInstance("dummy") | ||
instance.Spec.ServiceClassName = "foo" | ||
instance.Spec.PlanName = "bar" | ||
|
||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err != nil { | ||
actions := "" | ||
for _, action := range mockSCClient.Actions() { | ||
actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", " | ||
} | ||
t.Errorf("unexpected error returned from admission handler: %v", actions) | ||
} | ||
} | ||
|
||
func TestWithNoPlanFailsWithNoServiceClass(t *testing.T) { | ||
mockSCClient := newMockServiceCatalogClientForTest(&servicecatalog.ServiceClass{}) | ||
handler, informerFactory, err := newHandlerForTest(mockSCClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
|
||
instance := newInstance("dummy") | ||
instance.Spec.ServiceClassName = "foo" | ||
|
||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err == nil { | ||
t.Errorf("unexpected success with no plan specified and no serviceclass existing") | ||
} | ||
if !strings.Contains(err.Error(), "does not exist, PlanName must be") { | ||
t.Errorf("did not find expected error") | ||
} | ||
} | ||
|
||
func TestWithNoPlanWorksWithSinglePlan(t *testing.T) { | ||
sc := newServiceClass("foo", "bar") | ||
mockSCClient := newMockServiceCatalogClientForTest(sc) | ||
mockSCClient.AddReactor("get", "serviceclasses", func(action core.Action) (bool, runtime.Object, error) { | ||
return true, sc, nil | ||
}) | ||
handler, informerFactory, err := newHandlerForTest(mockSCClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
|
||
instance := newInstance("dummy") | ||
instance.Spec.ServiceClassName = "foo" | ||
instance.Spec.PlanName = "bar" | ||
|
||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err == nil { | ||
actions := "" | ||
for _, action := range mockSCClient.Actions() { | ||
actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", " | ||
} | ||
t.Errorf("expected error returned from admission handler: %v", actions) | ||
} | ||
} | ||
|
||
/* | ||
// TestAdmissionNamespaceDoesNotExist verifies instance is not admitted if namespace does not exist. | ||
func TestAdmissionNamespaceDoesNotExist(t *testing.T) { | ||
namespace := "test" | ||
mockClient := newMockClientForTest() | ||
mockKubeClient := newMockServiceCatalogClientForTest(map[string]v1.NamespacePhase{}) | ||
mockKubeClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) { | ||
return true, nil, fmt.Errorf("nope, out of luck") | ||
}) | ||
handler, informerFactory, kubeInformerFactory, err := newHandlerForTest(mockClient, mockKubeClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
kubeInformerFactory.Start(wait.NeverStop) | ||
instance := newInstance(namespace) | ||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err == nil { | ||
actions := "" | ||
for _, action := range mockClient.Actions() { | ||
actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", " | ||
} | ||
t.Errorf("expected error returned from admission handler: %v", actions) | ||
} | ||
} | ||
// TestAdmissionNamespaceActive verifies a resource is admitted when the namespace is active. | ||
func TestAdmissionNamespaceActive(t *testing.T) { | ||
namespace := "test" | ||
mockClient := newMockClientForTest() | ||
mockKubeClient := newMockServiceCatalogClientForTest(map[string]v1.NamespacePhase{ | ||
namespace: v1.NamespaceActive, | ||
}) | ||
handler, informerFactory, kubeInformerFactory, err := newHandlerForTest(mockClient, mockKubeClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
kubeInformerFactory.Start(wait.NeverStop) | ||
instance := newInstance(namespace) | ||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err != nil { | ||
t.Errorf("unexpected error returned from admission handler") | ||
} | ||
} | ||
// TestAdmissionNamespaceTerminating verifies a resource is not created when the namespace is terminating. | ||
func TestAdmissionNamespaceTerminating(t *testing.T) { | ||
namespace := "test" | ||
mockClient := newMockClientForTest() | ||
mockKubeClient := newMockServiceCatalogClientForTest(map[string]v1.NamespacePhase{ | ||
namespace: v1.NamespaceTerminating, | ||
}) | ||
handler, informerFactory, kubeInformerFactory, err := newHandlerForTest(mockClient, mockKubeClient) | ||
if err != nil { | ||
t.Errorf("unexpected error initializing handler: %v", err) | ||
} | ||
informerFactory.Start(wait.NeverStop) | ||
kubeInformerFactory.Start(wait.NeverStop) | ||
instance := newInstance(namespace) | ||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Create, nil)) | ||
if err == nil { | ||
t.Errorf("Expected error rejecting creates in a namespace when it is terminating") | ||
} | ||
// verify update operations in the namespace can proceed | ||
err = handler.Admit(admission.NewAttributesRecord(&instance, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Update, nil)) | ||
if err != nil { | ||
t.Errorf("Unexpected error returned from admission handler: %v", err) | ||
} | ||
// verify delete operations in the namespace can proceed | ||
err = handler.Admit(admission.NewAttributesRecord(nil, nil, servicecatalog.Kind("Instance").WithVersion("version"), instance.Namespace, instance.Name, servicecatalog.Resource("instances").WithVersion("version"), "", admission.Delete, nil)) | ||
if err != nil { | ||
t.Errorf("Unexpected error returned from admission handler: %v", err) | ||
} | ||
} | ||
*/ |