Skip to content

Commit 7bbc8ee

Browse files
Ville Aikaspmorie
Ville Aikas
authored andcommitted
Check service class / plan before allowing provisioning or plan changes. (#1439)
* Check service class / plan before allowing provisioning or plan changes. * Update test ServiceInstance to be more complete with normal state. Use it in couple of other places to reduce unnecessary duplicate code * Address PR comments * changes due to rebase * Use the name of the Service Plan instead of ServiceInstance spec since a K8S name could have been given
1 parent baf28de commit 7bbc8ee

File tree

3 files changed

+365
-14
lines changed

3 files changed

+365
-14
lines changed

pkg/controller/controller_instance.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const (
6666
errorNonexistentClusterServiceClassMessage string = "ReferencesNonexistentServiceClass"
6767
errorNonexistentClusterServicePlanReason string = "ReferencesNonexistentServicePlan"
6868
errorNonexistentClusterServiceBrokerReason string = "ReferencesNonexistentBroker"
69+
errorDeletedClusterServiceClassReason string = "ReferencesDeletedServiceClass"
70+
errorDeletedClusterServiceClassMessage string = "ReferencesDeletedServiceClass"
71+
errorDeletedClusterServicePlanReason string = "ReferencesDeletedServicePlan"
72+
errorDeletedClusterServicePlanMessage string = "ReferencesDeletedServicePlan"
6973
errorFindingNamespaceServiceInstanceReason string = "ErrorFindingNamespaceForInstance"
7074
errorOrphanMitigationFailedReason string = "OrphanMitigationFailed"
7175

@@ -626,6 +630,17 @@ func (c *controller) reconcileServiceInstance(instance *v1beta1.ServiceInstance)
626630
return err
627631
}
628632

633+
// Check if the ServiceClass or ServicePlan has been deleted and do not allow
634+
// creation of new ServiceInstances or plan upgrades. It's little complicated
635+
// since we do want to allow parameter changes on an instance whose plan or class
636+
// has been removed from the broker's catalog.
637+
// If changes are not allowed, the method will set the appropriate status / record
638+
// events, so we can just return here on failure.
639+
err = c.checkForRemovedClassAndPlan(instance, serviceClass, servicePlan)
640+
if err != nil {
641+
return err
642+
}
643+
629644
ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{})
630645
if err != nil {
631646
s := fmt.Sprintf("Failed to get namespace %q during instance create: %s", instance.Namespace, err)
@@ -2027,6 +2042,74 @@ func (c *controller) setServiceInstanceStartOrphanMitigation(toUpdate *v1beta1.S
20272042
)
20282043
}
20292044

2045+
// checkForRemovedClassAndPlan looks at serviceClass and servicePlan and
2046+
// if either has been deleted, will block a new instance creation. If
2047+
//
2048+
func (c *controller) checkForRemovedClassAndPlan(instance *v1beta1.ServiceInstance, serviceClass *v1beta1.ClusterServiceClass, servicePlan *v1beta1.ClusterServicePlan) error {
2049+
classDeleted := serviceClass.Status.RemovedFromBrokerCatalog
2050+
planDeleted := servicePlan.Status.RemovedFromBrokerCatalog
2051+
2052+
if !classDeleted && !planDeleted {
2053+
// Neither has been deleted, life's good.
2054+
return nil
2055+
}
2056+
2057+
isProvisioning := false
2058+
if instance.Status.ReconciledGeneration == 0 {
2059+
isProvisioning = true
2060+
}
2061+
2062+
// Regardless of what's been deleted, you can always update
2063+
// parameters (ie, not change plans)
2064+
if !isProvisioning && instance.Status.ExternalProperties != nil &&
2065+
servicePlan.Spec.ExternalName == instance.Status.ExternalProperties.ClusterServicePlanExternalName {
2066+
// Service Instance has already been provisioned and we're only
2067+
// updating parameters, so let it through.
2068+
return nil
2069+
}
2070+
2071+
// At this point we know that plan is being changed
2072+
if planDeleted {
2073+
s := fmt.Sprintf("Service Plan %q (K8S name: %q) has been deleted, can not provision.", servicePlan.Spec.ExternalName, servicePlan.Name)
2074+
glog.Warningf(
2075+
`%s "%s/%s": %s`,
2076+
typeSI, instance.Namespace, instance.Name, s,
2077+
)
2078+
c.recorder.Event(instance, corev1.EventTypeWarning, errorDeletedClusterServicePlanReason, s)
2079+
2080+
setServiceInstanceCondition(
2081+
instance,
2082+
v1beta1.ServiceInstanceConditionReady,
2083+
v1beta1.ConditionFalse,
2084+
errorDeletedClusterServicePlanReason,
2085+
s,
2086+
)
2087+
if _, err := c.updateServiceInstanceStatus(instance); err != nil {
2088+
return err
2089+
}
2090+
return fmt.Errorf(s)
2091+
}
2092+
2093+
s := fmt.Sprintf("Service Class %q (K8S name: %q) has been deleted, can not provision.", serviceClass.Spec.ExternalName, serviceClass.Name)
2094+
glog.Warningf(
2095+
`%s "%s/%s": %s`,
2096+
typeSI, instance.Namespace, instance.Name, s,
2097+
)
2098+
c.recorder.Event(instance, corev1.EventTypeWarning, errorDeletedClusterServiceClassReason, s)
2099+
2100+
setServiceInstanceCondition(
2101+
instance,
2102+
v1beta1.ServiceInstanceConditionReady,
2103+
v1beta1.ConditionFalse,
2104+
errorDeletedClusterServiceClassReason,
2105+
s,
2106+
)
2107+
if _, err := c.updateServiceInstanceStatus(instance); err != nil {
2108+
return err
2109+
}
2110+
return fmt.Errorf(s)
2111+
}
2112+
20302113
// shouldStartOrphanMitigation returns whether an error with the given status
20312114
// code indicates that orphan migitation should start.
20322115
func shouldStartOrphanMitigation(statusCode int) bool {

pkg/controller/controller_instance_test.go

Lines changed: 198 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,102 @@ func TestReconcileServiceInstance(t *testing.T) {
893893
}
894894
}
895895

