diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java index c3b90dad68..62ff077d27 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java @@ -37,7 +37,7 @@ public class SupportedParameters { public static final String APPLY_NAMESPACE_APPS = "app-names"; public static final String APPLY_NAMESPACE_SERVICES = "service-names"; public static final String APPLY_NAMESPACE_ROUTES = "app-routes"; - + public static final String BG_DEPENDENCY_AWARE_STOP_ORDER = "bg-dependency-aware-stop-order"; public static final String DEPLOY_MODE = "deploy_mode"; public static final String PATH = "path"; @@ -212,7 +212,7 @@ public class SupportedParameters { FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION); public static final Set GLOBAL_PARAMETERS = Set.of(KEEP_EXISTING_ROUTES, APPS_UPLOAD_TIMEOUT, APPS_TASK_EXECUTION_TIMEOUT, APPS_START_TIMEOUT, APPS_STAGE_TIMEOUT, APPLY_NAMESPACE, - ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE); + ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE, BG_DEPENDENCY_AWARE_STOP_ORDER); public static final Set DEPENDENCY_PARAMETERS = Set.of(BINDING_NAME, ENV_VAR_NAME, VISIBILITY, USE_LIVE_ROUTES, SERVICE_BINDING_CONFIG, DELETE_SERVICE_KEY_AFTER_DEPLOYMENT); 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 14714311fe..c9a093e8d5 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 @@ -195,6 +195,8 @@ public class Messages { public static final String ERROR_WHILE_DETERMINING_SERVICE_KEYS_TO_RECREATE = "Error while determining service keys to recreate"; public static final String ERROR_WHILE_UPDATING_SERVICE_KEYS_METADATA = "Error while updating service keys metadata"; public static final String ERROR_WHILE_POLLING_SERVICE_KEY_OPERATION_0 = "Error while polling service key operation \"{0}\""; + public static final String ERROR_WHILE_STOPPING_DEPENDENT_MODULES = "Failed to stop dependent module with name \"{0}\""; + public static final String ERROR_WHEN_CONFIGURING_STOPPING_OF_DEPENDENT_MODULES = "Failed when configuring the stopping of dependent modules \"{0}\""; public static final String ERROR_WHILE_BACKUP_APPLICATION = "Error while backup applcation \"{0}\""; public static final String ERROR_DURING_PREPARATION_BACKUP_MTA = "Error during preparation backup mta for rollback deployment"; @@ -271,6 +273,8 @@ public class Messages { 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}"; + public static final String SKIPPING_DEPENDENCY_ORDER_STOP = "Skipping stopping modules in dependency-aware order. This feature is reserved only for blue green deployment strategy."; + public static final String UNSUPPORTED_DEPLOYED_AFTER_SCHEMA_VERSION_WARNING = "Skipping module \"{0}\": major schema version \"{1}\" does not support 'deployed-after' (minimum supported version is \"{2}\")."; // 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/metadata/BlueGreenDeployMetadata.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java index cebcfc8c9b..17477e9c5b 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadata.java @@ -40,22 +40,26 @@ public static OperationMetadata getMetadata() { .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPLY_NAMESPACE_APP_NAMES.getName()) .type(ParameterType.BOOLEAN) - .customConverter(new ApplyNamespaceParameterConverter(Variables.APPLY_NAMESPACE_APP_NAMES)) + .customConverter(new ApplyNamespaceParameterConverter( + Variables.APPLY_NAMESPACE_APP_NAMES)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPLY_NAMESPACE_SERVICE_NAMES.getName()) .type(ParameterType.BOOLEAN) - .customConverter(new ApplyNamespaceParameterConverter(Variables.APPLY_NAMESPACE_SERVICE_NAMES)) + .customConverter(new ApplyNamespaceParameterConverter( + Variables.APPLY_NAMESPACE_SERVICE_NAMES)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPLY_NAMESPACE_APP_ROUTES.getName()) .type(ParameterType.BOOLEAN) - .customConverter(new ApplyNamespaceParameterConverter(Variables.APPLY_NAMESPACE_APP_ROUTES)) + .customConverter(new ApplyNamespaceParameterConverter( + Variables.APPLY_NAMESPACE_APP_ROUTES)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPLY_NAMESPACE_AS_SUFFIX.getName()) .type(ParameterType.BOOLEAN) - .customConverter(new ApplyNamespaceParameterConverter(Variables.APPLY_NAMESPACE_AS_SUFFIX)) + .customConverter(new ApplyNamespaceParameterConverter( + Variables.APPLY_NAMESPACE_AS_SUFFIX)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.VERSION_RULE.getName()) @@ -105,22 +109,26 @@ public static OperationMetadata getMetadata() { .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) - .customConverter(new TimeoutParameterConverter(Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE)) + .customConverter(new TimeoutParameterConverter( + Variables.APPS_START_TIMEOUT_PROCESS_VARIABLE)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) - .customConverter(new TimeoutParameterConverter(Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE)) + .customConverter(new TimeoutParameterConverter( + Variables.APPS_STAGE_TIMEOUT_PROCESS_VARIABLE)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) - .customConverter(new TimeoutParameterConverter(Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) + .customConverter(new TimeoutParameterConverter( + Variables.APPS_UPLOAD_TIMEOUT_PROCESS_VARIABLE)) .build()) .addParameter(ImmutableParameterMetadata.builder() .id(Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName()) .type(ParameterType.INTEGER) - .customConverter(new TimeoutParameterConverter(Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE)) + .customConverter(new TimeoutParameterConverter( + Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE)) .build()) // Special blue green deploy parameters: .addParameter(ImmutableParameterMetadata.builder() @@ -149,6 +157,11 @@ public static OperationMetadata getMetadata() { .id(Variables.SHOULD_BACKUP_PREVIOUS_VERSION.getName()) .type(ParameterType.BOOLEAN) .build()) + .addParameter(ImmutableParameterMetadata.builder() + .id(Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName()) + .type(ParameterType.BOOLEAN) + .defaultValue(false) + .build()) .build(); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java index 3cf7e74442..911420cd13 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStep.java @@ -16,6 +16,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.v2.ConfigurationEntriesCloudModelBuilder; import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.core.model.Phase; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.serialization.SecureSerialization; import org.cloudfoundry.multiapps.controller.core.util.NameUtil; @@ -23,6 +24,7 @@ import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.AdditionalModuleParametersReporter; import org.cloudfoundry.multiapps.controller.process.util.ApplicationEnvironmentCalculator; +import org.cloudfoundry.multiapps.controller.process.util.DependentModuleStopResolver; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.handlers.HandlerFactory; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; @@ -38,6 +40,8 @@ public class BuildApplicationDeployModelStep extends SyncFlowableStep { private ModuleToDeployHelper moduleToDeployHelper; @Inject private ApplicationEnvironmentCalculator applicationEnvironmentCalculator; + @Inject + private DependentModuleStopResolver moduleStopResolver; @Override protected StepPhase executeStep(ProcessContext context) { @@ -61,6 +65,7 @@ protected StepPhase executeStep(ProcessContext context) { buildConfigurationEntries(context, modifiedApp); context.setVariable(Variables.TASKS_TO_EXECUTE, modifiedApp.getTasks()); getStepLogger().debug(Messages.CLOUD_APP_MODEL_BUILT); + determineDependentModulesToStop(context, module); return StepPhase.DONE; } @@ -135,6 +140,14 @@ private ConfigurationEntriesCloudModelBuilder getConfigurationEntriesCloudModelB return new ConfigurationEntriesCloudModelBuilder(organizationName, spaceName, spaceGuid, namespace); } + private void determineDependentModulesToStop(ProcessContext context, Module module) { + if (context.getVariable(Variables.PHASE) != Phase.AFTER_RESUME) { + return; + } + List modulesToStop = moduleStopResolver.resolveDependentModulesToStop(context, module); + context.setVariable(Variables.DEPENDENT_MODULES_TO_STOP, modulesToStop); + } + @Override protected String getStepErrorMessage(ProcessContext context) { return Messages.ERROR_BUILDING_CLOUD_APP_MODEL; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStep.java new file mode 100644 index 0000000000..975eb82b72 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStep.java @@ -0,0 +1,90 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudRoute; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.core.cf.v2.ApplicationCloudModelBuilder; +import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.ApplicationEnvironmentCalculator; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.handlers.HandlerFactory; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +@Named("prepareToStopDependentModuleStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class PrepareToStopDependentModuleStep extends SyncFlowableStep { + + private ModuleToDeployHelper moduleToDeployHelper; + + private ApplicationEnvironmentCalculator applicationEnvironmentCalculator; + + @Inject + public PrepareToStopDependentModuleStep(ModuleToDeployHelper moduleToDeployHelper, + ApplicationEnvironmentCalculator applicationEnvironmentCalculator) { + this.moduleToDeployHelper = moduleToDeployHelper; + this.applicationEnvironmentCalculator = applicationEnvironmentCalculator; + } + + @Override + protected StepPhase executeStep(ProcessContext context) { + Module applicationModule = findModuleInDeploymentDescriptor(context, getCurrentModuleToStop(context).getName()); + context.setVariable(Variables.MODULE_TO_DEPLOY, applicationModule); + CloudApplicationExtended modifiedApp = getApplicationCloudModelBuilder(context).build(applicationModule, moduleToDeployHelper); + Map calculatedAppEnv = applicationEnvironmentCalculator.calculateNewApplicationEnv(context, modifiedApp); + modifiedApp = getCloudApplicationExtended(context, modifiedApp, calculatedAppEnv); + context.setVariable(Variables.APP_TO_PROCESS, modifiedApp); + return StepPhase.DONE; + } + + private CloudApplicationExtended getCloudApplicationExtended(ProcessContext context, CloudApplicationExtended modifiedApp, + Map calculatedAppEnv) { + return ImmutableCloudApplicationExtended.builder() + .from(modifiedApp) + .staging(modifiedApp.getStaging()) + .routes(getApplicationRoutes(context, modifiedApp)) + .env(calculatedAppEnv) + .build(); + } + + protected ApplicationCloudModelBuilder getApplicationCloudModelBuilder(ProcessContext context) { + return StepsUtil.getApplicationCloudModelBuilder(context); + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return MessageFormat.format(Messages.ERROR_WHILE_STOPPING_DEPENDENT_MODULES, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + private Module getCurrentModuleToStop(ProcessContext context) { + List modules = context.getVariable(Variables.DEPENDENT_MODULES_TO_STOP); + int index = context.getVariable(Variables.APPS_TO_STOP_INDEX); + return modules.get(index); + } + + private Module findModuleInDeploymentDescriptor(ProcessContext context, String module) { + HandlerFactory handlerFactory = StepsUtil.getHandlerFactory(context.getExecution()); + DeploymentDescriptor deploymentDescriptor = context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR); + return handlerFactory.getDescriptorHandler() + .findModule(deploymentDescriptor, module); + } + + private Set getApplicationRoutes(ProcessContext context, CloudApplicationExtended modifiedApp) { + if (Boolean.TRUE.equals(context.getVariable(Variables.USE_IDLE_URIS))) { + return modifiedApp.getIdleRoutes(); + } + return modifiedApp.getRoutes(); + } + +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStep.java new file mode 100644 index 0000000000..14e6b1ba72 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStep.java @@ -0,0 +1,35 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.text.MessageFormat; +import java.util.List; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.core.model.SubprocessPhase; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +@Named("prepareToStopDependentModulesStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class PrepareToStopDependentModulesStep extends SyncFlowableStep { + + @Override + protected StepPhase executeStep(ProcessContext context) { + List dependentModulesToStop = context.getVariable(Variables.DEPENDENT_MODULES_TO_STOP); + context.setVariable(Variables.APPS_TO_STOP_COUNT, dependentModulesToStop.size()); + context.setVariable(Variables.APPS_TO_STOP_INDEX, 0); + context.setVariable(Variables.INDEX_VARIABLE_NAME, Variables.APPS_TO_STOP_INDEX.getName()); + context.setVariable(Variables.SUBPROCESS_PHASE, SubprocessPhase.BEFORE_APPLICATION_STOP); + return StepPhase.DONE; + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return MessageFormat.format(Messages.ERROR_WHEN_CONFIGURING_STOPPING_OF_DEPENDENT_MODULES, + context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + +} \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStep.java new file mode 100644 index 0000000000..b455811144 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStep.java @@ -0,0 +1,66 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.text.MessageFormat; +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.core.model.BlueGreenApplicationNameSuffix; +import org.cloudfoundry.multiapps.controller.core.model.HookPhase; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.util.ApplicationWaitAfterStopHandler; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +@Named("stopDependentModuleStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class StopDependentModuleStep extends SyncFlowableStepWithHooks implements BeforeStepHookPhaseProvider, AfterStepHookPhaseProvider { + + private ApplicationWaitAfterStopHandler waitAfterStopHandler; + + @Inject + public StopDependentModuleStep(ApplicationWaitAfterStopHandler waitAfterStopHandler) { + this.waitAfterStopHandler = waitAfterStopHandler; + } + + @Override + protected StepPhase executeStepInternal(ProcessContext context) { + CloudControllerClient client = context.getControllerClient(); + String idleName = getCurrentModuleToStop(context).getName() + BlueGreenApplicationNameSuffix.IDLE.asSuffix(); + CloudApplication app = client.getApplication(idleName); + if (app != null && !app.getState() + .equals(CloudApplication.State.STOPPED)) { + client.stopApplication(idleName); + getStepLogger().info(Messages.APP_STOPPED, idleName); + } + waitAfterStopHandler.configureDelayAfterAppStop(context, idleName); + return StepPhase.DONE; + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return MessageFormat.format(Messages.ERROR_WHILE_STOPPING_DEPENDENT_MODULES, getCurrentModuleToStop(context).getName()); + } + + static Module getCurrentModuleToStop(ProcessContext context) { + List modules = context.getVariable(Variables.DEPENDENT_MODULES_TO_STOP); + int index = context.getVariable(Variables.APPS_TO_STOP_INDEX); + return modules.get(index); + } + + @Override + public List getHookPhasesBeforeStep(ProcessContext context) { + List hookPhases = List.of(HookPhase.BEFORE_STOP); + return hooksPhaseBuilder.buildHookPhases(hookPhases, context); + } + + @Override + public List getHookPhasesAfterStep(ProcessContext context) { + List hookPhases = List.of(HookPhase.AFTER_STOP); + return hooksPhaseBuilder.buildHookPhases(hookPhases, context); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolver.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolver.java new file mode 100644 index 0000000000..72a67a4315 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolver.java @@ -0,0 +1,119 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.cloudfoundry.multiapps.mta.util.PropertiesUtil; + +@Named +public class DependentModuleStopResolver { + + private static final int DEPLOYED_AFTER_MIN_SCHEMA_VERSION = 3; + + public List resolveDependentModulesToStop(ProcessContext context, Module root) { + DeploymentDescriptor descriptor = context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR); + if (!isDependencyAwareStopOrderEnabled(context, descriptor)) { + return Collections.emptyList(); + } + if (!Boolean.TRUE.equals(context.getVariable(Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY))) { + context.getStepLogger() + .warn(Messages.SKIPPING_DEPENDENCY_ORDER_STOP); + return Collections.emptyList(); + } + Map modulesByName = getModulesByName(descriptor); + Map> modulesDependentOn = buildModulesDependentOn(modulesByName.values(), context.getStepLogger()); + + List result = new ArrayList<>(); + Set visited = new HashSet<>(); + + collectModulesDependentOnPostOrder(root.getName(), modulesDependentOn, visited, result); + + result.remove(root); + return result; + } + + private boolean isDependencyAwareStopOrderEnabled(ProcessContext context, DeploymentDescriptor descriptor) { + boolean isExplicitlySetFromContext = VariableHandling.get(context.getExecution(), Variables.STOP_ORDER_IS_DEPENDENCY_AWARE); + if (isExplicitlySetFromContext) { + return true; + } + return (boolean) PropertiesUtil.getPropertyValue(List.of(descriptor.getParameters()), + SupportedParameters.BG_DEPENDENCY_AWARE_STOP_ORDER, false); + } + + private Map getModulesByName(DeploymentDescriptor descriptor) { + return descriptor.getModules() + .stream() + .collect(Collectors.toMap( + Module::getName, + Function.identity() + )); + } + + private Map> buildModulesDependentOn(Collection modules, StepLogger logger) { + return modules.stream() + .filter(module -> supportsDeployedAfter(module, logger)) + .flatMap(this::toDependentEntries) + .collect(groupByDependency()); + } + + private boolean supportsDeployedAfter(Module module, StepLogger logger) { + if (module.getMajorSchemaVersion() >= 3) { + return true; + } + logger.warn( + Messages.UNSUPPORTED_DEPLOYED_AFTER_SCHEMA_VERSION_WARNING, + module.getName(), + module.getMajorSchemaVersion(), DEPLOYED_AFTER_MIN_SCHEMA_VERSION); + return false; + } + + private Stream> toDependentEntries(Module module) { + List deployedAfter = module.getDeployedAfter(); + if (deployedAfter == null || deployedAfter.isEmpty()) { + return Stream.empty(); + } + + return deployedAfter.stream() + .map(dependencyName -> Map.entry(dependencyName, module)); + } + + private Collector, ?, Map>> groupByDependency() { + return Collectors.groupingBy( + Map.Entry::getKey, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()) + ); + } + + private void collectModulesDependentOnPostOrder(String moduleName, Map> modulesDependentOn, Set visited, + List result) { + for (Module dependent : modulesDependentOn.getOrDefault(moduleName, List.of())) { + String dependentName = dependent.getName(); + + if (!visited.add(dependentName)) { + continue; + } + + collectModulesDependentOnPostOrder(dependentName, modulesDependentOn, visited, result); + result.add(dependent); + } + } + +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java index bd9e78fc20..74959bd220 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java @@ -199,6 +199,12 @@ public interface Variables { Variable TASKS_INDEX = ImmutableSimpleVariable. builder() .name("tasksIndex") .build(); + Variable APPS_TO_STOP_COUNT = ImmutableSimpleVariable. builder() + .name("appsToStopCount") + .build(); + Variable APPS_TO_STOP_INDEX = ImmutableSimpleVariable. builder() + .name("appsToStopIndex") + .build(); Variable SERVICES_TO_CREATE_COUNT = ImmutableSimpleVariable. builder() .name("servicesToCreateCount") .build(); @@ -339,9 +345,9 @@ public interface Variables { .defaultValue(new MtaArchiveElements()) .build(); Variable MTA_ARCHIVE_CREATED_BY = ImmutableSimpleVariable. builder() - .name("mtaArchiveCreatedBy") - .defaultValue(null) - .build(); + .name("mtaArchiveCreatedBy") + .defaultValue(null) + .build(); Variable SERVICE_TO_PROCESS = ImmutableJsonStringVariable. builder() .name("serviceToProcess") .type(Variable.typeReference( @@ -468,6 +474,12 @@ public interface Variables { .type(new TypeReference<>() { }) .build(); + Variable> DEPENDENT_MODULES_TO_STOP = ImmutableJsonBinaryVariable.> builder() + .name("dependentModulesToStop") + .type(new TypeReference<>() { + }) + .defaultValue(Collections.emptyList()) + .build(); Variable> TRIGGERED_SERVICE_OPERATIONS = ImmutableJsonBinaryVariable.> builder() .name( "triggeredServiceOperations") @@ -920,4 +932,9 @@ public Serializer> getSerializer() { .name("processUserProvidedServices") .defaultValue(false) .build(); + + Variable STOP_ORDER_IS_DEPENDENCY_AWARE = ImmutableSimpleVariable. builder() + .name("stopOrderIsDependencyAware") + .defaultValue(false) + .build(); } diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn index efc2b686bc..b1e00f8e0b 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn @@ -1,5 +1,9 @@ - + @@ -20,7 +24,7 @@ - + @@ -63,9 +67,6 @@ - - - @@ -73,11 +74,6 @@ - - - - - @@ -94,7 +90,6 @@ - @@ -111,14 +106,10 @@ - - - + - - @@ -138,14 +129,11 @@ - - - + - - + @@ -167,9 +155,7 @@ - - - + @@ -177,7 +163,7 @@ - + @@ -199,9 +185,7 @@ - - - + @@ -220,33 +204,16 @@ - - - - - - - - - - ${(delayAfterAppStop)} - - - - - - - @@ -260,9 +227,6 @@ - - - @@ -278,449 +242,529 @@ + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn new file mode 100644 index 0000000000..d9282bc989 --- /dev/null +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + ${(delayAfterAppStop)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java index 3e30657e47..40c5bb6eaa 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/metadata/BlueGreenDeployMetadataTest.java @@ -54,6 +54,7 @@ protected String[] getParametersIds() { Variables.APPS_TASK_EXECUTION_TIMEOUT_PROCESS_VARIABLE.getName(), Variables.SKIP_APP_DIGEST_CALCULATION.getName(), Variables.SHOULD_BACKUP_PREVIOUS_VERSION.getName(), + Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName(), // @formatter:on }; } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStepTest.java index 8453576dd0..b94861c6a0 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/BuildApplicationDeployModelStepTest.java @@ -1,28 +1,33 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import java.util.List; import java.util.Map; import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.core.model.Phase; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.process.util.ApplicationEnvironmentCalculator; +import org.cloudfoundry.multiapps.controller.process.util.DependentModuleStopResolver; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.Module; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + class BuildApplicationDeployModelStepTest extends SyncFlowableStepTest { @Mock private ModuleToDeployHelper moduleToDeployHelper; @Mock private ApplicationEnvironmentCalculator applicationEnvironmentCalculator; + @Mock + private DependentModuleStopResolver moduleStopResolver; @Test void testModuleResolutionAsyncServiceBindings() { @@ -34,6 +39,27 @@ void testModuleResolutionAsyncServiceBindings() { assertStepFinishedSuccessfully(); } + @Test + void testModuleResolutionAsyncServiceBindings1() { + Module module = Module.createV3() + .setName("test-module"); + setUpMocks(module); + List modulesToStop = List.of(Module.createV3() + .setName("test-returned-module")); + when(moduleStopResolver.resolveDependentModulesToStop(any(), any())).thenReturn(modulesToStop); + context.setVariable(Variables.PHASE, Phase.AFTER_RESUME); + step.execute(execution); + assertTrue(context.getVariable(Variables.SHOULD_UNBIND_BIND_SERVICES_IN_PARALLEL)); + assertEquals( + modulesToStop.stream() + .map(Module::getName) + .toList(), + modulesToStop.stream() + .map(Module::getName) + .toList()); + assertStepFinishedSuccessfully(); + } + @Test void testModuleResolutionSyncServiceBindings() { Module module = Module.createV3() diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStepTest.java new file mode 100644 index 0000000000..e08defef17 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModuleStepTest.java @@ -0,0 +1,117 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableStaging; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.core.cf.util.ModulesCloudModelBuilderContentCalculator; +import org.cloudfoundry.multiapps.controller.core.cf.v2.ApplicationCloudModelBuilder; +import org.cloudfoundry.multiapps.controller.core.cf.v2.ServiceKeysCloudModelBuilder; +import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.process.util.ApplicationEnvironmentCalculator; +import org.cloudfoundry.multiapps.controller.process.util.DeprecatedBuildpackChecker; +import org.cloudfoundry.multiapps.controller.process.util.ProcessTypeParser; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class PrepareToStopDependentModuleStepTest extends SyncFlowableStepTest { + + @Mock + private ModuleToDeployHelper moduleToDeployHelper; + + @Mock + protected ApplicationCloudModelBuilder applicationCloudModelBuilder; + @Mock + protected ModulesCloudModelBuilderContentCalculator modulesCloudModelBuilderContentCalculator; + @Mock + protected ServiceKeysCloudModelBuilder serviceKeysCloudModelBuilder; + + @Mock + private ProcessTypeParser processTypeParser; + + @Mock + private DeprecatedBuildpackChecker deprecatedBuildpackChecker; + + @Mock + private ApplicationEnvironmentCalculator applicationEnvironmentCalculator; + + @Override + protected PrepareToStopDependentModuleStep createStep() { + return new PrepareToStopDependentModuleStepTest.PrepareToStopDependentModuleStepMock(moduleToDeployHelper, + applicationEnvironmentCalculator); + } + + @Test + void testPrepareToStopDependentModuleStep() { + Module module = Module.createV3() + .setName("test-module"); + setUpMocks(module); + step.execute(execution); + assertEquals(context.getVariable(Variables.APP_TO_PROCESS) + .getName(), module.getName()); + assertStepFinishedSuccessfully(); + } + + @Test + void testPrepareToStopDependentModuleStepIdleURIs() { + Module module = Module.createV3() + .setName("test-module"); + setUpMocks(module); + context.setVariable(Variables.USE_IDLE_URIS, true); + step.execute(execution); + assertEquals(context.getVariable(Variables.APP_TO_PROCESS) + .getName(), module.getName()); + assertStepFinishedSuccessfully(); + } + + private void setUpMocks(Module module) { + DeploymentDescriptor completeDeploymentDescriptor = DeploymentDescriptor.createV3(); + completeDeploymentDescriptor.setModules(List.of(module)); + context.setVariable(Variables.DEPENDENT_MODULES_TO_STOP, List.of(module)); + context.setVariable(Variables.APPS_TO_STOP_INDEX, 0); + context.setVariable(Variables.MTA_MAJOR_SCHEMA_VERSION, 3); + ImmutableCloudApplicationExtended applicationExtended = ImmutableCloudApplicationExtended.builder() + .name("test-module") + .metadata(ImmutableCloudMetadata.of( + UUID.randomUUID() + )) + .staging(createStaging( + true)) + .build(); + context.setVariable(Variables.APP_TO_PROCESS, applicationExtended); + context.setVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, completeDeploymentDescriptor); + context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, completeDeploymentDescriptor); + when(applicationCloudModelBuilder.build(Mockito.any(), Mockito.any())).thenReturn(applicationExtended); + } + + private Staging createStaging(boolean isReadinessHealthCheckEnabled) { + return ImmutableStaging.builder() + .isReadinessHealthCheckEnabled(isReadinessHealthCheckEnabled) + .readinessHealthCheckType("http") + .build(); + } + + private class PrepareToStopDependentModuleStepMock extends PrepareToStopDependentModuleStep { + + public PrepareToStopDependentModuleStepMock(ModuleToDeployHelper moduleToDeployHelper, + ApplicationEnvironmentCalculator applicationEnvironmentCalculator) { + super(moduleToDeployHelper, applicationEnvironmentCalculator); + } + + @Override + protected ApplicationCloudModelBuilder getApplicationCloudModelBuilder(ProcessContext context) { + return applicationCloudModelBuilder; + } + + } +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStepTest.java new file mode 100644 index 0000000000..42d039ffa0 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PrepareToStopDependentModulesStepTest.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.Collections; +import java.util.List; + +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Test; + +class PrepareToStopDependentModulesStepTest extends SyncFlowableStepTest { + + @Test + void testPrepareToStopDependentModulesStepEmpty() { + context.setVariable(Variables.DEPENDENT_MODULES_TO_STOP, Collections.emptyList()); + step.execute(execution); + context.setVariable(Variables.APPS_TO_STOP_COUNT, 0); + assertStepFinishedSuccessfully(); + } + + @Test + void testPrepareToStopDependentModulesStepTest() { + context.setVariable(Variables.DEPENDENT_MODULES_TO_STOP, List.of(Module.createV3() + .setName("module"))); + step.execute(execution); + context.setVariable(Variables.APPS_TO_STOP_COUNT, 1); + assertStepFinishedSuccessfully(); + } + + @Override + protected PrepareToStopDependentModulesStep createStep() { + return new PrepareToStopDependentModulesStep(); + } +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStepTest.java new file mode 100644 index 0000000000..a790b561e0 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/StopDependentModuleStepTest.java @@ -0,0 +1,89 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataParser; +import org.cloudfoundry.multiapps.controller.core.model.BlueGreenApplicationNameSuffix; +import org.cloudfoundry.multiapps.controller.process.util.ApplicationWaitAfterStopHandler; +import org.cloudfoundry.multiapps.controller.process.util.HooksPhaseGetter; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class StopDependentModuleStepTest extends SyncFlowableStepTest { + + private static String appName = "test-name"; + + private static String idleAppName = appName + BlueGreenApplicationNameSuffix.IDLE.asSuffix(); + + @Mock + private ApplicationWaitAfterStopHandler waitAfterStopHandler; + + @Mock + private MtaMetadataParser mtaMetadataParser; + + @Mock + private HooksPhaseGetter hooksPhaseGetter; + + @Test + void testStopDependentModuleStep() { + org.cloudfoundry.multiapps.mta.model.Module module = org.cloudfoundry.multiapps.mta.model.Module.createV3() + .setName(appName); + setUpMocks(module, CloudApplication.State.STARTED); + step.execute(execution); + assertStepFinishedSuccessfully(); + verify(client, times(1)).stopApplication(idleAppName); + } + + @Test + void testStopDependentModuleStepApplicationDoesNotExist() { + org.cloudfoundry.multiapps.mta.model.Module module = org.cloudfoundry.multiapps.mta.model.Module.createV3() + .setName(appName); + setUpMocks(module, CloudApplication.State.STARTED); + when(client.getApplication(idleAppName)).thenReturn(null); + step.execute(execution); + assertStepFinishedSuccessfully(); + verify(client, times(0)).stopApplication(idleAppName); + } + + @Test + void testStopDependentModuleStepStopped() { + org.cloudfoundry.multiapps.mta.model.Module module = org.cloudfoundry.multiapps.mta.model.Module.createV3() + .setName(appName); + setUpMocks(module, CloudApplication.State.STOPPED); + step.execute(execution); + assertStepFinishedSuccessfully(); + verify(client, times(0)).stopApplication(idleAppName); + } + + private void setUpMocks(Module module, CloudApplication.State state) { + DeploymentDescriptor completeDeploymentDescriptor = DeploymentDescriptor.createV3(); + completeDeploymentDescriptor.setModules(List.of(module)); + ImmutableCloudApplicationExtended applicationExtended = ImmutableCloudApplicationExtended.builder() + .name(appName) + .metadata(ImmutableCloudMetadata.of( + UUID.randomUUID() + )) + .state(state) + .build(); + when(client.getApplication(idleAppName)).thenReturn(applicationExtended); + context.setVariable(Variables.MODULE_TO_DEPLOY, module); + context.setVariable(Variables.DEPENDENT_MODULES_TO_STOP, List.of(module)); + context.setVariable(Variables.APPS_TO_STOP_INDEX, 0); + } + + @Override + protected StopDependentModuleStep createStep() { + return new StopDependentModuleStep(waitAfterStopHandler); + } +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolverTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolverTest.java new file mode 100644 index 0000000000..acd46e5a95 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/DependentModuleStopResolverTest.java @@ -0,0 +1,340 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class DependentModuleStopResolverTest { + + @Mock + private ProcessContext context; + + @Mock + private DelegateExecution execution; + + @Mock + private StepLogger logger; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + void setupContext(List modules) { + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + descriptor.setParameters(Map.of(SupportedParameters.BG_DEPENDENCY_AWARE_STOP_ORDER, true)); + Mockito.when(context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR)) + .thenReturn(descriptor); + Mockito.when(context.getVariable(Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY)) + .thenReturn(true); + descriptor.setModules(modules); + Mockito.when(context.getExecution()) + .thenReturn(execution); + Mockito.when(execution.getVariable(Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName())) + .thenReturn(false); + Mockito.when(context.getStepLogger()) + .thenReturn(logger); + } + + private List names(List modules) { + return modules.stream() + .map(Module::getName) + .toList(); + } + + @Test + void testDependentModuleStopResolver() { + Module module = Module.createV3() + .setName("test-module"); + setupContext(List.of(module)); + DependentModuleStopResolver dependentModuleStopResolver = new DependentModuleStopResolver(); + List result = dependentModuleStopResolver.resolveDependentModulesToStop(context, module); + assertTrue(result.isEmpty(), "Expected the list to be empty"); + } + + @Test + void testDependentModuleStopResolverUnsupported() { + Module module = Module.createV2() + .setName("test-module"); + setupContext(List.of(module)); + DependentModuleStopResolver dependentModuleStopResolver = new DependentModuleStopResolver(); + List result = dependentModuleStopResolver.resolveDependentModulesToStop(context, module); + verify(logger, times(1)).warn(anyString(), anyString(), anyInt(), anyInt()); + assertTrue(result.isEmpty(), "Expected the list to be empty"); + } + + @Test + void testDependentModuleStopResolverEmptyDeployedAfter() { + Module module = Module.createV3() + .setName("test-module") + .setDeployedAfter(Collections.emptyList()); + setupContext(List.of(module)); + DependentModuleStopResolver dependentModuleStopResolver = new DependentModuleStopResolver(); + List result = dependentModuleStopResolver.resolveDependentModulesToStop(context, module); + assertTrue(result.isEmpty(), "Expected the list to be empty"); + } + + @Test + void resolveDependentModulesLinearChain() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("B")); + + setupContext(List.of(a, b, c)); + DependentModuleStopResolver resolver = new DependentModuleStopResolver(); + + List result = resolver.resolveDependentModulesToStop(context, a); + + assertEquals(List.of("C", "B"), result.stream() + .map(Module::getName) + .toList()); + } + + @Test + void resolveDependentModuleDiamond() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("A")); + Module d = Module.createV3() + .setName("D") + .setDeployedAfter(List.of("B", "C")); + + setupContext(List.of(a, b, c, d)); + DependentModuleStopResolver resolver = new DependentModuleStopResolver(); + + List result = resolver.resolveDependentModulesToStop(context, a); + + assertEquals(List.of("D", "B", "C"), result.stream() + .map(Module::getName) + .toList()); + } + + @Test + void returnsEmptyWhenFeatureFlagDisabled() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + descriptor.setParameters(Map.of(SupportedParameters.BG_DEPENDENCY_AWARE_STOP_ORDER, false)); + Mockito.when(context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR)) + .thenReturn(descriptor); + descriptor.setModules(List.of(a, b)); + Mockito.when(context.getExecution()) + .thenReturn(execution); + Mockito.when(execution.getVariable(Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName())) + .thenReturn(false); + DependentModuleStopResolver resolver = new DependentModuleStopResolver(); + assertTrue(resolver.resolveDependentModulesToStop(context, a) + .isEmpty()); + } + + @Test + void noDependentModulesReturnsEmpty() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B"); + + setupContext(List.of(a, b)); + DependentModuleStopResolver resolver = new DependentModuleStopResolver(); + + assertTrue(resolver.resolveDependentModulesToStop(context, a) + .isEmpty()); + } + + @Test + void diamondDependencyPostOrderAndNoDuplicates() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("A")); + Module d = Module.createV3() + .setName("D") + .setDeployedAfter(List.of("B", "C")); + setupContext(List.of(a, b, c, d)); + DependentModuleStopResolver resolver = new DependentModuleStopResolver(); + + List result = names(resolver.resolveDependentModulesToStop(context, a)); + + assertEquals(List.of("D", "B", "C"), result); + assertEquals(result.size(), result.stream() + .distinct() + .count()); + } + + @Test + void multipleBranchesAllIncluded() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("A")); + Module d = Module.createV3() + .setName("D") + .setDeployedAfter(List.of("A")); + + setupContext(List.of(a, b, c, d)); + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + + assertTrue(result.containsAll(List.of("B", "C", "D"))); + } + + @Test + void cyclicDependencyDoesNotInfiniteLoop() { + Module a = Module.createV3() + .setName("A") + .setDeployedAfter(List.of("C")); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("B")); + + setupContext(List.of(a, b, c)); + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + + assertEquals(2, result.size()); + assertTrue(result.containsAll(List.of("B", "C"))); + } + + @Test + void missingDependencyIsIgnoredSafely() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A", "X")); + + setupContext(List.of(a, b)); + assertEquals(List.of("B"), + names(new DependentModuleStopResolver() + .resolveDependentModulesToStop(context, a))); + } + + @Test + void rootIsNotIncludedInResult() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + setupContext(List.of(a, b)); + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + + assertEquals(List.of("B"), result); + assertFalse(result.contains("A")); + } + + @Test + void orderingIsDeterministic() { + Module a = Module.createV3() + .setName("A"); + Module c = Module.createV3() + .setName("C") + .setDeployedAfter(List.of("A")); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + setupContext(List.of(a, c, b)); + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + + assertEquals(List.of("B", "C"), result); + } + + @Test + void testDependentModuleStopResolverContextFlag() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + descriptor.setParameters(Map.of(SupportedParameters.BG_DEPENDENCY_AWARE_STOP_ORDER, true)); + Mockito.when(context.getVariable(Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR)) + .thenReturn(descriptor); + descriptor.setModules(List.of(a, b)); + Mockito.when(context.getExecution()) + .thenReturn(execution); + Mockito.when(context.getVariable(Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY)) + .thenReturn(true); + Mockito.when(execution.getVariable(Variables.STOP_ORDER_IS_DEPENDENCY_AWARE.getName())) + .thenReturn(true); + + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + + assertEquals(List.of("B"), result); + assertFalse(result.contains("A")); + } + + @Test + void testDependentModuleStopResolverSkip() { + Module a = Module.createV3() + .setName("A"); + Module b = Module.createV3() + .setName("B") + .setDeployedAfter(List.of("A")); + setupContext(List.of(a, b)); + Mockito.when(context.getVariable(Variables.KEEP_ORIGINAL_APP_NAMES_AFTER_DEPLOY)) + .thenReturn(false); + + List result = names( + new DependentModuleStopResolver().resolveDependentModulesToStop(context, a) + ); + verify(logger, times(1)).warn(anyString()); + assertTrue(result.isEmpty(), "Expected the list to be empty"); + + } +} +