Skip to content

Commit

Permalink
Implement the default plan in admission controller
Browse files Browse the repository at this point in the history
  • Loading branch information
Ville Aikas committed Aug 10, 2017
1 parent a6bb576 commit 6489d90
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 2 deletions.
2 changes: 1 addition & 1 deletion charts/catalog/templates/apiserver-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ spec:
- {{ .Values.apiserver.audit.logPath }}
{{- end}}
- --admission-control
- "KubernetesNamespaceLifecycle"
- "KubernetesNamespaceLifecycle,DefaultServicePlan"
- --secure-port
- "8443"
- --storage-type
Expand Down
1 change: 1 addition & 0 deletions cmd/apiserver/app/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ package app
import (
// Admission policies
_ "github.com/kubernetes-incubator/service-catalog/plugin/pkg/admission/namespace/lifecycle"
_ "github.com/kubernetes-incubator/service-catalog/plugin/pkg/admission/serviceplan/default"
)
6 changes: 5 additions & 1 deletion pkg/apis/servicecatalog/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,11 @@ type InstanceSpec struct {

// PlanName is the name of the ServicePlan this Instance should be
// provisioned from.
PlanName string `json:"planName"`
// If omitted and there is only one plan in the specified ServiceClass
// it will be used.
// If omitted and there are more than one plan in the specified ServiceClass
// the request will be rejected.
PlanName string `json:"planName,omitempty"`

// Parameters is a set of the parameters to be
// passed to the underlying broker.
Expand Down
116 changes: 116 additions & 0 deletions plugin/pkg/admission/serviceplan/default/admission.go
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 plugin/pkg/admission/serviceplan/default/admission_test.go
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)
}
}
*/

0 comments on commit 6489d90

Please sign in to comment.