896+
// TestReconcileServiceInstanceFailsWithDeletedPlan tests that a ServiceInstance is not
897+
// created if the ServicePlan specified is marked as RemovedFromCatalog.
898+
func TestReconcileServiceInstanceFailsWithDeletedPlan(t *testing.T) {
899+
fakeKubeClient, fakeCatalogClient, fakeClusterServiceBrokerClient, testController, sharedInformers := newTestController(t, noFakeActions())
900+
901+
addGetNamespaceReaction(fakeKubeClient)
902+
903+
sharedInformers.ClusterServiceBrokers().Informer().GetStore().Add(getTestClusterServiceBroker())
904+
sharedInformers.ClusterServiceClasses().Informer().GetStore().Add(getTestClusterServiceClass())
905+
sp := getTestClusterServicePlan()
906+
sp.Status.RemovedFromBrokerCatalog = true
907+
sharedInformers.ClusterServicePlans().Informer().GetStore().Add(sp)
908+
909+
instance := getTestServiceInstanceWithRefs()
910+
911+
if err := testController.reconcileServiceInstance(instance); err == nil {
912+
t.Fatalf("This should fail")
913+
}
914+
915+
brokerActions := fakeClusterServiceBrokerClient.Actions()
916+
assertNumberOfClusterServiceBrokerActions(t, brokerActions, 0)
917+
918+
instanceKey := testNamespace + "/" + testServiceInstanceName
919+
920+
// Since synchronous operation, must not make it into the polling queue.
921+
if testController.pollingQueue.NumRequeues(instanceKey) != 0 {
922+
t.Fatalf("Expected polling queue to not have any record of test instance")
923+
}
924+
925+
actions := fakeCatalogClient.Actions()
926+
assertNumberOfActions(t, actions, 1)
927+
928+
// verify no kube actions
929+
kubeActions := fakeKubeClient.Actions()
930+
assertNumberOfActions(t, kubeActions, 0)
931+
932+
updatedServiceInstance := assertUpdateStatus(t, actions[0], instance)
933+
assertServiceInstanceReadyFalse(t, updatedServiceInstance, errorDeletedClusterServicePlanReason)
934+
935+
events := getRecordedEvents(testController)
936+
assertNumEvents(t, events, 1)
937+
938+
expectedEvent := corev1.EventTypeWarning + " " + errorDeletedClusterServicePlanReason + " Service Plan \"test-plan\" (K8S name: \"PGUID\") has been deleted, can not provision."
939+
if e, a := expectedEvent, events[0]; e != a {
940+
t.Fatalf("Received unexpected event: %v\nExpected: %v", a, e)
941+
}
942+
}
943+
944+
// TestReconcileServiceInstanceFailsWithDeletedClass tests that a ServiceInstance is not
945+
// created if the ServiceClass specified is marked as RemovedFromCatalog.
946+
func TestReconcileServiceInstanceFailsWithDeletedClass(t *testing.T) {
947+
fakeKubeClient, fakeCatalogClient, fakeClusterServiceBrokerClient, testController, sharedInformers := newTestController(t, noFakeActions())
948+
949+
addGetNamespaceReaction(fakeKubeClient)
950+
951+
sharedInformers.ClusterServiceBrokers().Informer().GetStore().Add(getTestClusterServiceBroker())
952+
sc := getTestClusterServiceClass()
953+
sc.Status.RemovedFromBrokerCatalog = true
954+
sharedInformers.ClusterServiceClasses().Informer().GetStore().Add(sc)
955+
sharedInformers.ClusterServicePlans().Informer().GetStore().Add(getTestClusterServicePlan())
956+
957+
instance := getTestServiceInstanceWithRefs()
958+
959+
if err := testController.reconcileServiceInstance(instance); err == nil {
960+
t.Fatalf("This should have failed")
961+
}
962+
963+
brokerActions := fakeClusterServiceBrokerClient.Actions()
964+
assertNumberOfClusterServiceBrokerActions(t, brokerActions, 0)
965+
966+
instanceKey := testNamespace + "/" + testServiceInstanceName
967+
968+
// Since synchronous operation, must not make it into the polling queue.
969+
if testController.pollingQueue.NumRequeues(instanceKey) != 0 {
970+
t.Fatalf("Expected polling queue to not have any record of test instance")
971+
}
972+
973+
actions := fakeCatalogClient.Actions()
974+
assertNumberOfActions(t, actions, 1)
975+
976+
// verify no kube actions
977+
kubeActions := fakeKubeClient.Actions()
978+
assertNumberOfActions(t, kubeActions, 0)
979+
980+
updatedServiceInstance := assertUpdateStatus(t, actions[0], instance)
981+
assertServiceInstanceReadyFalse(t, updatedServiceInstance, errorDeletedClusterServiceClassReason)
982+
983+
events := getRecordedEvents(testController)
984+
assertNumEvents(t, events, 1)
985+
986+
expectedEvent := corev1.EventTypeWarning + " " + errorDeletedClusterServiceClassReason + " Service Class \"test-serviceclass\" (K8S name: \"SCGUID\") has been deleted, can not provision."
987+
if e, a := expectedEvent, events[0]; e != a {
988+
t.Fatalf("Received unexpected event: %v\nExpected: %v", a, e)
989+
}
990+
}
991+
896992
// TestReconcileServiceInstance tests synchronously provisioning a new service
897993
func TestReconcileServiceInstanceSuccessWithK8SNames(t *testing.T) {
898994
fakeKubeClient, fakeCatalogClient, fakeClusterServiceBrokerClient, testController, sharedInformers := newTestController(t, fakeosb.FakeClientConfiguration{
@@ -3756,13 +3852,7 @@ func TestReconcileServiceInstanceWithUpdateCallFailure(t *testing.T) {
37563852
sharedInformers.ClusterServiceClasses().Informer().GetStore().Add(getTestClusterServiceClass())
37573853
sharedInformers.ClusterServicePlans().Informer().GetStore().Add(getTestClusterServicePlan())
37583854

3759-
instance := getTestServiceInstanceWithRefs()
3760-
instance.Generation = 2
3761-
instance.Status.ReconciledGeneration = 1
3762-
3763-
instance.Status.ExternalProperties = &v1beta1.ServiceInstancePropertiesState{
3764-
ClusterServicePlanExternalName: "old-plan-name",
3765-
}
3855+
instance := getTestServiceInstanceUpdatingPlan()
37663856

37673857
if err := testController.reconcileServiceInstance(instance); err == nil {
37683858
t.Fatalf("Should not be able to make the ServiceInstance.")
@@ -3823,13 +3913,7 @@ func TestReconcileServiceInstanceWithUpdateFailure(t *testing.T) {
38233913
sharedInformers.ClusterServiceClasses().Informer().GetStore().Add(getTestClusterServiceClass())
38243914
sharedInformers.ClusterServicePlans().Informer().GetStore().Add(getTestClusterServicePlan())
38253915

3826-
instance := getTestServiceInstanceWithRefs()
3827-
instance.Generation = 2
3828-
instance.Status.ReconciledGeneration = 1
3829-
3830-
instance.Status.ExternalProperties = &v1beta1.ServiceInstancePropertiesState{
3831-
ClusterServicePlanExternalName: "old-plan-name",
3832-
}
3916+
instance := getTestServiceInstanceUpdatingPlan()
38333917

38343918
if err := testController.reconcileServiceInstance(instance); err != nil {
38353919
t.Fatalf("unexpected error: %v", err)
@@ -4305,3 +4389,103 @@ func TestPollServiceInstanceAsyncFailureUpdating(t *testing.T) {
43054389
updatedServiceInstance := assertUpdateStatus(t, actions[0], instance)
43064390
assertServiceInstanceRequestFailingErrorNoOrphanMitigation(t, updatedServiceInstance, v1beta1.ServiceInstanceOperationUpdate, errorUpdateInstanceCallFailedReason, errorUpdateInstanceCallFailedReason, instance)
43074391
}
4392+
4393+
func TestCheckClassAndPlanForDeletion(t *testing.T) {
4394+
cases := []struct {
4395+
name string
4396+
instance *v1beta1.ServiceInstance
4397+
class *v1beta1.ClusterServiceClass
4398+
plan *v1beta1.ClusterServicePlan
4399+
success bool
4400+
expectedReason string
4401+
expectedErrors []string
4402+
}{
4403+
{
4404+
name: "non-deleted plan and class works",
4405+
instance: getTestServiceInstance(),
4406+
class: getTestClusterServiceClass(),
4407+
plan: getTestClusterServicePlan(),
4408+
success: true,
4409+
},
4410+
{
4411+
name: "deleted plan fails",
4412+
instance: getTestServiceInstance(),
4413+
class: getTestClusterServiceClass(),
4414+
plan: getTestMarkedAsRemovedClusterServicePlan(),
4415+
success: false,
4416+
expectedReason: errorDeletedClusterServicePlanReason,
4417+
expectedErrors: []string{"Service Plan", "has been deleted"},
4418+
},
4419+
{
4420+
name: "deleted class fails",
4421+
instance: getTestServiceInstance(),
4422+
class: getTestMarkedAsRemovedClusterServiceClass(),
4423+
plan: getTestClusterServicePlan(),
4424+
success: false,
4425+
expectedReason: errorDeletedClusterServiceClassReason,
4426+
expectedErrors: []string{"Service Class", "has been deleted"},
4427+
},
4428+
{
4429+
name: "deleted plan and class fails",
4430+
instance: getTestServiceInstance(),
4431+
class: getTestClusterServiceClass(),
4432+
plan: getTestMarkedAsRemovedClusterServicePlan(),
4433+
success: false,
4434+
expectedReason: errorDeletedClusterServicePlanReason,
4435+
expectedErrors: []string{"Service Plan", "has been deleted"},
4436+
},
4437+
{
4438+
name: "Updating plan fails",
4439+
instance: getTestServiceInstanceUpdatingPlan(),
4440+
class: getTestClusterServiceClass(),
4441+
plan: getTestMarkedAsRemovedClusterServicePlan(),
4442+
success: false,
4443+
expectedReason: errorDeletedClusterServicePlanReason,
4444+
expectedErrors: []string{"Service Plan", "has been deleted"},
4445+
},
4446+
{
4447+
name: "Updating parameters works",
4448+
instance: getTestServiceInstanceUpdatingParametersOfDeletedPlan(),
4449+
class: getTestClusterServiceClass(),
4450+
plan: getTestMarkedAsRemovedClusterServicePlan(),
4451+
success: true,
4452+
},
4453+
}
4454+
4455+
for _, tc := range cases {
4456+
fakeKubeClient, fakeCatalogClient, fakeClusterServiceBrokerClient, testController, _ := newTestController(t, noFakeActions())
4457+
4458+
err := testController.checkForRemovedClassAndPlan(tc.instance, tc.class, tc.plan)
4459+
if err != nil {
4460+
if tc.success {
4461+
t.Errorf("%q: Unexpected error %v", tc.name, err)
4462+
}
4463+
for _, exp := range tc.expectedErrors {
4464+
if e, a := exp, err.Error(); !strings.Contains(a, e) {
4465+
t.Errorf("%q: Did not find expected error %q : got %q", tc.name, e, a)
4466+
}
4467+
}
4468+
} else if !tc.success {
4469+
t.Errorf("%q: Did not get a failure when expected one", tc.name)
4470+
}
4471+
4472+
// no kube or broker actions ever
4473+
assertNumberOfActions(t, fakeKubeClient.Actions(), 0)
4474+
brokerActions := fakeClusterServiceBrokerClient.Actions()
4475+
assertNumberOfClusterServiceBrokerActions(t, brokerActions, 0)
4476+
4477+
// If things succeeded, make sure no actions on the catalog client
4478+
// and if things fail, make sure instance status is updated and
4479+
// an event is generated
4480+
actions := fakeCatalogClient.Actions()
4481+
if tc.success {
4482+
assertNumberOfActions(t, actions, 0)
4483+
} else {
4484+
assertNumberOfActions(t, actions, 1)
4485+
assertUpdateStatus(t, actions[0], tc.instance)
4486+
assertServiceInstanceReadyFalse(t, tc.instance, tc.expectedReason)
4487+
events := getRecordedEvents(testController)
4488+
assertNumEvents(t, events, 1)
4489+
}
4490+
}
4491+
}

0 commit comments

Comments
 (0)