Skip to content

Commit b5c1062

Browse files
Merge pull request #116 from orange-cloudfoundry/svcat-updates
support for K8S svcat service instance updates
2 parents 6bc5eaa + 73c4f41 commit b5c1062

File tree

5 files changed

+110
-45
lines changed

5 files changed

+110
-45
lines changed

osb-cmdb/src/main/java/com/orange/oss/osbcmdb/AbstractOsbCmdbService.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
import reactor.util.Loggers;
1717

1818
import org.springframework.cloud.servicebroker.exception.ServiceBrokerException;
19-
import org.springframework.cloud.servicebroker.exception.ServiceBrokerInvalidParametersException;
20-
import org.springframework.cloud.servicebroker.model.catalog.Plan;
21-
import org.springframework.cloud.servicebroker.model.catalog.ServiceDefinition;
2219

2320
public class AbstractOsbCmdbService {
2421

@@ -66,7 +63,7 @@ protected ServiceInstance getCfServiceInstance(CloudFoundryOperations spacedTarg
6663
}
6764

6865
protected CloudFoundryOperations getSpaceScopedOperations(String spaceName) {
69-
createSpace(spaceName).block();
66+
lookupOrCreateSpace(spaceName).block();
7067

7168
//We expect the builder to reuse most of Cf Java client internal objects and thus be lightweigth
7269
return DefaultCloudFoundryOperations.builder()
@@ -85,20 +82,6 @@ protected String getSpacedIdFromTargettedOperationsInternals(CloudFoundryOperati
8582
return spaceId;
8683
}
8784

88-
protected void validateServiceDefinitionAndPlanIds(ServiceDefinition serviceDefinition, Plan plan,
89-
String serviceDefinitionId,
90-
String planId) {
91-
if (plan == null) {
92-
LOG.info("Invalid plan received with unknown id {}", planId);
93-
throw new ServiceBrokerInvalidParametersException("Invalid plan received with unknown id:" + planId);
94-
}
95-
if (serviceDefinition == null) {
96-
LOG.info("Invalid service definition received with unknown id {}", serviceDefinitionId);
97-
throw new ServiceBrokerInvalidParametersException(
98-
"Invalid service definition received with unknown id:" + serviceDefinitionId);
99-
}
100-
}
101-
10285
private Mono<Void> addSpaceDeveloperRoleForCurrentUser(String orgName, String spaceName) {
10386
return Mono.defer(() -> operations.userAdmin().setSpaceRole(SetSpaceRoleRequest.builder()
10487
.spaceRole(SpaceRole.DEVELOPER)
@@ -111,7 +94,7 @@ private Mono<Void> addSpaceDeveloperRoleForCurrentUser(String orgName, String sp
11194
.format("Error setting space developer role for space %s: %s", spaceName, e.getMessage()))));
11295
}
11396

114-
private Mono<String> createSpace(String spaceName) {
97+
private Mono<String> lookupOrCreateSpace(String spaceName) {
11598
return getSpaceId(spaceName)
11699
.switchIfEmpty(Mono.just(this.defaultOrg)
117100
.flatMap(orgName -> getOrganizationId(orgName)

osb-cmdb/src/main/java/com/orange/oss/osbcmdb/servicebinding/OsbCmdbServiceBinding.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ public Mono<CreateServiceInstanceBindingResponse> createServiceInstanceBinding(
4646
return osbInterceptor.createServiceInstanceBinding(request);
4747
}
4848

49-
//Validate mandatory service id and plan Id (even if plan is currently unused)
50-
validateServiceDefinitionAndPlanIds(request.getServiceDefinition(), request.getPlan(),
51-
request.getServiceDefinitionId(), request.getPlanId());
49+
//No need to validate mandatory service id and plan Id as sc-osb does it already
5250

5351
//Lookup corresponding service instance in the backend org to validate incoming request against security
5452
// attacks passing forged service instance guid
@@ -98,9 +96,7 @@ public Mono<DeleteServiceInstanceBindingResponse> deleteServiceInstanceBinding(
9896
return osbInterceptor.deleteServiceInstanceBinding(request);
9997
}
10098

101-
//Validate mandatory service id and plan Id (even if plan is currently unused)
102-
validateServiceDefinitionAndPlanIds(request.getServiceDefinition(), request.getPlan(),
103-
request.getServiceDefinitionId(), request.getPlanId());
99+
//No need to validate mandatory service id and plan Id as sc-osb does it already
104100

105101
//Lookup corresponding service instance to validate incoming request against security attacks passing
106102
// forged service instance guid

osb-cmdb/src/main/java/com/orange/oss/osbcmdb/serviceinstance/MaintenanceInfoFormatterService.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import org.springframework.cloud.servicebroker.exception.ServiceBrokerMaintenanceInfoConflictException;
1111
import org.springframework.cloud.servicebroker.model.catalog.MaintenanceInfo;
12+
import org.springframework.cloud.servicebroker.model.catalog.Plan;
1213
import org.springframework.cloud.servicebroker.model.instance.CreateServiceInstanceRequest;
1314
import org.springframework.cloud.servicebroker.model.instance.UpdateServiceInstanceRequest;
1415
import org.springframework.lang.Nullable;
@@ -76,7 +77,9 @@ public org.cloudfoundry.client.v2.MaintenanceInfo formatForBackendInstance(Updat
7677
.description(requestMaintenanceInfo.getDescription())
7778
.build();
7879
}
79-
MaintenanceInfo brokeredServiceMI = request.getPlan().getMaintenanceInfo();
80+
Plan requestedPlan = request.getPlan(); // may be null with svcat client, but we should have returned early
81+
// since no maintenance info change requested
82+
MaintenanceInfo brokeredServiceMI = requestedPlan.getMaintenanceInfo();
8083
MaintenanceInfo inferredBackendMI = unmergeInfos(brokeredServiceMI);
8184
if (DEFAULT_MISSING_BACKEND_MI.equals(inferredBackendMI)) {
8285
return null;
@@ -114,7 +117,7 @@ public MaintenanceInfo getOsbCmdbMaintenanceInfo() {
114117
* Indicates whether the request is a pure upgrade request `cf update-service --upgrade` resulting from a version
115118
* bump introduced by osb-cmdb, and no backend version bump.
116119
*
117-
* @return False if the update request should be passed to tbe backend broker, true if backend update should be
120+
* @return False if the update request should be passed to the backend broker, true if backend update should be
118121
* skipped (i.e. noop)
119122
*/
120123
public boolean isNoOpUpgradeBackingService(UpdateServiceInstanceRequest request) {
@@ -124,7 +127,16 @@ public boolean isNoOpUpgradeBackingService(UpdateServiceInstanceRequest request)
124127
if (!hasMaintenanceInfoChangeRequest(request)) {
125128
return false;
126129
}
127-
MaintenanceInfo brokeredServiceMI = request.getPlan().getMaintenanceInfo();
130+
Plan requestedPlan = request.getPlan();
131+
if (requestedPlan == null) {
132+
//Likely case of K8S service catalog client which is ommited service plan unless a plan upgrade is requested
133+
//Svcat does not support maintenance info upgrade requests, so we always propagate the request to the
134+
//backing service broker in this case
135+
LOG.info("Update request received without plan. Assuming req from K8S svcat client and propagating to the" +
136+
" backing broker");
137+
return false;
138+
}
139+
MaintenanceInfo brokeredServiceMI = requestedPlan.getMaintenanceInfo();
128140
MaintenanceInfo inferredBackendMI = unmergeInfos(brokeredServiceMI);
129141
return inferredBackendMI.equals(DEFAULT_MISSING_BACKEND_MI);
130142
}
@@ -133,7 +145,14 @@ public boolean isNoOpUpgradeBackingService(UpdateServiceInstanceRequest request)
133145
* Validates incoming USI request and rejects requests with unsupported maintenance info
134146
*/
135147
public void validateAnyUpgradeRequest(UpdateServiceInstanceRequest request) {
136-
MaintenanceInfo catalogMi = request.getPlan().getMaintenanceInfo();
148+
Plan requestPlan = request.getPlan();
149+
if (requestPlan == null) {
150+
//Case of a K8S svcat which is omitting the plan when plan change isn't requested.
151+
//Since no plan change is requested, we don't need to validate that the plan matches the current
152+
//plan (i.e. maintenance info match)
153+
return;
154+
}
155+
MaintenanceInfo catalogMi = requestPlan.getMaintenanceInfo();
137156
MaintenanceInfo requestedMi = request.getMaintenanceInfo();
138157
validateRequestedMaintenanceInfo(requestedMi, catalogMi);
139158
}
@@ -172,6 +191,7 @@ boolean hasMaintenanceInfoChangeRequest(UpdateServiceInstanceRequest request) {
172191
"client)");
173192
return false;
174193
}
194+
175195
if (request.getMaintenanceInfo() != null) {
176196
hasMaintenanceInfoChangeRequest =
177197
!request.getMaintenanceInfo().equals(request.getPreviousValues().getMaintenanceInfo());

osb-cmdb/src/main/java/com/orange/oss/osbcmdb/serviceinstance/OsbCmdbServiceInstance.java

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,10 @@ public Mono<CreateServiceInstanceResponse> createServiceInstance(CreateServiceIn
202202
if (osbInterceptor != null && osbInterceptor.accept(request)) {
203203
return osbInterceptor.createServiceInstance(request);
204204
}
205-
validateServiceDefinitionAndPlanIds(request.getServiceDefinition(), request.getPlan(),
206-
request.getServiceDefinitionId(), request.getPlanId());
205+
//No need to validate mandatory service id and plan Id as sc-osb does it already
206+
//See https://github.com/spring-cloud/spring-cloud-open-service-broker/blob
207+
///7d374332419aad628ca76351fd53ca7e351c57de/spring-cloud-open-service-broker-core/src/main/java/org/springframework/cloud/servicebroker/controller/ServiceInstanceController.java#L111-L112
208+
207209
maintenanceInfoFormatterService.validateAnyCreateRequest(request);
208210
String backingServiceName = request.getServiceDefinition().getName();
209211
String backingServicePlanName = request.getPlan().getName();
@@ -213,7 +215,7 @@ public Mono<CreateServiceInstanceResponse> createServiceInstance(CreateServiceIn
213215
spacedTargetedOperations = getSpaceScopedOperations(backingServiceName);
214216
//Lookup guids necessary for low level api usage, and that CloudFoundryOperations hides in its response
215217
String spaceId = getSpacedIdFromTargettedOperationsInternals(spacedTargetedOperations);
216-
String backingServicePlanId = fetchBackingServicePlanId(backingServiceName, backingServicePlanName,
218+
String backingServicePlanId = fetchRequestedBackingServicePlanId(backingServiceName, backingServicePlanName,
217219
spaceId);
218220

219221

@@ -305,9 +307,7 @@ public Mono<DeleteServiceInstanceResponse> deleteServiceInstance(DeleteServiceIn
305307
return osbInterceptor.deleteServiceInstance(request);
306308
}
307309

308-
//Validate mandatory service id and plan Id
309-
validateServiceDefinitionAndPlanIds(request.getServiceDefinition(), request.getPlan(),
310-
request.getServiceDefinitionId(), request.getPlanId());
310+
//No need to validate mandatory service id and plan Id as sc-osb does it already
311311

312312
String backingServiceInstanceName =
313313
ServiceInstanceNameHelper.truncateNameToCfMaxSize(request.getServiceInstanceId());
@@ -475,12 +475,22 @@ public Mono<UpdateServiceInstanceResponse> updateServiceInstance(UpdateServiceIn
475475
return osbInterceptor.updateServiceInstance(request);
476476
}
477477

478-
validateServiceDefinitionAndPlanIds(request.getServiceDefinition(), request.getPlan(),
479-
request.getServiceDefinitionId(), request.getPlanId());
478+
//No need to validate mandatory service id as sc-osb does it already
479+
//However since planId params optional sc-osb does not yet reject planId not matching any known plan
480+
validateServicePlanId(request.getPlan(), request.getPlanId());
481+
480482
maintenanceInfoFormatterService.validateAnyUpgradeRequest(request);
481483

482484
String backingServiceName = request.getServiceDefinition().getName();
483-
String backingServicePlanName = request.getPlan().getName();
485+
String requestedBackingServicePlanName;
486+
if (request.getPlan() != null) {
487+
//possibly receiving a plan update, or just CF CC_NG passing the current planid unchanged
488+
//in case of param update
489+
requestedBackingServicePlanName = request.getPlan().getName();
490+
} else {
491+
//Possibly a K8S svcat param update which is omitting the optional planId (as specs allows)
492+
requestedBackingServicePlanName = null;
493+
}
484494
String backingServiceInstanceName = ServiceInstanceNameHelper
485495
.truncateNameToCfMaxSize(request.getServiceInstanceId());
486496

@@ -497,7 +507,7 @@ public Mono<UpdateServiceInstanceResponse> updateServiceInstance(UpdateServiceIn
497507

498508
//Lookup guids necessary for low level api usage, and that CloudFoundryOperations hides in its response
499509
String spaceId = getSpacedIdFromTargettedOperationsInternals(spacedTargetedOperations);
500-
String backingServicePlanId = fetchBackingServicePlanId(backingServiceName, backingServicePlanName, spaceId);
510+
String requestedBackingServicePlanId = fetchRequestedBackingServicePlanId(backingServiceName, requestedBackingServicePlanName, spaceId);
501511

502512
if (maintenanceInfoFormatterService.isNoOpUpgradeBackingService(request)) {
503513
LOG.info("NoOp upgrade detected, returning early 200 OK");
@@ -531,7 +541,7 @@ public Mono<UpdateServiceInstanceResponse> updateServiceInstance(UpdateServiceIn
531541
updateServiceInstanceResponse = client.serviceInstances()
532542
.update(org.cloudfoundry.client.v2.serviceinstances.UpdateServiceInstanceRequest.builder()
533543
.serviceInstanceId(existingBackingServiceInstance.getId())
534-
.servicePlanId(backingServicePlanId)
544+
.servicePlanId(requestedBackingServicePlanId) //possibly null
535545
.parameters(formatParameters(metaData, request.getParameters()))
536546
.maintenanceInfo(formattedForBackendInstanceMI)
537547
.acceptsIncomplete(request.isAsyncAccepted())
@@ -581,6 +591,13 @@ public Mono<UpdateServiceInstanceResponse> updateServiceInstance(UpdateServiceIn
581591
return Mono.just(responseBuilder.build());
582592
}
583593

594+
protected void validateServicePlanId(Plan plan, String planId) {
595+
if (planId !=null && plan == null) {
596+
LOG.info("Invalid plan received with unknown id {}", planId);
597+
throw new ServiceBrokerInvalidParametersException("Invalid plan received with unknown id:" + planId);
598+
}
599+
}
600+
584601
protected CmdbOperationState fromJson(String operation) {
585602
try {
586603
return OBJECT_MAPPER.readValue(operation, CmdbOperationState.class);
@@ -617,8 +634,11 @@ private OperationState convertCfStateToOsbState(String cfServiceInstanceState) {
617634
}
618635
}
619636

620-
private String fetchBackingServicePlanId(String backingServiceName, String backingServicePlanName, String spaceId) {
621-
//Inspired from first instrcutions of cf-java-client:
637+
private String fetchRequestedBackingServicePlanId(String backingServiceName, String updatedBackingServicePlanName, String spaceId) {
638+
if (updatedBackingServicePlanName == null) {
639+
return null;
640+
}
641+
//Inspired from first instructions of cf-java-client:
622642
// org.cloudfoundry.operations.services.DefaultServices.createInstance()
623643
String backingServiceId = PaginationUtils
624644
.requestClientV2Resources(page -> client.spaces()
@@ -641,11 +661,11 @@ private String fetchBackingServicePlanId(String backingServiceName, String backi
641661
.page(page)
642662
.serviceId(backingServiceId)
643663
.build()))
644-
.filter(resource -> backingServicePlanName.equals(ResourceUtils.getEntity(resource).getName()))
664+
.filter(resource -> updatedBackingServicePlanName.equals(ResourceUtils.getEntity(resource).getName()))
645665
.single()
646666
.map(ResourceUtils::getId)
647667
.onErrorResume(NoSuchElementException.class,
648-
t -> ExceptionUtils.illegalArgument("Service plan %s does not exist", backingServicePlanName))
668+
t -> ExceptionUtils.illegalArgument("Service plan %s does not exist", updatedBackingServicePlanName))
649669
.block();
650670
}
651671

@@ -849,8 +869,12 @@ private Mono<UpdateServiceInstanceResponse> handleUpdateException(Exception orig
849869
.build());
850870

851871
case OsbApiConstants.LAST_OPERATION_STATE_SUCCEEDED:
872+
Plan requestedPlan = request.getPlan();
873+
String requestedPlanName = requestedPlan == null ?
874+
null :
875+
requestedPlan.getName();
852876
if (updatedSi.getService().equals(request.getServiceDefinition().getName()) &&
853-
updatedSi.getPlan().equals(request.getPlan().getName())) {
877+
updatedSi.getPlan().equals(requestedPlanName)) {
854878
LOG.info("Concurrent update request has completed. " +
855879
"Returning 200 OK");
856880
//200 OK

osb-cmdb/src/test/java/com/orange/oss/osbcmdb/serviceinstance/MaintenanceInfoFormatterServiceTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ void test_validate_upgrade_request_no_mi() {
7171
maintenanceInfoFormatterService.validateAnyUpgradeRequest(updateServiceInstanceRequest);
7272
}
7373

74+
@DisplayName("validates upgrade requests include valid maintenance info (when no plan specified)")
75+
@Test
76+
void test_validate_upgrade_request_no_plan_specified() {
77+
//Given
78+
MaintenanceInfoFormatterService maintenanceInfoFormatterService = new MaintenanceInfoFormatterService(
79+
anOsbCmdbInfoV1());
80+
UpdateServiceInstanceRequest updateServiceInstanceRequest = UpdateServiceInstanceRequest.builder()
81+
.build();
82+
maintenanceInfoFormatterService.validateAnyUpgradeRequest(updateServiceInstanceRequest);
83+
}
84+
7485
@DisplayName("validates upgrade requests include valid maintenance info")
7586
@Test
7687
void test_validate_create_request() {
@@ -234,6 +245,22 @@ void test_isNoOpUpgradeBackingService_with_params() {
234245
assertThat(isNoOpUpgradeBackingService).isFalse();
235246
}
236247

248+
@DisplayName("an update request should NOT be a noop, when params requested (no plan update), even if backend has" +
249+
" no MI")
250+
@Test
251+
void test_isNoOpUpgradeBackingService_with_params_without_plan() {
252+
//Given
253+
MaintenanceInfoFormatterService maintenanceInfoFormatterService = new MaintenanceInfoFormatterService(
254+
anOsbCmdbInfoV1());
255+
UpdateServiceInstanceRequest updateServiceInstanceRequest = UpdateServiceInstanceRequest.builder()
256+
.parameters(Collections.singletonMap("a-key", "a-value"))
257+
.build();
258+
//when
259+
boolean isNoOpUpgradeBackingService = maintenanceInfoFormatterService
260+
.isNoOpUpgradeBackingService(updateServiceInstanceRequest);
261+
assertThat(isNoOpUpgradeBackingService).isFalse();
262+
}
263+
237264
@DisplayName("an update request should NOT be a noop, when plan update requested, even if backend has no MI")
238265
@Test
239266
void test_isNoOpUpgradeBackingService_with_plan_update() {
@@ -253,6 +280,21 @@ void test_isNoOpUpgradeBackingService_with_plan_update() {
253280
assertThat(isNoOpUpgradeBackingService).isFalse();
254281
}
255282

283+
@DisplayName("Formats the request to send to backend service : case of a client K8S svcat request without MI/ plan")
284+
@Test
285+
void test_formatForBackendInstance_case0() {
286+
//given an osb-cmdb configured with a bump
287+
MaintenanceInfoFormatterService maintenanceInfoFormatterService = new MaintenanceInfoFormatterService(
288+
anOsbCmdbInfoV1());
289+
//when receiving a request made by a client from catalog
290+
org.cloudfoundry.client.v2.MaintenanceInfo requestBackendServiceMi = maintenanceInfoFormatterService
291+
.formatForBackendInstance(UpdateServiceInstanceRequest.builder()
292+
.build());
293+
//then no MI is passed to backend
294+
assertThat(requestBackendServiceMi).isNull();
295+
//actually this should previously be detected as a noop and update should not be requested to backend
296+
}
297+
256298
@DisplayName("Formats the request to send to backend service : case of backend service has no Mi and osb-cmdb " +
257299
"bump " +
258300
"(e.g. cf-mysql")

0 commit comments

Comments
 (0)