diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactory.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactory.java index f590ef4fb3..b033b8692b 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactory.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactory.java @@ -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; @@ -34,6 +35,7 @@ private void addDynamicResolvableParameter(Set dynam addDynamicResolvableParameter(dynamicResolvableParameters, parametersStringValues); addDynamicResolvableParameter(dynamicResolvableParameters, propertiesStringValues); + addDynamicResolvableParametersFromServiceKeysConfig(dynamicResolvableParameters, resource.getParameters()); } private List getStringValueParameters(Map parameters) { @@ -58,4 +60,29 @@ private void addDynamicResolvableParameter(Set dynam } } + private void addDynamicResolvableParametersFromServiceKeysConfig(Set dynamicResolvableParameters, + Map 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 dynamicResolvableParameters, Object config) { + if (!(config instanceof Map configMap)) { + return; + } + List configStringValues = configMap.values() + .stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .collect(Collectors.toList()); + addDynamicResolvableParameter(dynamicResolvableParameters, configStringValues); + } + } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactoryTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactoryTest.java index c8b0aa225e..d522fe4ce3 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactoryTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/v3/DynamicResolvableParametersFactoryTest.java @@ -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 resultParameters = factory.create(); + + assertTrue(resultParameters.contains(expectedParameter), + MessageFormat.format("Expected parameter \"{0}\" was not found in the Set \"{1}\"", expectedParameter, resultParameters)); + } + } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java index 86ef2d9b56..8ee26944e7 100755 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java @@ -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() { } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStep.java new file mode 100644 index 0000000000..e334d3626f --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStep.java @@ -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 bindingParameters = context.getVariable(Variables.SERVICE_BINDING_PARAMETERS); + if (bindingParameters == null || bindingParameters.isEmpty()) { + return StepPhase.DONE; + } + CloudControllerClient client = context.getControllerClient(); + Map serviceInstanceNamesByRelationship = buildServiceInstanceNamesByRelationship(context); + ServiceKeyCredentialResolver resolver = new ServiceKeyCredentialResolver(client, serviceInstanceNamesByRelationship); + Map resolvedBindingParameters = resolver.resolveReferencesInMap(bindingParameters); + + context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, resolvedBindingParameters); + return StepPhase.DONE; + } + + private Map buildServiceInstanceNamesByRelationship(ProcessContext context) { + Map serviceInstanceNamesByRelationship = new HashMap<>(); + List 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; + } + +} \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStep.java new file mode 100644 index 0000000000..8198453c5b --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStep.java @@ -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 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 buildServiceInstanceNamesByRelationship(ProcessContext context) { + DeploymentDescriptor descriptor = context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR); + Map 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()); + } + +} \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolver.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolver.java new file mode 100644 index 0000000000..3489119580 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolver.java @@ -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//service-keys..credentials.}} 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 serviceInstanceNamesByRelationship; + + public ServiceKeyCredentialResolver(CloudControllerClient client, Map serviceInstanceNamesByRelationship) { + this.client = client; + this.serviceInstanceNamesByRelationship = serviceInstanceNamesByRelationship; + } + + public CloudServiceKey resolve(CloudServiceKey serviceKey) { + Map credentials = serviceKey.getCredentials(); + if (credentials == null || credentials.isEmpty()) { + return serviceKey; + } + Map 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 resolveReferencesInMap(Map values) { + if (values == null || values.isEmpty()) { + return values; + } + Map resolved = new LinkedHashMap<>(values); + boolean changed = false; + for (Map.Entry 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; + } + +} \ No newline at end of file diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/create-service-key.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/create-service-key.bpmn index 47297a3fd3..f45958c2b6 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/create-service-key.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/create-service-key.bpmn @@ -16,6 +16,8 @@ + + @@ -34,7 +36,7 @@ - + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/manage-app-service-bindings.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/manage-app-service-bindings.bpmn index 1d37925af6..b0b0ad3c11 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/manage-app-service-bindings.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/manage-app-service-bindings.bpmn @@ -9,6 +9,8 @@ + + @@ -23,7 +25,7 @@ - + diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStepTest.java new file mode 100644 index 0000000000..4c1a616bcd --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveBindingCredentialReferencesStepTest.java @@ -0,0 +1,87 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; +import java.util.Map; + +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.ImmutableCloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResolveBindingCredentialReferencesStepTest extends SyncFlowableStepTest { + + private static final String REFERENCED_RESOURCE = "ztis-config"; + private static final String REFERENCED_SERVICE_INSTANCE = "ztis-config"; + private static final String REFERENCED_KEY = "ztis-key"; + + @Test + void testResolvesCredentialReferenceInBindingParameters() { + context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, + Map.of("issuer", + "{ds/" + REFERENCED_RESOURCE + "/service-keys." + REFERENCED_KEY + ".credentials.issuer}", + "app-identifier", "my-app-identifier")); + Mockito.when(client.getServiceKey(REFERENCED_SERVICE_INSTANCE, REFERENCED_KEY)) + .thenReturn(buildKey(REFERENCED_KEY, REFERENCED_SERVICE_INSTANCE, Map.of("issuer", "CN=the-issuer"))); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + Map resolved = context.getVariable(Variables.SERVICE_BINDING_PARAMETERS); + assertEquals("CN=the-issuer", resolved.get("issuer")); + assertEquals("my-app-identifier", resolved.get("app-identifier")); + } + + @Test + void testNoOpWhenNoReferences() { + context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, Map.of("app-identifier", "my-app-identifier")); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + Map resolved = context.getVariable(Variables.SERVICE_BINDING_PARAMETERS); + assertEquals("my-app-identifier", resolved.get("app-identifier")); + Mockito.verify(client, Mockito.never()) + .getServiceKey(Mockito.anyString(), Mockito.anyString()); + } + + @Test + void testUsesReferencedResourceServiceNameForReadBack() { + String referencedResourceName = "ztis-resource"; + String referencedServiceInstanceName = "custom-ztis-service-name"; + context.setVariable(Variables.SERVICES_TO_BIND, + List.of(ImmutableCloudServiceInstanceExtended.builder() + .resourceName(referencedResourceName) + .name(referencedServiceInstanceName) + .build())); + context.setVariable(Variables.SERVICE_BINDING_PARAMETERS, + Map.of("issuer", + "{ds/" + referencedResourceName + "/service-keys." + REFERENCED_KEY + ".credentials.issuer}")); + Mockito.when(client.getServiceKey(referencedServiceInstanceName, REFERENCED_KEY)) + .thenReturn(buildKey(REFERENCED_KEY, referencedServiceInstanceName, Map.of("issuer", "CN=the-issuer"))); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + Map resolved = context.getVariable(Variables.SERVICE_BINDING_PARAMETERS); + assertEquals("CN=the-issuer", resolved.get("issuer")); + } + + private CloudServiceKey buildKey(String keyName, String serviceInstanceName, Map credentials) { + return ImmutableCloudServiceKey.builder() + .name(keyName) + .credentials(credentials) + .serviceInstance(ImmutableCloudServiceInstanceExtended.builder() + .name(serviceInstanceName) + .build()) + .build(); + } + + @Override + protected ResolveBindingCredentialReferencesStep createStep() { + return new ResolveBindingCredentialReferencesStep(); + } +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStepTest.java new file mode 100644 index 0000000000..18c8042463 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveServiceKeyCredentialReferencesStepTest.java @@ -0,0 +1,98 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; +import java.util.Map; + +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.ImmutableCloudServiceInstanceExtended; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResolveServiceKeyCredentialReferencesStepTest extends SyncFlowableStepTest { + + private static final String REFERENCED_RESOURCE = "ztis-config"; + private static final String REFERENCED_SERVICE_INSTANCE = "ztis-config"; + private static final String REFERENCED_KEY = "ztis-key"; + + @Test + void testResolvesCredentialReferenceInServiceKeyToProcess() { + CloudServiceKey keyToProcess = buildKey("caas-v2-key", "caas-v2", + Map.of("issuer", + "{ds/" + REFERENCED_SERVICE_INSTANCE + "/service-keys." + REFERENCED_KEY + + ".credentials.issuer}")); + context.setVariable(Variables.SERVICE_KEY_TO_PROCESS, keyToProcess); + Mockito.when(client.getServiceKey(REFERENCED_SERVICE_INSTANCE, REFERENCED_KEY)) + .thenReturn(buildKey(REFERENCED_KEY, REFERENCED_SERVICE_INSTANCE, Map.of("issuer", "CN=the-issuer"))); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + CloudServiceKey resolved = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS); + assertEquals("CN=the-issuer", resolved.getCredentials() + .get("issuer")); + } + + @Test + void testNoOpWhenNoCredentialReferences() { + CloudServiceKey keyToProcess = buildKey("caas-v2-key", "caas-v2", Map.of("app-identifier", "caas-coll-client")); + context.setVariable(Variables.SERVICE_KEY_TO_PROCESS, keyToProcess); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + CloudServiceKey resolved = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS); + assertEquals("caas-coll-client", resolved.getCredentials() + .get("app-identifier")); + Mockito.verify(client, Mockito.never()) + .getServiceKey(Mockito.anyString(), Mockito.anyString()); + } + + @Test + void testUsesReferencedResourceServiceNameForReadBack() { + String referencedResourceName = "ztis-resource"; + String referencedServiceInstanceName = "custom-ztis-service-name"; + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3() + .setId("test-id") + .setResources(List.of(Resource.createV3() + .setName(referencedResourceName) + .setParameters(Map.of(SupportedParameters.SERVICE_NAME, + referencedServiceInstanceName)))); + context.setVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + CloudServiceKey keyToProcess = buildKey("caas-v2-key", "caas-v2", + Map.of("issuer", + "{ds/" + referencedResourceName + "/service-keys." + REFERENCED_KEY + + ".credentials.issuer}")); + context.setVariable(Variables.SERVICE_KEY_TO_PROCESS, keyToProcess); + Mockito.when(client.getServiceKey(referencedServiceInstanceName, REFERENCED_KEY)) + .thenReturn(buildKey(REFERENCED_KEY, referencedServiceInstanceName, Map.of("issuer", "CN=the-issuer"))); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + CloudServiceKey resolved = context.getVariable(Variables.SERVICE_KEY_TO_PROCESS); + assertEquals("CN=the-issuer", resolved.getCredentials() + .get("issuer")); + } + + private CloudServiceKey buildKey(String keyName, String serviceInstanceName, Map credentials) { + return ImmutableCloudServiceKey.builder() + .name(keyName) + .credentials(credentials) + .serviceInstance(ImmutableCloudServiceInstanceExtended.builder() + .name(serviceInstanceName) + .build()) + .build(); + } + + @Override + protected ResolveServiceKeyCredentialReferencesStep createStep() { + return new ResolveServiceKeyCredentialReferencesStep(); + } +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolverTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolverTest.java new file mode 100644 index 0000000000..e0b19874cb --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ServiceKeyCredentialResolverTest.java @@ -0,0 +1,112 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +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.facade.domain.ImmutableServiceCredentialBindingOperation; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ServiceCredentialBindingOperation; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class ServiceKeyCredentialResolverTest { + + private static final String REFERENCED_RESOURCE = "ztis-config"; + private static final String REFERENCED_SERVICE_INSTANCE = "ztis-config-instance"; + private static final String REFERENCED_KEY = "ztis-key"; + + @Mock + private CloudControllerClient client; + + private ServiceKeyCredentialResolver resolver; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + resolver = new ServiceKeyCredentialResolver(client, Map.of(REFERENCED_RESOURCE, REFERENCED_SERVICE_INSTANCE)); + } + + @Test + void testResolvesServiceKeyCredentialReference() { + Mockito.when(client.getServiceKey(REFERENCED_SERVICE_INSTANCE, REFERENCED_KEY)) + .thenReturn(buildReferencedKey(Map.of("issuer", "CN=the-issuer"))); + CloudServiceKey keyToResolve = buildKeyWithCredentials(Map.of("issuer", + "{ds/" + REFERENCED_RESOURCE + "/service-keys." + REFERENCED_KEY + + ".credentials.issuer}")); + + CloudServiceKey resolved = resolver.resolve(keyToResolve); + + Assertions.assertEquals("CN=the-issuer", resolved.getCredentials() + .get("issuer")); + } + + @Test + void testResolvesReferencesInMap() { + Mockito.when(client.getServiceKey(REFERENCED_SERVICE_INSTANCE, REFERENCED_KEY)) + .thenReturn(buildReferencedKey(Map.of("issuer", "CN=the-issuer"))); + Map bindingConfig = Map.of("issuer", + "{ds/" + REFERENCED_RESOURCE + "/service-keys." + REFERENCED_KEY + + ".credentials.issuer}", + "static", "unchanged"); + + Map resolved = resolver.resolveReferencesInMap(bindingConfig); + + Assertions.assertEquals("CN=the-issuer", resolved.get("issuer")); + Assertions.assertEquals("unchanged", resolved.get("static")); + } + + @Test + void testLeavesPlainCredentialsUntouched() { + CloudServiceKey keyToResolve = buildKeyWithCredentials(Map.of("app-identifier", "caas-coll-client")); + + CloudServiceKey resolved = resolver.resolve(keyToResolve); + + Assertions.assertEquals("caas-coll-client", resolved.getCredentials() + .get("app-identifier")); + Mockito.verifyNoInteractions(client); + } + + @Test + void testFailsLoudlyWhenReferencedFieldMissing() { + Mockito.when(client.getServiceKey(REFERENCED_SERVICE_INSTANCE, REFERENCED_KEY)) + .thenReturn(buildReferencedKey(Map.of("subject", "CN=only-subject"))); + CloudServiceKey keyToResolve = buildKeyWithCredentials(Map.of("issuer", + "{ds/" + REFERENCED_RESOURCE + "/service-keys." + REFERENCED_KEY + + ".credentials.issuer}")); + + Assertions.assertThrows(ContentException.class, () -> resolver.resolve(keyToResolve)); + } + + private CloudServiceKey buildKeyWithCredentials(Map credentials) { + return ImmutableCloudServiceKey.builder() + .name("caas-v2-key") + .credentials(credentials) + .serviceInstance(ImmutableCloudServiceInstanceExtended.builder() + .name("caas-v2") + .build()) + .build(); + } + + private CloudServiceKey buildReferencedKey(Map credentials) { + return ImmutableCloudServiceKey.builder() + .name(REFERENCED_KEY) + .credentials(credentials) + .serviceInstance(ImmutableCloudServiceInstanceExtended.builder() + .name(REFERENCED_SERVICE_INSTANCE) + .build()) + .serviceKeyOperation(ImmutableServiceCredentialBindingOperation.builder() + .state(ServiceCredentialBindingOperation.State.SUCCEEDED) + .type(ServiceCredentialBindingOperation.Type.CREATE) + .build()) + .build(); + } + +} \ No newline at end of file