Repository navigation
Feature/dynamic service key 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
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,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()); | ||
| } | ||
|
Comment on lines
+41
to
+43
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. In the other method you also check for serviceName. Don't we need there the same validation? |
||
| 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); | ||
|
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. Is it requierd to have a validation if serviceKeyToProcess is set or we will go in this step only if its set? |
||
| 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; | ||
| } | ||
|
Comment on lines
+49
to
+57
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't we just return values if nothing was changed and work with the values map instead of creating a new one resolved? |
||
| 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); | ||
|
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? |
||
| 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.
This method and teh method in ResolveServiceKeyCredentialReferencesStep are doing almost the same thing. Can you somehow combine them?