Repository navigation
Feature/dynamic service binding credentials #1929
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a5b9bc6
f7fb784
12700f7
5fe8070
0a3d993
be3a124
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.cloudfoundry.multiapps.controller.process.steps; | ||
|
|
||
| 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(); | ||
| List<CloudServiceInstanceExtended> servicesToBind = context.getVariable(Variables.SERVICES_TO_BIND); | ||
| Map<String, String> serviceInstanceNamesByRelationship = ServiceKeyCredentialResolver.buildServiceInstanceNamesFromServicesToBind(servicesToBind); | ||
| ServiceKeyCredentialResolver resolver = new ServiceKeyCredentialResolver(client, serviceInstanceNamesByRelationship); | ||
| Map<String, Object> resolvedBindingParameters = resolver.resolveReferencesInMap(bindingParameters); | ||
|
|
||
| context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, resolvedBindingParameters); | ||
| return StepPhase.DONE; | ||
| } | ||
|
|
||
| @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,40 @@ | ||
| package org.cloudfoundry.multiapps.controller.process.steps; | ||
|
|
||
| import java.text.MessageFormat; | ||
| 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.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.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(); | ||
|
|
||
| DeploymentDescriptor descriptor = context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR); | ||
| Map<String, String> serviceInstanceNamesByRelationship = ServiceKeyCredentialResolver.buildServiceInstanceNamesFromDescriptor(descriptor); | ||
| ServiceKeyCredentialResolver resolver = new ServiceKeyCredentialResolver(client, serviceInstanceNamesByRelationship); | ||
| CloudServiceKey resolvedServiceKey = resolver.resolve(serviceKeyToProcess); | ||
|
|
||
| context.setVariable(Variables.SERVICE_KEY_TO_PROCESS, resolvedServiceKey); | ||
| return StepPhase.DONE; | ||
| } | ||
|
|
||
| @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,124 @@ | ||
| package org.cloudfoundry.multiapps.controller.process.util; | ||
|
|
||
| import java.text.MessageFormat; | ||
| import java.util.HashMap; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| 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.client.lib.domain.CloudServiceInstanceExtended; | ||
| import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; | ||
| import org.cloudfoundry.multiapps.controller.process.Messages; | ||
| import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; | ||
| import org.cloudfoundry.multiapps.mta.model.Resource; | ||
| 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 static Map<String, String> buildServiceInstanceNamesFromDescriptor(DeploymentDescriptor descriptor) { | ||
| Map<String, String> serviceInstanceNames = new HashMap<>(); | ||
| if (descriptor == null) { | ||
| return serviceInstanceNames; | ||
| } | ||
| for (Resource resource : descriptor.getResources()) { | ||
| Object serviceName = resource.getParameters() | ||
| .get(SupportedParameters.SERVICE_NAME); | ||
| serviceInstanceNames.put(resource.getName(), | ||
| serviceName instanceof String ? (String) serviceName : resource.getName()); | ||
| } | ||
| return serviceInstanceNames; | ||
| } | ||
|
|
||
| public static Map<String, String> buildServiceInstanceNamesFromServicesToBind(List<CloudServiceInstanceExtended> servicesToBind) { | ||
| Map<String, String> serviceInstanceNames = new HashMap<>(); | ||
| if (servicesToBind == null) { | ||
| return serviceInstanceNames; | ||
| } | ||
| for (CloudServiceInstanceExtended service : servicesToBind) { | ||
| serviceInstanceNames.put(service.getResourceName(), service.getName()); | ||
| } | ||
| return serviceInstanceNames; | ||
| } | ||
|
|
||
| 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 = null; | ||
| for (Map.Entry<String, Object> entry : values.entrySet()) { | ||
| if (!isServiceKeyCredentialReference(entry.getValue())) { | ||
| continue; | ||
| } | ||
| if (resolved == null) { | ||
| resolved = new LinkedHashMap<>(values); | ||
| } | ||
| resolved.put(entry.getKey(), resolveReference((String) entry.getValue())); | ||
| } | ||
| return resolved != null ? 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you get the service key from the flowable context so we don't have to make extra API calls?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API call is necessary here. |
||
| 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; | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it requierd to have a validation if serviceKeyToProcess is set or we will go in this step only if its set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. Per the BPMN flow in
create-service-key.bpmn,resolveServiceKeyCredentialReferencesis on the default (non-POLL) path andSERVICE_KEY_TO_PROCESSis always set before this step runs. Other steps in the same subprocess (CreateServiceKeyStep,DeleteServiceKeyStep) follow the same pattern without a null-check.