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 66cc75ee4d..fd59692774 100644 --- 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 @@ -267,6 +267,7 @@ public class Messages { public static final String SERVICE_INSTANCE_0_PLAN_UPDATE_FAILED_IGNORING_FAILURE = "Service instance: \"{0}\" plan update failed, ignoring failure..."; public static final String SERVICE_INSTANCE_0_PARAMETERS_UPDATE_FAILED_IGNORING_FAILURE = "Service instance: \"{0}\" parameters update failed, ignoring failure..."; public static final String SERVICE_INSTANCE_0_TAGS_UPDATE_FAILED_IGNORING_FAILURE = "Service instance: \"{0}\" tags update failed, ignoring failure..."; + public static final String ONLY_FIRST_SERVICE_WILL_BE_CREATED = "Only the first service will be created because the provided 'service-name' fields are duplicated! All other services with the same 'service-name' will be ignored! Duplicated names: {0}"; // INFO log messages public static final String ACQUIRING_LOCK = "Process \"{0}\" attempting to acquire lock for operation on MTA \"{1}\""; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStep.java index 7e990ce124..4644496c23 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStep.java @@ -1,5 +1,6 @@ package org.cloudfoundry.multiapps.controller.process.steps; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -7,8 +8,9 @@ import java.util.Set; import java.util.stream.Collectors; +import com.sap.cloudfoundry.client.facade.CloudControllerClient; +import com.sap.cloudfoundry.client.facade.domain.CloudEntity; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.common.util.MiscUtil; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudServiceInstanceExtended; @@ -23,8 +25,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; -import com.sap.cloudfoundry.client.facade.CloudControllerClient; - @Named("extractBatchedServicesWithResolvedDynamicParametersStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class ExtractBatchedServicesWithResolvedDynamicParametersStep extends SyncFlowableStep { @@ -34,16 +34,21 @@ protected StepPhase executeStep(ProcessContext context) { getStepLogger().debug(Messages.EXTRACT_SERVICES_AND_RESOLVE_DYNAMIC_PARAMETERS_FROM_BATCH); Set dynamicResolvableParameters = context.getVariable(Variables.DYNAMIC_RESOLVABLE_PARAMETERS); - List servicesCalculatedForDeployment = context.getVariableBackwardsCompatible(Variables.BATCH_TO_PROCESS); - Set dynamicParametersWithResolvedExistingInstances = resolveDynamicPramatersWithExistingInstances(context.getControllerClient(), - dynamicResolvableParameters, - servicesCalculatedForDeployment); + List servicesCalculatedForDeployment = context.getVariableBackwardsCompatible( + Variables.BATCH_TO_PROCESS); + Set dynamicParametersWithResolvedExistingInstances = resolveDynamicPramatersWithExistingInstances( + context.getControllerClient(), + dynamicResolvableParameters, + servicesCalculatedForDeployment); List resolvedServiceInstances = servicesCalculatedForDeployment.stream() - .map(service -> resolveDynamicParametersOfServiceInstance(service, - dynamicParametersWithResolvedExistingInstances)) + .map( + service -> resolveDynamicParametersOfServiceInstance( + service, + dynamicParametersWithResolvedExistingInstances)) .collect(Collectors.toList()); + checkForDuplicatedServiceNameFields(resolvedServiceInstances); setServicesToCreate(context, resolvedServiceInstances); context.setVariable(Variables.DYNAMIC_RESOLVABLE_PARAMETERS, dynamicParametersWithResolvedExistingInstances); return StepPhase.DONE; @@ -55,9 +60,9 @@ protected String getStepErrorMessage(ProcessContext context) { } private Set - resolveDynamicPramatersWithExistingInstances(CloudControllerClient client, - Set dynamicResolvableParameters, - List servicesCalculatedForDeployment) { + resolveDynamicPramatersWithExistingInstances(CloudControllerClient client, + Set dynamicResolvableParameters, + List servicesCalculatedForDeployment) { Map existingServiceGuids = getExistingServiceGuidsIfNeeded(client, dynamicResolvableParameters, servicesCalculatedForDeployment); Set resolvedDynamicParameters = new HashSet<>(dynamicResolvableParameters); @@ -65,7 +70,8 @@ protected String getStepErrorMessage(ProcessContext context) { if (existingServiceGuids.containsKey(dynamicParameter.getRelationshipEntityName())) { resolvedDynamicParameters.remove(dynamicParameter); resolvedDynamicParameters.add(ImmutableDynamicResolvableParameter.copyOf(dynamicParameter) - .withValue(existingServiceGuids.get(dynamicParameter.getRelationshipEntityName()))); + .withValue(existingServiceGuids.get( + dynamicParameter.getRelationshipEntityName()))); } } return resolvedDynamicParameters; @@ -92,14 +98,16 @@ private boolean isServiceInstanceGuidRequired(Set dy CloudServiceInstanceExtended serviceCalculatedForDeployment) { return dynamicResolvableParameters.stream() .anyMatch(dynamicParameter -> dynamicParameter.getRelationshipEntityName() - .equals(serviceCalculatedForDeployment.getResourceName())); + .equals( + serviceCalculatedForDeployment.getResourceName())); } private CloudServiceInstanceExtended - resolveDynamicParametersOfServiceInstance(CloudServiceInstanceExtended service, - Set dynamicResolvableParameters) { + resolveDynamicParametersOfServiceInstance(CloudServiceInstanceExtended service, + Set dynamicResolvableParameters) { DynamicParametersResolver resolver = new DynamicParametersResolver(service.getResourceName(), - new DynamicResolvableParametersHelper(dynamicResolvableParameters)); + new DynamicResolvableParametersHelper( + dynamicResolvableParameters)); Map resolvedServiceParameters = MiscUtil.cast(new VisitableObject(service.getCredentials()).accept(resolver)); return ImmutableCloudServiceInstanceExtended.copyOf(service) .withCredentials(resolvedServiceParameters); @@ -109,11 +117,39 @@ private boolean isServiceInstanceGuidRequired(Set dy private void setServicesToCreate(ProcessContext context, List servicesCalculatedForDeployment) { List servicesToCreate = servicesCalculatedForDeployment.stream() - .filter(CloudServiceInstanceExtended::isManaged) + .filter( + CloudServiceInstanceExtended::isManaged) .collect(Collectors.toList()); getStepLogger().debug(Messages.SERVICES_TO_CREATE, SecureSerialization.toJson(servicesToCreate)); context.setVariable(Variables.SERVICES_TO_CREATE, servicesToCreate); context.setVariable(Variables.SERVICES_TO_CREATE_COUNT, servicesToCreate.size()); } + private void checkForDuplicatedServiceNameFields(List resolvedServiceInstances) { + List resolvedServiceInstancesNames = resolvedServiceInstances.stream() + .map(CloudEntity::getName) + .toList(); + + List duplicatedNames = getDuplicatedNames(resolvedServiceInstancesNames); + if (!duplicatedNames.isEmpty()) { + getStepLogger().warn( + Messages.ONLY_FIRST_SERVICE_WILL_BE_CREATED, String.join(" ", duplicatedNames)); + } + } + + private List getDuplicatedNames(List resolvedServiceInstancesNames) { + List duplicatedNames = new ArrayList<>(); + Map frequencyOfNamesMap = new HashMap<>(); + for (String name : resolvedServiceInstancesNames) { + frequencyOfNamesMap.merge(name, 1, Integer::sum); + } + + for (String name : frequencyOfNamesMap.keySet()) { + if (frequencyOfNamesMap.get(name) > 1) { + duplicatedNames.add(name); + } + } + return duplicatedNames; + } + } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java index 36331a066b..1a1384edb2 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ExtractBatchedServicesWithResolvedDynamicParametersStepTest.java @@ -1,11 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import java.text.MessageFormat; import java.util.Collections; import java.util.List; @@ -22,6 +16,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.v2.ResourceType; import org.cloudfoundry.multiapps.controller.core.model.DynamicResolvableParameter; import org.cloudfoundry.multiapps.controller.core.model.ImmutableDynamicResolvableParameter; +import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.Resource; @@ -29,6 +24,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; class ExtractBatchedServicesWithResolvedDynamicParametersStepTest extends SyncFlowableStepTest { @@ -39,61 +41,94 @@ class ExtractBatchedServicesWithResolvedDynamicParametersStepTest static Stream testExecute() { return Stream.of( - // (1) 3 input services but only 2 will be created due to specified "existing-service" resource type - Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() - .name(SERVICE_NAME_1) - .type(ServiceInstanceType.MANAGED) - .isManaged(true) - .build(), - ImmutableCloudServiceInstanceExtended.builder() - .name(SERVICE_NAME_2) - .type(ServiceInstanceType.USER_PROVIDED) - .isManaged(true) - .build(), - ImmutableCloudServiceInstanceExtended.builder() - .name(SERVICE_NAME_3) - .isManaged(false) - .build()), - Collections.emptySet(), List.of(ImmutableCloudServiceInstanceExtended.builder() - .resourceName(SERVICE_NAME_1) - .type(ServiceInstanceType.MANAGED) - .build(), - ImmutableCloudServiceInstanceExtended.builder() - .resourceName(SERVICE_NAME_2) - .type(ServiceInstanceType.USER_PROVIDED) - .build()), - false), - // (2) Resolve dynamic parameter inside parameters - Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() - .name(SERVICE_NAME_2) - .type(ServiceInstanceType.USER_PROVIDED) - .isManaged(true) - .credentials(Map.of("db-service-guid", - "{ds/service-1/service-guid}")) - .build()), - Set.of(ImmutableDynamicResolvableParameter.builder() - .relationshipEntityName(SERVICE_NAME_1) - .parameterName("service-guid") - .value("1") - .build()), - List.of(ImmutableCloudServiceInstanceExtended.builder() - .resourceName(SERVICE_NAME_2) - .type(ServiceInstanceType.USER_PROVIDED) - .credentials(Map.of("db-service-guid", "1")) - .build()), - false), - // (3) Fail step due to not resolved dynamic parameter - Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() - .name(SERVICE_NAME_2) - .type(ServiceInstanceType.USER_PROVIDED) - .credentials(Map.of("db-service-guid", - "{ds/service-1/service-guid}")) - .build()), - Set.of(ImmutableDynamicResolvableParameter.builder() - .relationshipEntityName(SERVICE_NAME_1) - .parameterName("service-guid") - .build()), - null, true) + // (1) 3 input services but only 2 will be created due to specified "existing-service" resource type + Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_1) + .type(ServiceInstanceType.MANAGED) + .isManaged(true) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_2) + .type(ServiceInstanceType.USER_PROVIDED) + .isManaged(true) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_3) + .isManaged(false) + .build()), + Collections.emptySet(), List.of(ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_1) + .type(ServiceInstanceType.MANAGED) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_2) + .type(ServiceInstanceType.USER_PROVIDED) + .build()), + false, false), + // (2) Resolve dynamic parameter inside parameters + Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_2) + .type(ServiceInstanceType.USER_PROVIDED) + .isManaged(true) + .credentials(Map.of("db-service-guid", + "{ds/service-1/service-guid}")) + .build()), + Set.of(ImmutableDynamicResolvableParameter.builder() + .relationshipEntityName(SERVICE_NAME_1) + .parameterName("service-guid") + .value("1") + .build()), + List.of(ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_2) + .type(ServiceInstanceType.USER_PROVIDED) + .credentials(Map.of("db-service-guid", "1")) + .build()), + false, false), + // (3) Fail step due to not resolved dynamic parameter + Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_2) + .type(ServiceInstanceType.USER_PROVIDED) + .credentials(Map.of("db-service-guid", + "{ds/service-1/service-guid}")) + .build()), + Set.of(ImmutableDynamicResolvableParameter.builder() + .relationshipEntityName(SERVICE_NAME_1) + .parameterName("service-guid") + .build()), + null, true, false), + // (4) 3 input services but with a duplicated name - thus a warning will be thrown and only the first service will be created + Arguments.of(List.of(ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_1) + .resourceName(SERVICE_NAME_1) + .type(ServiceInstanceType.MANAGED) + .isManaged(true) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_1) + .resourceName(SERVICE_NAME_2) + .type(ServiceInstanceType.MANAGED) + .isManaged(true) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .name(SERVICE_NAME_1) + .resourceName(SERVICE_NAME_3) + .type(ServiceInstanceType.MANAGED) + .isManaged(true) + .build()), + Collections.emptySet(), List.of(ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_1) + .type(ServiceInstanceType.MANAGED) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_2) + .type(ServiceInstanceType.MANAGED) + .build(), + ImmutableCloudServiceInstanceExtended.builder() + .resourceName(SERVICE_NAME_3) + .type(ServiceInstanceType.MANAGED) + .build()), + + false, true) ); } @@ -101,7 +136,7 @@ static Stream testExecute() { @ParameterizedTest @MethodSource void testExecute(List batchToProcess, Set dynamicResolvableParameters, - List expectedServicesToCreate, boolean expectedException) { + List expectedServicesToCreate, boolean expectedException, boolean expectedWarning) { loadParameters(batchToProcess, dynamicResolvableParameters); if (expectedException) { @@ -110,16 +145,22 @@ void testExecute(List batchToProcess, Set expectedServiceNames = expectedServicesToCreate.stream() .map(CloudServiceInstanceExtended::getResourceName) .collect(Collectors.toList()); Map expectedServicesType = expectedServicesToCreate.stream() - .collect(Collectors.toMap(CloudServiceInstanceExtended::getResourceName, - CloudServiceInstanceExtended::getType)); + .collect(Collectors.toMap( + CloudServiceInstanceExtended::getResourceName, + CloudServiceInstanceExtended::getType)); Map> expectedServicesParameters = expectedServicesToCreate.stream() - .collect(Collectors.toMap(CloudServiceInstanceExtended::getResourceName, - CloudServiceInstanceExtended::getCredentials)); + .collect(Collectors.toMap( + CloudServiceInstanceExtended::getResourceName, + CloudServiceInstanceExtended::getCredentials)); List servicesToCreateResult = context.getVariable(Variables.SERVICES_TO_CREATE); for (var serviceToCreateResult : servicesToCreateResult) {