package com.sap.cloud.lm.sl.cf.process.steps; import static com.sap.cloud.lm.sl.common.util.JsonUtil.convertJsonToList; import static com.sap.cloud.lm.sl.common.util.JsonUtil.convertJsonToMap; import static com.sap.cloud.lm.sl.common.util.JsonUtil.toJson; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.UUID; import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.collections4.ListUtils; import org.cloudfoundry.client.lib.CloudControllerClient; import org.cloudfoundry.client.lib.CloudOperationException; import org.cloudfoundry.client.lib.domain.CloudApplication; import org.cloudfoundry.client.lib.domain.CloudOrganization; import org.cloudfoundry.client.lib.domain.CloudSpace; import org.cloudfoundry.client.lib.domain.ImmutableCloudApplication; import org.cloudfoundry.client.lib.domain.ImmutableCloudOrganization; import org.cloudfoundry.client.lib.domain.ImmutableCloudSpace; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import com.sap.cloud.lm.sl.cf.client.lib.domain.CloudApplicationExtended; import com.sap.cloud.lm.sl.cf.core.cf.HandlerFactory; import com.sap.cloud.lm.sl.cf.core.cf.v2.ApplicationCloudModelBuilder; import com.sap.cloud.lm.sl.cf.core.helpers.ApplicationAttributes; import com.sap.cloud.lm.sl.cf.core.helpers.ClientHelper; import com.sap.cloud.lm.sl.cf.core.helpers.DummyConfigurationFilterParser; import com.sap.cloud.lm.sl.cf.core.helpers.ModuleToDeployHelper; import com.sap.cloud.lm.sl.cf.core.helpers.ReferencingPropertiesVisitor; import com.sap.cloud.lm.sl.cf.core.helpers.v2.ConfigurationReferencesResolver; import com.sap.cloud.lm.sl.cf.core.model.CloudTarget; import com.sap.cloud.lm.sl.cf.core.model.ConfigurationEntry; import com.sap.cloud.lm.sl.cf.core.model.ConfigurationSubscription; import com.sap.cloud.lm.sl.cf.core.model.ConfigurationSubscription.ModuleDto; import com.sap.cloud.lm.sl.cf.core.model.ConfigurationSubscription.RequiredDependencyDto; import com.sap.cloud.lm.sl.cf.core.model.SupportedParameters; import com.sap.cloud.lm.sl.cf.core.persistence.service.ConfigurationEntryService; import com.sap.cloud.lm.sl.cf.core.persistence.service.ConfigurationSubscriptionService; import com.sap.cloud.lm.sl.cf.core.security.serialization.SecureSerialization; import com.sap.cloud.lm.sl.cf.process.Messages; import com.sap.cloud.lm.sl.cf.process.flowable.FlowableFacade; import com.sap.cloud.lm.sl.cf.process.variables.Variables; import com.sap.cloud.lm.sl.common.SLException; import com.sap.cloud.lm.sl.mta.helpers.VisitableObject; import com.sap.cloud.lm.sl.mta.model.DeploymentDescriptor; import com.sap.cloud.lm.sl.mta.model.Module; import com.sap.cloud.lm.sl.mta.parsers.v2.DeploymentDescriptorParser; import com.sap.cloud.lm.sl.mta.parsers.v2.ModuleParser; import com.sap.cloud.lm.sl.mta.resolvers.Reference; import com.sap.cloud.lm.sl.mta.resolvers.ReferencePattern; import com.sap.cloud.lm.sl.mta.resolvers.ResolverBuilder; import com.sap.cloud.lm.sl.mta.util.ValidatorUtil; @Named("updateSubscribersStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class UpdateSubscribersStep extends SyncFlowableStep { /* * This schema version will be used only for the handling of the subscription entities and it should always be the same as the latest * version that is supported by the deploy service, as it is assumed that the latest version of the MTA specification will always * support a superset of the features supported by the previous versions. * * The major schema version of the MTA that is currently being deployed should NOT be used instead of this one, as problems could occur * if the subscriber has a different major schema version. If, for example, the current MTA has a major schema version 1, and the * subscriber has a major schema version 2, then this would result in the creation of a handler factory for version 1. That would cause * the update of the subscriber to fail, as required dependency entities do not exist in version 1 of the MTA specification and * therefore cannot be parsed by the version 1 parser that would be returned by that handler factory. */ // FIXME: Either store the major schema version in the subscriptions table // or change this to "3" and verify that everything is // working... private static final int MAJOR_SCHEMA_VERSION = 2; private static final String SCHEMA_VERSION = "2.1.0"; private static final String DUMMY_VERSION = "1.0.0"; protected BiFunction<ClientHelper, String, CloudTarget> targetCalculator = ClientHelper::computeTarget; @Inject private ConfigurationSubscriptionService configurationSubscriptionService; @Inject private ConfigurationEntryService configurationEntryService; @Inject private FlowableFacade flowableFacade; @Inject private ModuleToDeployHelper moduleToDeployHelper; @Override protected StepPhase executeStep(ProcessContext context) { getStepLogger().debug(Messages.UPDATING_SUBSCRIBERS); List<ConfigurationEntry> publishedEntries = StepsUtil.getPublishedEntriesFromSubProcesses(context, flowableFacade); List<ConfigurationEntry> deletedEntries = StepsUtil.getDeletedEntriesFromAllProcesses(context, flowableFacade); List<ConfigurationEntry> updatedEntries = ListUtils.union(publishedEntries, deletedEntries); CloudControllerClient clientForCurrentSpace = context.getControllerClient(); List<CloudApplication> updatedSubscribers = new ArrayList<>(); List<CloudApplication> updatedServiceBrokerSubscribers = new ArrayList<>(); List<ConfigurationSubscription> subscriptions = configurationSubscriptionService.createQuery() .onSelectMatching(updatedEntries) .list(); for (ConfigurationSubscription subscription : subscriptions) { ClientHelper clientHelper = new ClientHelper(clientForCurrentSpace); CloudTarget target = targetCalculator.apply(clientHelper, subscription.getSpaceId()); if (target == null) { getStepLogger().warn(Messages.COULD_NOT_COMPUTE_ORG_AND_SPACE, subscription.getSpaceId()); continue; } CloudApplication updatedApplication = updateSubscriber(context, target, subscription); if (updatedApplication != null) { updatedApplication = addOrgAndSpaceIfNecessary(updatedApplication, target); addApplicationToProperList(updatedSubscribers, updatedServiceBrokerSubscribers, updatedApplication); } } context.setVariable(Variables.UPDATED_SUBSCRIBERS, removeDuplicates(updatedSubscribers)); context.setVariable(Variables.UPDATED_SERVICE_BROKER_SUBSCRIBERS, updatedServiceBrokerSubscribers); getStepLogger().debug(Messages.SUBSCRIBERS_UPDATED); return StepPhase.DONE; } @Override protected String getStepErrorMessage(ProcessContext context) { return Messages.ERROR_UPDATING_SUBSCRIBERS; } private void addApplicationToProperList(List<CloudApplication> updatedSubscribers, List<CloudApplication> updatedServiceBrokerSubscribers, CloudApplication updatedApplication) { ApplicationAttributes appAttributes = ApplicationAttributes.fromApplication(updatedApplication); if (appAttributes.get(SupportedParameters.CREATE_SERVICE_BROKER, Boolean.class, false)) { updatedServiceBrokerSubscribers.add(updatedApplication); } else { updatedSubscribers.add(updatedApplication); } } private CloudApplication addOrgAndSpaceIfNecessary(CloudApplication application, CloudTarget cloudTarget) { // The entity returned by the getApplication(String appName) method of // the CF Java client does not contain a CloudOrganization, // because the value of the 'inline-relations-depth' is hardcoded to 1 // (see the findApplicationResource method of // org.cloudfoundry.client.lib.rest.CloudControllerClientImpl). if (application.getSpace() == null || application.getSpace() .getOrganization() == null) { CloudSpace space = createDummySpace(cloudTarget); return ImmutableCloudApplication.copyOf(application) .withSpace(space); } return application; } private CloudSpace createDummySpace(CloudTarget cloudTarget) { CloudOrganization org = createDummyOrg(cloudTarget.getOrganizationName()); return ImmutableCloudSpace.builder() .name(cloudTarget.getSpaceName()) .organization(org) .build(); } private CloudOrganization createDummyOrg(String orgName) { return ImmutableCloudOrganization.builder() .name(orgName) .build(); } private List<CloudApplication> removeDuplicates(List<CloudApplication> applications) { Map<UUID, CloudApplication> applicationsMap = new LinkedHashMap<>(); for (CloudApplication application : applications) { applicationsMap.put(application.getMetadata() .getGuid(), application); } return new ArrayList<>(applicationsMap.values()); } private CloudApplication updateSubscriber(ProcessContext context, CloudTarget cloudTarget, ConfigurationSubscription subscription) { try { return attemptToUpdateSubscriber(context, getClient(context, cloudTarget), subscription); } catch (CloudOperationException | SLException e) { String appName = subscription.getAppName(); String mtaId = subscription.getMtaId(); String subscriptionName = getRequiredDependency(subscription).getName(); getStepLogger().warn(e, Messages.COULD_NOT_UPDATE_SUBSCRIBER, appName, mtaId, subscriptionName); return null; } } private CloudApplication attemptToUpdateSubscriber(ProcessContext context, CloudControllerClient client, ConfigurationSubscription subscription) { HandlerFactory handlerFactory = new HandlerFactory(MAJOR_SCHEMA_VERSION); DeploymentDescriptor dummyDescriptor = buildDummyDescriptor(subscription, handlerFactory); getStepLogger().debug(com.sap.cloud.lm.sl.cf.core.Messages.DEPLOYMENT_DESCRIPTOR, SecureSerialization.toJson(dummyDescriptor)); ConfigurationReferencesResolver resolver = handlerFactory.getConfigurationReferencesResolver(configurationEntryService, new DummyConfigurationFilterParser(subscription.getFilter()), new CloudTarget(context.getVariable(Variables.ORGANIZATION_NAME), context.getVariable(Variables.SPACE_NAME)), configuration); resolver.resolve(dummyDescriptor); getStepLogger().debug(Messages.RESOLVED_DEPLOYMENT_DESCRIPTOR, SecureSerialization.toJson(dummyDescriptor)); dummyDescriptor = handlerFactory.getDescriptorReferenceResolver(dummyDescriptor, new ResolverBuilder(), new ResolverBuilder(), new ResolverBuilder()) .resolve(); getStepLogger().debug(Messages.RESOLVED_DEPLOYMENT_DESCRIPTOR, SecureSerialization.toJson(dummyDescriptor)); ApplicationCloudModelBuilder applicationCloudModelBuilder = handlerFactory.getApplicationCloudModelBuilder(dummyDescriptor, shouldUsePrettyPrinting(), null, "", context.getVariable(Variables.MTA_NAMESPACE), getStepLogger()); Module module = dummyDescriptor.getModules() .get(0); CloudApplicationExtended application = applicationCloudModelBuilder.build(module, moduleToDeployHelper); CloudApplication existingApplication = client.getApplication(subscription.getAppName()); Map<String, String> updatedEnvironment = application.getEnv(); Map<String, String> currentEnvironment = new LinkedHashMap<>(existingApplication.getEnv()); boolean neededToBeUpdated = updateCurrentEnvironment(currentEnvironment, updatedEnvironment, getPropertiesToTransfer(subscription, resolver)); if (!neededToBeUpdated) { return null; } getStepLogger().info(Messages.UPDATING_SUBSCRIBER, subscription.getAppName(), subscription.getMtaId(), getRequiredDependency(subscription).getName()); client.updateApplicationEnv(existingApplication.getName(), currentEnvironment); return existingApplication; } private List<String> getPropertiesToTransfer(ConfigurationSubscription subscription, ConfigurationReferencesResolver resolver) { List<String> result = new ArrayList<>(getFirstComponents(resolver.getExpandedProperties())); String listName = getRequiredDependency(subscription).getList(); if (listName == null) { result.addAll(getPropertiesWithReferencesToConfigurationResource(subscription)); result.addAll(getRequiredDependency(subscription).getProperties() .keySet()); } else { result.add(listName); } return result; } private List<String> getPropertiesWithReferencesToConfigurationResource(ConfigurationSubscription subscription) { ReferenceDetector detector = new ReferenceDetector(getRequiredDependency(subscription).getName()); new VisitableObject(subscription.getModuleDto() .getProperties()).accept(detector); return getFirstComponents(detector.getRelevantProperties()); } private boolean updateCurrentEnvironment(Map<String, String> currentEnvironment, Map<String, String> updatedEnvironment, List<String> propertiesToTransfer) { boolean neededToBeUpdated = false; for (String propertyToTransfer : propertiesToTransfer) { String currentProperty = currentEnvironment.get(propertyToTransfer); String updatedProperty = updatedEnvironment.get(propertyToTransfer); if (!Objects.equals(currentProperty, updatedProperty)) { neededToBeUpdated = true; currentEnvironment.put(propertyToTransfer, updatedEnvironment.get(propertyToTransfer)); } } return neededToBeUpdated; } private List<String> getFirstComponents(List<String> properties) { return properties.stream() .map(this::getFirstComponent) .collect(Collectors.toList()); } private String getFirstComponent(String propertyName) { int index = propertyName.indexOf(ValidatorUtil.DEFAULT_SEPARATOR); if (index != -1) { return propertyName.substring(0, index); } return propertyName; } private RequiredDependencyDto getRequiredDependency(ConfigurationSubscription subscription) { return subscription.getModuleDto() .getRequiredDependencies() .get(0); } private CloudControllerClient getClient(ProcessContext context, CloudTarget cloudTarget) { return context.getControllerClient(cloudTarget.getOrganizationName(), cloudTarget.getSpaceName()); } private DeploymentDescriptor buildDummyDescriptor(ConfigurationSubscription subscription, HandlerFactory handlerFactory) { ModuleDto moduleDto = subscription.getModuleDto(); String resourceJson = toJson(subscription.getResourceDto()); Map<String, Object> resourceMap = convertJsonToMap(resourceJson); Map<String, Object> moduleMap = new TreeMap<>(); moduleMap.put(ModuleParser.NAME, moduleDto.getName()); moduleMap.put(ModuleParser.TYPE, moduleDto.getName()); moduleMap.put(ModuleParser.PROPERTIES, moduleDto.getProperties()); moduleMap.put(ModuleParser.PROVIDES, convertJsonToList(toJson(moduleDto.getProvidedDependencies()))); moduleMap.put(ModuleParser.REQUIRES, convertJsonToList(toJson(moduleDto.getRequiredDependencies()))); Map<String, Object> dummyDescriptorMap = new TreeMap<>(); dummyDescriptorMap.put(DeploymentDescriptorParser.SCHEMA_VERSION, SCHEMA_VERSION); dummyDescriptorMap.put(DeploymentDescriptorParser.ID, subscription.getMtaId()); dummyDescriptorMap.put(DeploymentDescriptorParser.MODULES, Collections.singletonList(moduleMap)); dummyDescriptorMap.put(DeploymentDescriptorParser.VERSION, DUMMY_VERSION); dummyDescriptorMap.put(DeploymentDescriptorParser.RESOURCES, Collections.singletonList(resourceMap)); return handlerFactory.getDescriptorParser() .parseDeploymentDescriptor(dummyDescriptorMap); } protected boolean shouldUsePrettyPrinting() { return true; } private static class ReferenceDetector extends ReferencingPropertiesVisitor { public ReferenceDetector(String name) { super(ReferencePattern.FULLY_QUALIFIED, reference -> name.equals(reference.getDependencyName())); } private final List<String> relevantProperties = new ArrayList<>(); @Override protected Object visit(String key, String value, List<Reference> references) { relevantProperties.add(key); return value; } public List<String> getRelevantProperties() { return relevantProperties; } } }