Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.cloudfoundry.multiapps.controller.core.model.DynamicResolvableParameter;
import org.cloudfoundry.multiapps.controller.core.model.ImmutableDynamicResolvableParameter;
import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters;
import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor;
import org.cloudfoundry.multiapps.mta.model.Resource;
import org.cloudfoundry.multiapps.mta.util.DynamicParameterUtil;
Expand All @@ -34,6 +35,7 @@ private void addDynamicResolvableParameter(Set<DynamicResolvableParameter> dynam

addDynamicResolvableParameter(dynamicResolvableParameters, parametersStringValues);
addDynamicResolvableParameter(dynamicResolvableParameters, propertiesStringValues);
addDynamicResolvableParametersFromServiceKeysConfig(dynamicResolvableParameters, resource.getParameters());
}

private List<String> getStringValueParameters(Map<String, Object> parameters) {
Expand All @@ -58,4 +60,29 @@ private void addDynamicResolvableParameter(Set<DynamicResolvableParameter> dynam
}
}

private void addDynamicResolvableParametersFromServiceKeysConfig(Set<DynamicResolvableParameter> dynamicResolvableParameters,
Map<String, Object> parameters) {
Object serviceKeys = parameters.get(SupportedParameters.SERVICE_KEYS);
if (!(serviceKeys instanceof List<?> serviceKeysList)) {
return;
}
for (Object serviceKey : serviceKeysList) {
if (serviceKey instanceof Map<?, ?> serviceKeyMap) {
addDynamicResolvableParametersFromConfig(dynamicResolvableParameters, serviceKeyMap.get(SupportedParameters.SERVICE_KEY_CONFIG));
}
}
}

private void addDynamicResolvableParametersFromConfig(Set<DynamicResolvableParameter> dynamicResolvableParameters, Object config) {
if (!(config instanceof Map<?, ?> configMap)) {
return;
}
List<String> configStringValues = configMap.values()
.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.collect(Collectors.toList());
addDynamicResolvableParameter(dynamicResolvableParameters, configStringValues);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,29 @@ void testParametersResolveDepthIsAlwaysOne() {
assertTrue(resultParameters.isEmpty(), MessageFormat.format("Expected empty Set but contains \"{0}\"", resultParameters));
}

@Test
void testDetectionOfServiceKeyCredentialReferenceInServiceKeysConfig() {
DynamicResolvableParameter expectedParameter = ImmutableDynamicResolvableParameter.builder()
.parameterName("service-keys.ztis-key.credentials.issuer")
.relationshipEntityName("ztis-config")
.build();

DeploymentDescriptor descriptor = DeploymentDescriptor.createV3()
.setId("test-id")
.setResources(List.of(Resource.createV3()
.setName("caas-v2")
.setParameters(Map.of("service-keys",
List.of(Map.of("name",
"caas-v2-key",
"config",
Map.of("issuer",
"{ds/ztis-config/service-keys.ztis-key.credentials.issuer}")))))));

DynamicResolvableParametersFactory factory = new DynamicResolvableParametersFactory(descriptor);
Set<DynamicResolvableParameter> resultParameters = factory.create();

assertTrue(resultParameters.contains(expectedParameter),
MessageFormat.format("Expected parameter \"{0}\" was not found in the Set \"{1}\"", expectedParameter, resultParameters));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ public class Messages {
public static final String PARSE_NULL_STRING_ERROR = "Cannot parse null string";
public static final String INVALID_BOOLEAN_VALUE = "Invalid boolean value: must be 'true' or 'false'";
public static final String DISABLE_AUTOSCALER_LABEL_CONTENT = "Disabled_by_MTA_operation_{0}";
public static final String UNABLE_TO_RESOLVE_SERVICE_KEY_CREDENTIAL_REFERENCE = "Unable to resolve service key credential reference \"{0}\": field \"{1}\" was not found in the credentials of service key \"{2}\" of service instance \"{3}\"";
public static final String ERROR_RESOLVING_BINDING_CREDENTIAL_REFERENCES = "Error while resolving service key credential references in service binding parameters";

protected Messages() {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.cloudfoundry.multiapps.controller.process.steps;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.inject.Named;
import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient;
import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended;
import org.cloudfoundry.multiapps.controller.process.Messages;
import org.cloudfoundry.multiapps.controller.process.util.ServiceKeyCredentialResolver;
import org.cloudfoundry.multiapps.controller.process.variables.Variables;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;

@Named("resolveBindingCredentialReferencesStep")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ResolveBindingCredentialReferencesStep extends SyncFlowableStep {

@Override
protected StepPhase executeStep(ProcessContext context) {
Map<String, Object> bindingParameters = context.getVariable(Variables.SERVICE_BINDING_PARAMETERS);
if (bindingParameters == null || bindingParameters.isEmpty()) {
return StepPhase.DONE;
}
CloudControllerClient client = context.getControllerClient();
Map<String, String> serviceInstanceNamesByRelationship = buildServiceInstanceNamesByRelationship(context);
ServiceKeyCredentialResolver resolver = new ServiceKeyCredentialResolver(client, serviceInstanceNamesByRelationship);
Map<String, Object> resolvedBindingParameters = resolver.resolveReferencesInMap(bindingParameters);

context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, resolvedBindingParameters);
return StepPhase.DONE;
}

private Map<String, String> buildServiceInstanceNamesByRelationship(ProcessContext context) {
Map<String, String> serviceInstanceNamesByRelationship = new HashMap<>();
List<CloudServiceInstanceExtended> servicesToBind = context.getVariable(Variables.SERVICES_TO_BIND);
if (servicesToBind == null) {
return serviceInstanceNamesByRelationship;
}
for (CloudServiceInstanceExtended service : servicesToBind) {
serviceInstanceNamesByRelationship.put(service.getResourceName(), service.getName());
}
return serviceInstanceNamesByRelationship;
}

@Override
protected String getStepErrorMessage(ProcessContext context) {
return Messages.ERROR_RESOLVING_BINDING_CREDENTIAL_REFERENCES;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.cloudfoundry.multiapps.controller.process.steps;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

import jakarta.inject.Named;
import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient;
import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey;
import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters;
import org.cloudfoundry.multiapps.controller.process.Messages;
import org.cloudfoundry.multiapps.controller.process.util.ServiceKeyCredentialResolver;
import org.cloudfoundry.multiapps.controller.process.variables.Variables;
import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor;
import org.cloudfoundry.multiapps.mta.model.Resource;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;

@Named("resolveServiceKeyCredentialReferencesStep")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ResolveServiceKeyCredentialReferencesStep extends SyncFlowableStep {

@Override
protected StepPhase executeStep(ProcessContext context) {
CloudServiceKey serviceKeyToProcess = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS);
CloudControllerClient client = context.getControllerClient();

Map<String, String> serviceInstanceNamesByRelationship = buildServiceInstanceNamesByRelationship(context);
ServiceKeyCredentialResolver resolver = new ServiceKeyCredentialResolver(client, serviceInstanceNamesByRelationship);
CloudServiceKey resolvedServiceKey = resolver.resolve(serviceKeyToProcess);

context.setVariable(Variables.SERVICE_KEY_TO_PROCESS, resolvedServiceKey);
return StepPhase.DONE;
}

private Map<String, String> buildServiceInstanceNamesByRelationship(ProcessContext context) {
DeploymentDescriptor descriptor = context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR);
Map<String, String> serviceInstanceNamesByRelationship = new HashMap<>();
if (descriptor == null) {
return serviceInstanceNamesByRelationship;
}
for (Resource resource : descriptor.getResources()) {
Object serviceName = resource.getParameters()
.get(SupportedParameters.SERVICE_NAME);
serviceInstanceNamesByRelationship.put(resource.getName(),
serviceName instanceof String ? (String) serviceName : resource.getName());
}
return serviceInstanceNamesByRelationship;
}

@Override
protected String getStepErrorMessage(ProcessContext context) {
CloudServiceKey serviceKeyToProcess = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS);
return MessageFormat.format(Messages.ERROR_WHILE_CREATING_SERVICE_KEY_0, serviceKeyToProcess.getName());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.cloudfoundry.multiapps.controller.process.util;

import java.text.MessageFormat;
import java.util.LinkedHashMap;
import java.util.Map;

import org.cloudfoundry.multiapps.common.ContentException;
import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient;
import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey;
import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey;
import org.cloudfoundry.multiapps.controller.process.Messages;
import org.cloudfoundry.multiapps.mta.util.DynamicParameterUtil;

/**
* Resolves {@code {ds/<resource>/service-keys.<key>.credentials.<field>}} references found in a service key's credentials by reading the
* referenced (already created) service key's credentials from the Cloud Controller.
*/
public class ServiceKeyCredentialResolver {

private final CloudControllerClient client;
private final Map<String, String> serviceInstanceNamesByRelationship;

public ServiceKeyCredentialResolver(CloudControllerClient client, Map<String, String> serviceInstanceNamesByRelationship) {
this.client = client;
this.serviceInstanceNamesByRelationship = serviceInstanceNamesByRelationship;
}

public CloudServiceKey resolve(CloudServiceKey serviceKey) {
Map<String, Object> credentials = serviceKey.getCredentials();
if (credentials == null || credentials.isEmpty()) {
return serviceKey;
}
Map<String, Object> resolvedCredentials = resolveReferencesInMap(credentials);
if (resolvedCredentials == credentials) {
return serviceKey;
}
return ImmutableCloudServiceKey.copyOf(serviceKey)
.withCredentials(resolvedCredentials);
}

/**
* Resolves {@code {ds/...}} service-key credential references in an arbitrary map (e.g. a module's service-binding config), returning a
* new map with the resolved values. Returns the same map instance when there is nothing to resolve.
*/
public Map<String, Object> resolveReferencesInMap(Map<String, Object> values) {
if (values == null || values.isEmpty()) {
return values;
}
Map<String, Object> resolved = new LinkedHashMap<>(values);
boolean changed = false;
for (Map.Entry<String, Object> entry : values.entrySet()) {
if (!isServiceKeyCredentialReference(entry.getValue())) {
continue;
}
resolved.put(entry.getKey(), resolveReference((String) entry.getValue()));
changed = true;
}
return changed ? resolved : values;
}

private boolean isServiceKeyCredentialReference(Object value) {
if (!(value instanceof String stringValue) || !stringValue.matches(DynamicParameterUtil.REGEX_PATTERN_FOR_DYNAMIC_PARAMETERS)) {
return false;
}
return DynamicParameterUtil.isServiceKeyCredentialReference(DynamicParameterUtil.getParameterName(stringValue));
}

private Object resolveReference(String reference) {
String relationshipName = DynamicParameterUtil.getRelationshipName(reference);
String parameterName = DynamicParameterUtil.getParameterName(reference);
String serviceKeyName = DynamicParameterUtil.getServiceKeyName(parameterName);
String credentialFieldPath = DynamicParameterUtil.getCredentialFieldPath(parameterName);

String serviceInstanceName = serviceInstanceNamesByRelationship.getOrDefault(relationshipName, relationshipName);
CloudServiceKey referencedKey = client.getServiceKey(serviceInstanceName, serviceKeyName);
return extractCredentialField(reference, referencedKey, serviceInstanceName, serviceKeyName, credentialFieldPath);
}

private Object extractCredentialField(String reference, CloudServiceKey referencedKey, String serviceInstanceName,
String serviceKeyName, String credentialFieldPath) {
Object current = referencedKey == null ? null : referencedKey.getCredentials();
for (String field : credentialFieldPath.split("\\.")) {
if (!(current instanceof Map<?, ?> currentMap) || !currentMap.containsKey(field)) {
throw new ContentException(MessageFormat.format(Messages.UNABLE_TO_RESOLVE_SERVICE_KEY_CREDENTIAL_REFERENCE, reference,
credentialFieldPath, serviceKeyName, serviceInstanceName));
}
current = currentMap.get(field);
}
return current;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
</intermediateCatchEvent>
<sequenceFlow id="sid-B3A86402-0BC1-465A-BAD2-93829333DC41" sourceRef="sid-F4EBBC99-8981-4446-AC1B-C0784AE40B43" targetRef="checkForServiceKeyOperationsInProgress"></sequenceFlow>
<serviceTask id="createServiceKey" name="Create Service Key" flowable:async="true" flowable:delegateExpression="${createServiceKeyStep}"></serviceTask>
<serviceTask id="resolveServiceKeyCredentialReferences" name="Resolve Service Key Credential References" flowable:async="true" flowable:delegateExpression="${resolveServiceKeyCredentialReferencesStep}"></serviceTask>
<sequenceFlow id="sid-RESOLVE-TO-CREATE" sourceRef="resolveServiceKeyCredentialReferences" targetRef="createServiceKey"></sequenceFlow>
<endEvent id="sid-9EDD5B98-B265-45B1-A803-875D219B57A4" name="End"></endEvent>
<exclusiveGateway id="sid-72D65FFE-5C1F-4F0D-ABE2-8BD2677A02CB" default="sid-7B00943B-08BB-4B8B-AE9C-119EE95559E8"></exclusiveGateway>
<intermediateCatchEvent id="sid-4E70F863-226F-436B-BE8A-FF9E5E9A037C">
Expand All @@ -34,7 +36,7 @@
</sequenceFlow>
<sequenceFlow id="sid-7C5012B6-BC95-492E-A10D-C5B71F0D540C" sourceRef="createServiceKey" targetRef="sid-72D65FFE-5C1F-4F0D-ABE2-8BD2677A02CB"></sequenceFlow>
<sequenceFlow id="sid-7B00943B-08BB-4B8B-AE9C-119EE95559E8" sourceRef="sid-72D65FFE-5C1F-4F0D-ABE2-8BD2677A02CB" targetRef="sid-9EDD5B98-B265-45B1-A803-875D219B57A4"></sequenceFlow>
<sequenceFlow id="sid-CC4F8D6C-48CB-4F88-B40C-651021CB216E" sourceRef="sid-FBA43AEE-A382-4C41-99A4-B4854C534ED7" targetRef="createServiceKey"></sequenceFlow>
<sequenceFlow id="sid-CC4F8D6C-48CB-4F88-B40C-651021CB216E" sourceRef="sid-FBA43AEE-A382-4C41-99A4-B4854C534ED7" targetRef="resolveServiceKeyCredentialReferences"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_createServiceKeySubProcess">
<bpmndi:BPMNPlane bpmnElement="createServiceKeySubProcess" id="BPMNPlane_createServiceKeySubProcess">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<startEvent id="startEvent" name="Start" flowable:formFieldValidation="true"></startEvent>
<serviceTask id="unbindServiceFromApplicationTask" name="Unbind Service from Application" flowable:async="true" flowable:delegateExpression="${unbindServiceFromApplicationStep}"></serviceTask>
<serviceTask id="bindServiceToApplicationTask" name="Bind Service to Application" flowable:async="true" flowable:delegateExpression="${bindServiceToApplicationStep}"></serviceTask>
<serviceTask id="resolveBindingCredentialReferencesTask" name="Resolve Binding Credential References" flowable:async="true" flowable:delegateExpression="${resolveBindingCredentialReferencesStep}"></serviceTask>
<sequenceFlow id="sid-RESOLVE-BINDING-TO-BIND" sourceRef="resolveBindingCredentialReferencesTask" targetRef="bindServiceToApplicationTask"></sequenceFlow>
<endEvent id="endEvent" name="End">
<extensionElements>
<flowable:executionListener event="end" delegateExpression="${manageAppServiceBindingEndListener}"></flowable:executionListener>
Expand All @@ -23,7 +25,7 @@
<sequenceFlow id="sid-8FD6B60A-3F5B-4339-B958-7F983A3C6E64" sourceRef="sid-AB2AFF59-667E-4260-BCB5-D958FBF901A1" targetRef="unbindServiceFromApplicationTask">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${shouldUnbindServiceFromApp}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-297C30EF-2F6E-42A7-AE45-F3C6C975DF88" sourceRef="sid-3DD45EF2-BB87-406C-98BC-507B59E921D8" targetRef="bindServiceToApplicationTask">
<sequenceFlow id="sid-297C30EF-2F6E-42A7-AE45-F3C6C975DF88" sourceRef="sid-3DD45EF2-BB87-406C-98BC-507B59E921D8" targetRef="resolveBindingCredentialReferencesTask">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${shouldBindServiceToApp}]]></conditionExpression>
</sequenceFlow>
<serviceTask id="checkForServiceBindingOperationInProgress" name="Check For Service Binding Operation In Progress" flowable:async="true" flowable:delegateExpression="${checkServiceBindingOperationStep}"></serviceTask>
Expand Down
Loading
Loading