From b27261710362a437c870feb56aa9d9ef5c4b2eaa Mon Sep 17 00:00:00 2001 From: stiv03 Date: Thu, 2 Apr 2026 13:31:00 +0300 Subject: [PATCH 01/10] Add phase-configs target-app resolution in ExecuteTaskStep for blue-green deployments Route hook task execution to idle or live app based on phase-configs in the hook descriptor. Pass idleMtaColor, liveMtaColor, subprocessPhase and phase variables to all hook call activities in BPMN processes. Add determineDeploymentTypeSafely to avoid SERVICE_ID lookup failures in hook subprocesses. # Conflicts: # pom.xml --- .../core/model/SupportedParameters.java | 4 +- .../process/steps/ExecuteTaskStep.java | 67 ++++++++++++++++++- .../util/DeploymentTypeDeterminer.java | 4 ++ .../process/backup-existing-app.bpmn | 4 ++ .../controller/process/deploy-app.bpmn | 8 +++ .../process/stop-dependent-modules.bpmn | 3 + .../controller/process/undeploy-app.bpmn | 8 +++ pom.xml | 2 +- 8 files changed, 96 insertions(+), 4 deletions(-) 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 62ff077d27..23b3079142 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 @@ -217,7 +217,9 @@ public class SupportedParameters { 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); - public static final Set MODULE_HOOK_PARAMETERS = Set.of(NAME, COMMAND, MEMORY, DISK_QUOTA, HOOK_REQUIRES); + public static final String HOOK_TARGET_APP = "hook-target-app"; + + public static final Set MODULE_HOOK_PARAMETERS = Set.of(NAME, COMMAND, MEMORY, DISK_QUOTA, HOOK_REQUIRES, HOOK_TARGET_APP); public static final Set CONFIGURATION_REFERENCE_PARAMETERS = Set.of(PROVIDER_NID, PROVIDER_ID, TARGET, VERSION, DEPRECATED_CONFIG_MTA_ID, DEPRECATED_CONFIG_MTA_VERSION, diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 57f2e88526..a333517d90 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -11,10 +11,13 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; +import org.cloudfoundry.multiapps.controller.core.model.HookPhaseProcessType; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Hook; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @@ -35,13 +38,73 @@ protected StepPhase executeAsyncStep(ProcessContext context) { CloudTask taskToExecute = StepsUtil.getTask(context); CloudControllerClient client = context.getControllerClient(); - getStepLogger().info(Messages.EXECUTING_TASK_ON_APP, taskToExecute.getName(), app.getName()); - CloudTask startedTask = client.runTask(app.getName(), taskToExecute); + String appName = resolveTargetAppName(context, app); + + getStepLogger().info(Messages.EXECUTING_TASK_ON_APP, taskToExecute.getName(), appName); + CloudTask startedTask = client.runTask(appName, taskToExecute); context.setVariable(Variables.STARTED_TASK, startedTask); context.setVariable(Variables.START_TIME, currentTimeSupplier.getAsLong()); return StepPhase.POLL; } + private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app) { + Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); + if (hook == null || hook.getPhaseConfigs().isEmpty()) { + return app.getName(); + } + + String currentPhase = buildCurrentPhaseString(context, hook); + String targetApp = hook.getPhaseConfigs() + .stream() + .filter(config -> currentPhase.equals(config.get("phase"))) + .map(config -> config.get("target-app")) + .findFirst() + .orElse(null); + + if (targetApp == null) { + return app.getName(); + } + + return resolveAppNameForTarget(context, app, targetApp); + } + + private String buildCurrentPhaseString(ProcessContext context, Hook hook) { + // The hook's own phase string already contains the correct deployment type prefix. + // Use it directly rather than reconstructing from context variables that may be + // unavailable in the hook subprocess (e.g. SERVICE_ID absent → null process type). + return hook.getPhases() + .stream() + .filter(p -> p.contains(".application.")) + .findFirst() + .orElse(""); + } + + private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { + ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); + ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); + + if (idleColor == null || liveColor == null) { + return app.getName(); + } + + if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { + return swapColorSuffix(app.getName(), liveColor, idleColor); + } + if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { + return swapColorSuffix(app.getName(), idleColor, liveColor); + } + return app.getName(); + } + + private String swapColorSuffix(String appName, ApplicationColor fromColor, ApplicationColor toColor) { + String fromSuffix = fromColor.asSuffix(); + String toSuffix = toColor.asSuffix(); + if (appName.endsWith(fromSuffix)) { + return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; + } + return appName; + } + @Override protected String getStepErrorMessage(ProcessContext context) { CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java index f4889b1c3c..c4cf10fe61 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java @@ -20,4 +20,8 @@ public ProcessType determineDeploymentType(ProcessContext context) { return processTypeParser.getProcessType(context.getExecution()); } + public ProcessType determineDeploymentTypeSafely(ProcessContext context) { + return processTypeParser.getProcessType(context.getExecution(), false); + } + } diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index e9522fa8ff..f43ecd459b 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -24,6 +24,10 @@ + + + + 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 5015ce6d84..cc38abce03 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 @@ -115,6 +115,10 @@ + + + + @@ -143,6 +147,10 @@ + + + + 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 index f6dfd51040..c2f200a6f0 100644 --- 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 @@ -57,6 +57,9 @@ + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index 5062e65228..f69fc3daf1 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -25,6 +25,10 @@ + + + + @@ -55,6 +59,10 @@ + + + + diff --git a/pom.xml b/pom.xml index 676c05301a..b0a52f6ab2 100644 --- a/pom.xml +++ b/pom.xml @@ -494,7 +494,7 @@ commons-collections4 ${commons-collections4.version} - + org.apache.httpcomponents.core5 httpcore5 From 8e768cd2a2c34e0bdf1448c4a89c6a5ab3ad4d57 Mon Sep 17 00:00:00 2001 From: stiv03 Date: Fri, 3 Apr 2026 10:56:57 +0300 Subject: [PATCH 02/10] Fix phase-configs target-app resolution for multi-phase hooks buildCurrentPhaseString was always picking the first phase from hook.getPhases(), causing wrong target-app resolution when a hook registered for multiple phases fired during a later phase. Also fixed getDeploymentTypeString returning "deploy" instead of "blue-green" inside hook subprocesses where SERVICE_ID is absent. Introduced HOOK_EXECUTION_PHASE variable set by HooksExecutor to record which phase triggered the current hook execution. ExecuteTaskStep now matches against this value to find the correct phase-config entry. Variable is passed to all hook call activities in deploy-app, undeploy-app, backup-existing-app and stop-dependent-modules BPMNs. --- .../controller/process/steps/ExecuteTaskStep.java | 12 +++++++----- .../controller/process/util/HooksExecutor.java | 5 +++++ .../controller/process/variables/Variables.java | 3 +++ .../controller/process/backup-existing-app.bpmn | 3 ++- .../multiapps/controller/process/deploy-app.bpmn | 2 ++ .../controller/process/stop-dependent-modules.bpmn | 1 + .../multiapps/controller/process/undeploy-app.bpmn | 2 ++ 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index a333517d90..69dc593d43 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -69,14 +69,16 @@ private String resolveTargetAppName(ProcessContext context, CloudApplicationExte } private String buildCurrentPhaseString(ProcessContext context, Hook hook) { - // The hook's own phase string already contains the correct deployment type prefix. - // Use it directly rather than reconstructing from context variables that may be - // unavailable in the hook subprocess (e.g. SERVICE_ID absent → null process type). + String hookExecutionPhase = context.getVariable(Variables.HOOK_EXECUTION_PHASE); return hook.getPhases() .stream() - .filter(p -> p.contains(".application.")) + .filter(p -> p.equals(hookExecutionPhase)) .findFirst() - .orElse(""); + .orElseGet(() -> hook.getPhases() + .stream() + .filter(p -> p.contains(".application.")) + .findFirst() + .orElse("")); } private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java index 001209579d..8ccf349c28 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java @@ -50,6 +50,11 @@ private List executeHooks(StepPhase currentStepPhase) { moduleToDeploy.getName()); updateExecutedHooksForModule(alreadyExecutedHooksForModule, hooksWithPhases.getHookPhases(), hooksWithPhases.getHooks()); context.setVariable(Variables.HOOKS_FOR_EXECUTION, hooksWithPhases.getHooks()); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, hooksWithPhases.getHookPhases() + .stream() + .findFirst() + .map(HookPhase::getValue) + .orElse(null)); return hooksWithPhases.getHooks(); } 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 8c35932c48..ff895a7232 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 @@ -580,6 +580,9 @@ public interface Variables { .type(Variable.typeReference(Hook.class)) .defaultValue(Collections.emptyList()) .build(); + Variable HOOK_EXECUTION_PHASE = ImmutableSimpleVariable. builder() + .name("hookExecutionPhase") + .build(); Variable> MODULES_TO_DEPLOY = ImmutableJsonBinaryListVariable. builder() .name("modulesToDeploy") .type(Variable.typeReference(Module.class)) diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index f43ecd459b..d9724a2826 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -26,7 +26,8 @@ - + + 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 cc38abce03..bae2ad3664 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 @@ -117,6 +117,7 @@ + @@ -149,6 +150,7 @@ + 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 index c2f200a6f0..68ffddb76e 100644 --- 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 @@ -58,6 +58,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index f69fc3daf1..e431438330 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -27,6 +27,7 @@ + @@ -61,6 +62,7 @@ + From ceef42aa38f98647e4825d10602d49fd2738654f Mon Sep 17 00:00:00 2001 From: stiv03 Date: Wed, 8 Apr 2026 15:05:03 +0300 Subject: [PATCH 03/10] Fix phase-configs target-app routing when live app has no color suffix --- .../multiapps/controller/process/steps/ExecuteTaskStep.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 69dc593d43..38de854e12 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -104,6 +104,9 @@ private String swapColorSuffix(String appName, ApplicationColor fromColor, Appli if (appName.endsWith(fromSuffix)) { return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; } + if (!appName.endsWith(toSuffix)) { + return appName + toSuffix; + } return appName; } From eab5ad411a8b199fe7fcdaee4224553a16eba900 Mon Sep 17 00:00:00 2001 From: stiv03 Date: Wed, 15 Apr 2026 16:49:58 +0300 Subject: [PATCH 04/10] Add phases-config hook parameter support with duplicate phase validation --- .../core/model/SupportedParameters.java | 3 +- .../controller/process/Messages.java | 1 + .../process/steps/ExecuteTaskStep.java | 42 ++++++++++++++----- .../process/steps/MergeDescriptorsStep.java | 31 ++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) 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 23b3079142..5e9c684f8d 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 @@ -218,8 +218,9 @@ public class SupportedParameters { SERVICE_BINDING_CONFIG, DELETE_SERVICE_KEY_AFTER_DEPLOYMENT); public static final String HOOK_TARGET_APP = "hook-target-app"; + public static final String PHASES_CONFIG = "phases-config"; - public static final Set MODULE_HOOK_PARAMETERS = Set.of(NAME, COMMAND, MEMORY, DISK_QUOTA, HOOK_REQUIRES, HOOK_TARGET_APP); + public static final Set MODULE_HOOK_PARAMETERS = Set.of(NAME, COMMAND, MEMORY, DISK_QUOTA, HOOK_REQUIRES, HOOK_TARGET_APP, PHASES_CONFIG); public static final Set CONFIGURATION_REFERENCE_PARAMETERS = Set.of(PROVIDER_NID, PROVIDER_ID, TARGET, VERSION, DEPRECATED_CONFIG_MTA_ID, DEPRECATED_CONFIG_MTA_VERSION, 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 e06960b330..acfa5f08ff 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 @@ -149,6 +149,7 @@ public class Messages { public static final String ERROR_PREPARING_TO_EXECUTE_TASKS_ON_APP = "Error preparing to execute tasks on application \"{0}\""; public static final String ERROR_PREPARING_TO_RESTART_SERVICE_BROKER_SUBSCRIBERS = "Error preparing to restart service broker subscribers"; public static final String ERROR_EXECUTING_TASK_0_ON_APP_1 = "Execution of task \"{0}\" on application \"{1}\" failed."; + public static final String DUPLICATE_PHASE_IN_PHASES_CONFIG = "Duplicate phase \"{0}\" in \"phases-config\" of hook \"{1}\". Only one target-app per phase is supported."; public static final String ERROR_DETECTING_COMPONENTS_TO_UNDEPLOY = "Error detecting components to undeploy"; public static final String ERROR_DELETING_IDLE_ROUTES = "Error deleting idle routes"; public static final String ERROR_CREATING_SERVICE_BROKERS = "Error creating service brokers"; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 38de854e12..82e9e6b548 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -3,6 +3,7 @@ import java.text.MessageFormat; import java.time.Duration; import java.util.List; +import java.util.Map; import java.util.function.LongSupplier; import jakarta.inject.Inject; @@ -13,6 +14,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; import org.cloudfoundry.multiapps.controller.core.model.HookPhaseProcessType; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; @@ -27,6 +29,10 @@ public class ExecuteTaskStep extends TimeoutAsyncFlowableStep { protected LongSupplier currentTimeSupplier = System::currentTimeMillis; + private static final String PHASE_KEY = "phase"; + private static final String TARGET_APP_KEY = "target-app"; + private static final String APPLICATION_PHASE_SUBSTRING = ".application."; + @Inject private CloudControllerClientFactory clientFactory; @Inject @@ -49,17 +55,21 @@ protected StepPhase executeAsyncStep(ProcessContext context) { private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app) { Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); - if (hook == null || hook.getPhaseConfigs().isEmpty()) { + if (hook == null) { + return app.getName(); + } + + List> phasesConfig = getPhasesConfig(hook); + if (phasesConfig.isEmpty()) { return app.getName(); } String currentPhase = buildCurrentPhaseString(context, hook); - String targetApp = hook.getPhaseConfigs() - .stream() - .filter(config -> currentPhase.equals(config.get("phase"))) - .map(config -> config.get("target-app")) - .findFirst() - .orElse(null); + String targetApp = phasesConfig.stream() + .filter(config -> currentPhase.equals(config.get(PHASE_KEY))) + .map(config -> config.get(TARGET_APP_KEY)) + .findFirst() + .orElse(null); if (targetApp == null) { return app.getName(); @@ -68,6 +78,16 @@ private String resolveTargetAppName(ProcessContext context, CloudApplicationExte return resolveAppNameForTarget(context, app, targetApp); } + @SuppressWarnings("unchecked") + private List> getPhasesConfig(Hook hook) { + Object value = hook.getParameters() + .get(SupportedParameters.PHASES_CONFIG); + if (value instanceof List) { + return (List>) value; + } + return List.of(); + } + private String buildCurrentPhaseString(ProcessContext context, Hook hook) { String hookExecutionPhase = context.getVariable(Variables.HOOK_EXECUTION_PHASE); return hook.getPhases() @@ -76,7 +96,7 @@ private String buildCurrentPhaseString(ProcessContext context, Hook hook) { .findFirst() .orElseGet(() -> hook.getPhases() .stream() - .filter(p -> p.contains(".application.")) + .filter(p -> p.contains(APPLICATION_PHASE_SUBSTRING)) .findFirst() .orElse("")); } @@ -89,10 +109,12 @@ private String resolveAppNameForTarget(ProcessContext context, CloudApplicationE return app.getName(); } - if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { + if (HookPhaseProcessType.HookProcessPhase.IDLE.getType() + .equals(targetApp)) { return swapColorSuffix(app.getName(), liveColor, idleColor); } - if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { + if (HookPhaseProcessType.HookProcessPhase.LIVE.getType() + .equals(targetApp)) { return swapColorSuffix(app.getName(), idleColor, liveColor); } return app.getName(); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java index aeba1c87fa..f58924f348 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java @@ -1,6 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -8,7 +9,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; +import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.cf.CloudHandlerFactory; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.helpers.MtaDescriptorMerger; import org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptor; import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableBackupDescriptor; @@ -20,6 +23,8 @@ import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.ExtensionDescriptor; +import org.cloudfoundry.multiapps.mta.model.Hook; +import org.cloudfoundry.multiapps.mta.model.Module; import org.cloudfoundry.multiapps.mta.model.Platform; import org.cloudfoundry.multiapps.mta.resolvers.ReferenceContainer; import org.cloudfoundry.multiapps.mta.resolvers.ReferencesFinder; @@ -59,6 +64,7 @@ protected StepPhase executeStep(ProcessContext context) { .toList()); context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, descriptor); + validatePhasesConfig(context, descriptor); warnForUnsupportedParameters(descriptor); backupDeploymentDescriptor(context, descriptor); @@ -66,6 +72,31 @@ protected StepPhase executeStep(ProcessContext context) { return StepPhase.DONE; } + private void validatePhasesConfig(ProcessContext context, DeploymentDescriptor descriptor) { + if (context.getVariable(Variables.MTA_MAJOR_SCHEMA_VERSION) < 3) { + return; + } + descriptor.getModules() + .stream() + .flatMap(module -> module.getHooks().stream()) + .forEach(this::validateNoDuplicatePhases); + } + + @SuppressWarnings("unchecked") + private void validateNoDuplicatePhases(Hook hook) { + Object value = hook.getParameters().get(SupportedParameters.PHASES_CONFIG); + if (!(value instanceof List)) { + return; + } + Set seen = new HashSet<>(); + for (Map entry : (List>) value) { + String phase = entry.get("phase"); + if (phase != null && !seen.add(phase)) { + throw new SLException(MessageFormat.format(Messages.DUPLICATE_PHASE_IN_PHASES_CONFIG, phase, hook.getName())); + } + } + } + private void warnForUnsupportedParameters(DeploymentDescriptor descriptor) { List references = new ReferencesFinder().getAllReferences(descriptor); Map> unsupportedParameters = unsupportedParameterFinder.findUnsupportedParameters(descriptor, From 9165d29697e973b2c708169f3da6674aa5bc888c Mon Sep 17 00:00:00 2001 From: stiv03 Date: Mon, 20 Apr 2026 11:07:34 +0300 Subject: [PATCH 05/10] Refactoring --- .../process/steps/ExecuteTaskStep.java | 8 +++---- .../process/steps/MergeDescriptorsStep.java | 22 +++++++++++-------- pom.xml | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 82e9e6b548..061d041bbe 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -80,10 +80,10 @@ private String resolveTargetAppName(ProcessContext context, CloudApplicationExte @SuppressWarnings("unchecked") private List> getPhasesConfig(Hook hook) { - Object value = hook.getParameters() - .get(SupportedParameters.PHASES_CONFIG); - if (value instanceof List) { - return (List>) value; + Object phasesConfigValue = hook.getParameters() + .get(SupportedParameters.PHASES_CONFIG); + if (phasesConfigValue instanceof List) { + return (List>) phasesConfigValue; } return List.of(); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java index f58924f348..244c9c9626 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java @@ -35,6 +35,9 @@ @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class MergeDescriptorsStep extends SyncFlowableStep { + private static final int HOOKS_MIN_SCHEMA_VERSION = 3; + private static final String PHASES_CONFIG_PHASE_KEY = "phase"; + @Inject private DescriptorBackupService descriptorBackupService; @@ -73,25 +76,26 @@ protected StepPhase executeStep(ProcessContext context) { } private void validatePhasesConfig(ProcessContext context, DeploymentDescriptor descriptor) { - if (context.getVariable(Variables.MTA_MAJOR_SCHEMA_VERSION) < 3) { + if (context.getVariable(Variables.MTA_MAJOR_SCHEMA_VERSION) < HOOKS_MIN_SCHEMA_VERSION) { return; } descriptor.getModules() .stream() .flatMap(module -> module.getHooks().stream()) - .forEach(this::validateNoDuplicatePhases); + .forEach(this::validateHookHasNoDuplicatePhaseConfigs); } @SuppressWarnings("unchecked") - private void validateNoDuplicatePhases(Hook hook) { - Object value = hook.getParameters().get(SupportedParameters.PHASES_CONFIG); - if (!(value instanceof List)) { + private void validateHookHasNoDuplicatePhaseConfigs(Hook hook) { + Object phasesConfigValue = hook.getParameters().get(SupportedParameters.PHASES_CONFIG); + if (!(phasesConfigValue instanceof List)) { return; } - Set seen = new HashSet<>(); - for (Map entry : (List>) value) { - String phase = entry.get("phase"); - if (phase != null && !seen.add(phase)) { + List> phasesConfig = (List>) phasesConfigValue; + Set seenPhases = new HashSet<>(); + for (Map phaseConfig : phasesConfig) { + String phase = phaseConfig.get(PHASES_CONFIG_PHASE_KEY); + if (phase != null && !seenPhases.add(phase)) { throw new SLException(MessageFormat.format(Messages.DUPLICATE_PHASE_IN_PHASES_CONFIG, phase, hook.getName())); } } diff --git a/pom.xml b/pom.xml index b0a52f6ab2..676c05301a 100644 --- a/pom.xml +++ b/pom.xml @@ -494,7 +494,7 @@ commons-collections4 ${commons-collections4.version} - + org.apache.httpcomponents.core5 httpcore5 From aab7ff0f85ba21e6cbdf353f04ed7baccfe8445f Mon Sep 17 00:00:00 2001 From: stiv03 Date: Mon, 20 Apr 2026 15:10:19 +0300 Subject: [PATCH 06/10] Extract hook task target app resolution into dedicated step --- .../process/steps/ExecuteTaskStep.java | 94 +------------ .../steps/ResolveHookTaskTargetAppStep.java | 126 ++++++++++++++++++ .../process/execute-hook-tasks.bpmn | 4 +- 3 files changed, 131 insertions(+), 93 deletions(-) create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 061d041bbe..57f2e88526 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -3,7 +3,6 @@ import java.text.MessageFormat; import java.time.Duration; import java.util.List; -import java.util.Map; import java.util.function.LongSupplier; import jakarta.inject.Inject; @@ -12,14 +11,10 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; -import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; -import org.cloudfoundry.multiapps.controller.core.model.HookPhaseProcessType; -import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; -import org.cloudfoundry.multiapps.mta.model.Hook; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @@ -29,10 +24,6 @@ public class ExecuteTaskStep extends TimeoutAsyncFlowableStep { protected LongSupplier currentTimeSupplier = System::currentTimeMillis; - private static final String PHASE_KEY = "phase"; - private static final String TARGET_APP_KEY = "target-app"; - private static final String APPLICATION_PHASE_SUBSTRING = ".application."; - @Inject private CloudControllerClientFactory clientFactory; @Inject @@ -44,94 +35,13 @@ protected StepPhase executeAsyncStep(ProcessContext context) { CloudTask taskToExecute = StepsUtil.getTask(context); CloudControllerClient client = context.getControllerClient(); - String appName = resolveTargetAppName(context, app); - - getStepLogger().info(Messages.EXECUTING_TASK_ON_APP, taskToExecute.getName(), appName); - CloudTask startedTask = client.runTask(appName, taskToExecute); + getStepLogger().info(Messages.EXECUTING_TASK_ON_APP, taskToExecute.getName(), app.getName()); + CloudTask startedTask = client.runTask(app.getName(), taskToExecute); context.setVariable(Variables.STARTED_TASK, startedTask); context.setVariable(Variables.START_TIME, currentTimeSupplier.getAsLong()); return StepPhase.POLL; } - private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app) { - Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); - if (hook == null) { - return app.getName(); - } - - List> phasesConfig = getPhasesConfig(hook); - if (phasesConfig.isEmpty()) { - return app.getName(); - } - - String currentPhase = buildCurrentPhaseString(context, hook); - String targetApp = phasesConfig.stream() - .filter(config -> currentPhase.equals(config.get(PHASE_KEY))) - .map(config -> config.get(TARGET_APP_KEY)) - .findFirst() - .orElse(null); - - if (targetApp == null) { - return app.getName(); - } - - return resolveAppNameForTarget(context, app, targetApp); - } - - @SuppressWarnings("unchecked") - private List> getPhasesConfig(Hook hook) { - Object phasesConfigValue = hook.getParameters() - .get(SupportedParameters.PHASES_CONFIG); - if (phasesConfigValue instanceof List) { - return (List>) phasesConfigValue; - } - return List.of(); - } - - private String buildCurrentPhaseString(ProcessContext context, Hook hook) { - String hookExecutionPhase = context.getVariable(Variables.HOOK_EXECUTION_PHASE); - return hook.getPhases() - .stream() - .filter(p -> p.equals(hookExecutionPhase)) - .findFirst() - .orElseGet(() -> hook.getPhases() - .stream() - .filter(p -> p.contains(APPLICATION_PHASE_SUBSTRING)) - .findFirst() - .orElse("")); - } - - private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { - ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); - ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); - - if (idleColor == null || liveColor == null) { - return app.getName(); - } - - if (HookPhaseProcessType.HookProcessPhase.IDLE.getType() - .equals(targetApp)) { - return swapColorSuffix(app.getName(), liveColor, idleColor); - } - if (HookPhaseProcessType.HookProcessPhase.LIVE.getType() - .equals(targetApp)) { - return swapColorSuffix(app.getName(), idleColor, liveColor); - } - return app.getName(); - } - - private String swapColorSuffix(String appName, ApplicationColor fromColor, ApplicationColor toColor) { - String fromSuffix = fromColor.asSuffix(); - String toSuffix = toColor.asSuffix(); - if (appName.endsWith(fromSuffix)) { - return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; - } - if (!appName.endsWith(toSuffix)) { - return appName + toSuffix; - } - return appName; - } - @Override protected String getStepErrorMessage(ProcessContext context) { CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java new file mode 100644 index 0000000000..e33dfb1c47 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java @@ -0,0 +1,126 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; +import org.cloudfoundry.multiapps.controller.core.model.HookPhaseProcessType; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Hook; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +@Named("resolveHookTaskTargetAppStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class ResolveHookTaskTargetAppStep extends SyncFlowableStep { + + private static final String PHASE_KEY = "phase"; + private static final String TARGET_APP_KEY = "target-app"; + private static final String APPLICATION_PHASE_SUBSTRING = ".application."; + + @Override + protected StepPhase executeStep(ProcessContext context) { + CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); + Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); + + String resolvedAppName = resolveTargetAppName(context, app, hook); + if (!resolvedAppName.equals(app.getName())) { + context.setVariable(Variables.APP_TO_PROCESS, buildAppWithName(app, resolvedAppName)); + } + + return StepPhase.DONE; + } + + private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app, Hook hook) { + if (hook == null) { + return app.getName(); + } + + List> phasesConfig = getPhasesConfig(hook); + if (phasesConfig.isEmpty()) { + return app.getName(); + } + + String currentPhase = buildCurrentPhaseString(context, hook); + String targetApp = phasesConfig.stream() + .filter(config -> currentPhase.equals(config.get(PHASE_KEY))) + .map(config -> config.get(TARGET_APP_KEY)) + .findFirst() + .orElse(null); + + if (targetApp == null) { + return app.getName(); + } + + return resolveAppNameForTarget(context, app, targetApp); + } + + @SuppressWarnings("unchecked") + private List> getPhasesConfig(Hook hook) { + Object phasesConfigValue = hook.getParameters() + .get(SupportedParameters.PHASES_CONFIG); + if (phasesConfigValue instanceof List) { + return (List>) phasesConfigValue; + } + return List.of(); + } + + private String buildCurrentPhaseString(ProcessContext context, Hook hook) { + String hookExecutionPhase = context.getVariable(Variables.HOOK_EXECUTION_PHASE); + return hook.getPhases() + .stream() + .filter(p -> p.equals(hookExecutionPhase)) + .findFirst() + .orElseGet(() -> hook.getPhases() + .stream() + .filter(p -> p.contains(APPLICATION_PHASE_SUBSTRING)) + .findFirst() + .orElse("")); + } + + private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { + ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); + ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); + + if (idleColor == null || liveColor == null) { + return app.getName(); + } + + if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { + return swapColorSuffix(app.getName(), liveColor, idleColor); + } + if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { + return swapColorSuffix(app.getName(), idleColor, liveColor); + } + return app.getName(); + } + + private String swapColorSuffix(String appName, ApplicationColor fromColor, ApplicationColor toColor) { + String fromSuffix = fromColor.asSuffix(); + String toSuffix = toColor.asSuffix(); + if (appName.endsWith(fromSuffix)) { + return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; + } + if (!appName.endsWith(toSuffix)) { + return appName + toSuffix; + } + return appName; + } + + private CloudApplicationExtended buildAppWithName(CloudApplicationExtended app, String resolvedAppName) { + return org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended.copyOf(app) + .withName(resolvedAppName); + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return MessageFormat.format(Messages.ERROR_EXECUTING_HOOK, + context.getVariable(Variables.HOOK_FOR_EXECUTION).getName()); + } + +} diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/execute-hook-tasks.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/execute-hook-tasks.bpmn index 4408c082ff..927add0de7 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/execute-hook-tasks.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/execute-hook-tasks.bpmn @@ -9,13 +9,15 @@ + - + + From 695aea6c5cafaa0340321ebd1928772fb2365d17 Mon Sep 17 00:00:00 2001 From: stiv03 Date: Tue, 21 Apr 2026 12:05:21 +0300 Subject: [PATCH 07/10] Fix phases-config target app resolution for cf deploy --strategy blue-green --- .../steps/ResolveHookTaskTargetAppStep.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java index e33dfb1c47..cb05d9db28 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java @@ -7,6 +7,7 @@ import jakarta.inject.Named; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; +import org.cloudfoundry.multiapps.controller.core.model.BlueGreenApplicationNameSuffix; import org.cloudfoundry.multiapps.controller.core.model.HookPhaseProcessType; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.process.Messages; @@ -88,7 +89,7 @@ private String resolveAppNameForTarget(ProcessContext context, CloudApplicationE ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); if (idleColor == null || liveColor == null) { - return app.getName(); + return resolveAppNameWithLiveIdleSuffix(app.getName(), targetApp); } if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { @@ -100,6 +101,29 @@ private String resolveAppNameForTarget(ProcessContext context, CloudApplicationE return app.getName(); } + private String resolveAppNameWithLiveIdleSuffix(String appName, String targetApp) { + String liveSuffix = BlueGreenApplicationNameSuffix.LIVE.asSuffix(); + String idleSuffix = BlueGreenApplicationNameSuffix.IDLE.asSuffix(); + + if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { + if (appName.endsWith(liveSuffix)) { + return appName.substring(0, appName.length() - liveSuffix.length()); + } + if (!appName.endsWith(idleSuffix)) { + return appName + idleSuffix; + } + } + if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { + if (appName.endsWith(idleSuffix)) { + return appName.substring(0, appName.length() - idleSuffix.length()); + } + if (!appName.endsWith(liveSuffix)) { + return appName + liveSuffix; + } + } + return appName; + } + private String swapColorSuffix(String appName, ApplicationColor fromColor, ApplicationColor toColor) { String fromSuffix = fromColor.asSuffix(); String toSuffix = toColor.asSuffix(); From 26e0d65d7e4cd454cfe9d484da3e813ca0e69752 Mon Sep 17 00:00:00 2001 From: stiv03 Date: Tue, 21 Apr 2026 13:32:32 +0300 Subject: [PATCH 08/10] Add unit tests for ResolveHookTaskTargetAppStep --- .../ResolveHookTaskTargetAppStepTest.java | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java new file mode 100644 index 0000000000..ae96a5414c --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java @@ -0,0 +1,247 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; +import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Hook; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResolveHookTaskTargetAppStepTest extends SyncFlowableStepTest { + + private static final String BEFORE_STOP_LIVE = "blue-green.application.before-stop.live"; + private static final String BEFORE_START_IDLE = "blue-green.application.before-start.idle"; + + private static final String PHASE_KEY = "phase"; + private static final String TARGET_APP_KEY = "target-app"; + private static final String TARGET_IDLE = "idle"; + private static final String TARGET_LIVE = "live"; + + private static final String HOOK_NAME = "test-hook"; + private static final String APP_BASE_NAME = "my-app"; + private static final String APP_NAME_GREEN = "my-app-green"; + private static final String APP_NAME_BLUE = "my-app-blue"; + private static final String APP_NAME_LIVE = "my-app-live"; + private static final String APP_NAME_IDLE = "my-app-idle"; + + @Override + protected ResolveHookTaskTargetAppStep createStep() { + return new ResolveHookTaskTargetAppStep(); + } + + @Test + void appNameUnchangedWhenNoHookSet() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_BASE_NAME)); + context.setVariable(Variables.HOOK_FOR_EXECUTION, null); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_BASE_NAME, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void appNameUnchangedWhenHookHasNoPhasesConfig() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_BASE_NAME)); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHook(List.of(BEFORE_STOP_LIVE), Map.of())); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_BASE_NAME, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void appNameUnchangedWhenNoPhasesConfigMatchesCurrentPhase() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_LIVE)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_START_IDLE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_LIVE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void bgDeploy_targetIdle_resolvesToIdleColorApp() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_GREEN)); + context.setVariable(Variables.IDLE_MTA_COLOR, ApplicationColor.BLUE); + context.setVariable(Variables.LIVE_MTA_COLOR, ApplicationColor.GREEN); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_STOP_LIVE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_BLUE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void bgDeploy_targetLive_resolvesToLiveColorApp() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_BLUE)); + context.setVariable(Variables.IDLE_MTA_COLOR, ApplicationColor.BLUE); + context.setVariable(Variables.LIVE_MTA_COLOR, ApplicationColor.GREEN); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_START_IDLE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_START_IDLE), + List.of(Map.of(PHASE_KEY, BEFORE_START_IDLE, TARGET_APP_KEY, TARGET_LIVE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_GREEN, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void bgDeploy_targetIdle_appHasNoSuffix_appendsIdleColorSuffix() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_BASE_NAME)); + context.setVariable(Variables.IDLE_MTA_COLOR, ApplicationColor.BLUE); + context.setVariable(Variables.LIVE_MTA_COLOR, ApplicationColor.GREEN); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_STOP_LIVE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_BLUE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetIdle_appHasLiveSuffix_stripsLiveSuffix() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_LIVE)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_STOP_LIVE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_BASE_NAME, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetIdle_appHasNoSuffix_appendsIdleSuffix() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_BASE_NAME)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_STOP_LIVE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_IDLE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetIdle_appAlreadyHasIdleSuffix_unchanged() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_IDLE)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_STOP_LIVE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_STOP_LIVE), + List.of(Map.of(PHASE_KEY, BEFORE_STOP_LIVE, TARGET_APP_KEY, TARGET_IDLE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_IDLE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetLive_appHasIdleSuffix_stripsIdleSuffix() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_IDLE)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_START_IDLE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_START_IDLE), + List.of(Map.of(PHASE_KEY, BEFORE_START_IDLE, TARGET_APP_KEY, TARGET_LIVE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_BASE_NAME, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetLive_appAlreadyHasLiveSuffix_unchanged() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_NAME_LIVE)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_START_IDLE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_START_IDLE), + List.of(Map.of(PHASE_KEY, BEFORE_START_IDLE, TARGET_APP_KEY, TARGET_LIVE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_LIVE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + @Test + void strategyBgDeploy_targetLive_appHasNoSuffix_appendsLiveSuffix() { + context.setVariable(Variables.APP_TO_PROCESS, buildApp(APP_BASE_NAME)); + context.setVariable(Variables.HOOK_EXECUTION_PHASE, BEFORE_START_IDLE); + context.setVariable(Variables.HOOK_FOR_EXECUTION, buildHookWithPhasesConfig( + List.of(BEFORE_START_IDLE), + List.of(Map.of(PHASE_KEY, BEFORE_START_IDLE, TARGET_APP_KEY, TARGET_LIVE)) + )); + + step.execute(execution); + + assertStepFinishedSuccessfully(); + assertEquals(APP_NAME_LIVE, context.getVariable(Variables.APP_TO_PROCESS) + .getName()); + } + + private CloudApplicationExtended buildApp(String name) { + return ImmutableCloudApplicationExtended.builder() + .name(name) + .build(); + } + + private Hook buildHook(List phases, Map parameters) { + return Hook.createV3() + .setName(HOOK_NAME) + .setType("task") + .setPhases(phases) + .setParameters(parameters); + } + + private Hook buildHookWithPhasesConfig(List phases, + List> phasesConfig) { + return buildHook(phases, Map.of(SupportedParameters.PHASES_CONFIG, phasesConfig)); + } + +} From 80b634ab3f3f47e484857bca10f357599c177cc8 Mon Sep 17 00:00:00 2001 From: stiv03 Date: Thu, 23 Apr 2026 11:55:54 +0300 Subject: [PATCH 09/10] Fix comments --- .../controller/process/Messages.java | 2 + .../process/steps/MergeDescriptorsStep.java | 37 +------- .../steps/ResolveHookTaskTargetAppStep.java | 90 ++++++++----------- .../util/DeploymentTypeDeterminer.java | 4 - .../util/HookPhasesConfigValidator.java | 45 ++++++++++ .../process/util/HooksExecutor.java | 5 +- .../process/variables/Variables.java | 7 +- .../process/backup-existing-app.bpmn | 2 +- .../controller/process/deploy-app.bpmn | 4 +- .../process/stop-dependent-modules.bpmn | 2 +- .../controller/process/undeploy-app.bpmn | 4 +- .../ResolveHookTaskTargetAppStepTest.java | 57 +++++++++--- 12 files changed, 142 insertions(+), 117 deletions(-) create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java 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 acfa5f08ff..79b4700e73 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 @@ -150,6 +150,7 @@ public class Messages { public static final String ERROR_PREPARING_TO_RESTART_SERVICE_BROKER_SUBSCRIBERS = "Error preparing to restart service broker subscribers"; public static final String ERROR_EXECUTING_TASK_0_ON_APP_1 = "Execution of task \"{0}\" on application \"{1}\" failed."; public static final String DUPLICATE_PHASE_IN_PHASES_CONFIG = "Duplicate phase \"{0}\" in \"phases-config\" of hook \"{1}\". Only one target-app per phase is supported."; + public static final String INVALID_PHASES_CONFIG_NOT_A_LIST = "Parameter \"phases-config\" of hook \"{0}\" must be a list."; public static final String ERROR_DETECTING_COMPONENTS_TO_UNDEPLOY = "Error detecting components to undeploy"; public static final String ERROR_DELETING_IDLE_ROUTES = "Error deleting idle routes"; public static final String ERROR_CREATING_SERVICE_BROKERS = "Error creating service brokers"; @@ -410,6 +411,7 @@ public class Messages { public static final String WILL_NOT_REBIND_APP_TO_SERVICE_SAME_PARAMETERS = "Service instance \"{0}\" will not be rebound to application \"{1}\" because the binding parameters are not modified"; public static final String SERVICE_BROKER_DOES_NOT_EXIST = "Service broker with name \"{0}\" does not exist"; public static final String EXECUTING_HOOK_0 = "Executing hook \"{0}\""; + public static final String SKIPPING_HOOK_TASK_NO_LIVE_APP = "Skipping hook task on application \"{0}\" with target-app \"live\": no live application exists yet (initial deployment)"; public static final String WAITING_PREVIOUS_OPERATIONS_TO_FINISH = "Waiting for previous service operations to finish..."; public static final String ASYNC_OPERATION_FOR_SERVICE_BROKER_FINISHED = "Async operation for service broker \"{0}\" has finished"; public static final String STARTING_INCREMENTAL_APPLICATION_INSTANCE_UPDATE_FOR_0 = "Starting incremental application instance update for \"{0}\"..."; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java index 244c9c9626..616573986b 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/MergeDescriptorsStep.java @@ -1,7 +1,6 @@ package org.cloudfoundry.multiapps.controller.process.steps; import java.text.MessageFormat; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -9,22 +8,19 @@ import jakarta.inject.Inject; import jakarta.inject.Named; -import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.cf.CloudHandlerFactory; -import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.helpers.MtaDescriptorMerger; import org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptor; import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableBackupDescriptor; import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.security.SecretParametersCollector; +import org.cloudfoundry.multiapps.controller.process.util.HookPhasesConfigValidator; import org.cloudfoundry.multiapps.controller.process.util.NamespaceGlobalParameters; import org.cloudfoundry.multiapps.controller.process.util.UnsupportedParameterFinder; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.ExtensionDescriptor; -import org.cloudfoundry.multiapps.mta.model.Hook; -import org.cloudfoundry.multiapps.mta.model.Module; import org.cloudfoundry.multiapps.mta.model.Platform; import org.cloudfoundry.multiapps.mta.resolvers.ReferenceContainer; import org.cloudfoundry.multiapps.mta.resolvers.ReferencesFinder; @@ -36,7 +32,6 @@ public class MergeDescriptorsStep extends SyncFlowableStep { private static final int HOOKS_MIN_SCHEMA_VERSION = 3; - private static final String PHASES_CONFIG_PHASE_KEY = "phase"; @Inject private DescriptorBackupService descriptorBackupService; @@ -67,7 +62,9 @@ protected StepPhase executeStep(ProcessContext context) { .toList()); context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, descriptor); - validatePhasesConfig(context, descriptor); + if (context.getVariable(Variables.MTA_MAJOR_SCHEMA_VERSION) >= HOOKS_MIN_SCHEMA_VERSION) { + new HookPhasesConfigValidator().validate(descriptor); + } warnForUnsupportedParameters(descriptor); backupDeploymentDescriptor(context, descriptor); @@ -75,32 +72,6 @@ protected StepPhase executeStep(ProcessContext context) { return StepPhase.DONE; } - private void validatePhasesConfig(ProcessContext context, DeploymentDescriptor descriptor) { - if (context.getVariable(Variables.MTA_MAJOR_SCHEMA_VERSION) < HOOKS_MIN_SCHEMA_VERSION) { - return; - } - descriptor.getModules() - .stream() - .flatMap(module -> module.getHooks().stream()) - .forEach(this::validateHookHasNoDuplicatePhaseConfigs); - } - - @SuppressWarnings("unchecked") - private void validateHookHasNoDuplicatePhaseConfigs(Hook hook) { - Object phasesConfigValue = hook.getParameters().get(SupportedParameters.PHASES_CONFIG); - if (!(phasesConfigValue instanceof List)) { - return; - } - List> phasesConfig = (List>) phasesConfigValue; - Set seenPhases = new HashSet<>(); - for (Map phaseConfig : phasesConfig) { - String phase = phaseConfig.get(PHASES_CONFIG_PHASE_KEY); - if (phase != null && !seenPhases.add(phase)) { - throw new SLException(MessageFormat.format(Messages.DUPLICATE_PHASE_IN_PHASES_CONFIG, phase, hook.getName())); - } - } - } - private void warnForUnsupportedParameters(DeploymentDescriptor descriptor) { List references = new ReferencesFinder().getAllReferences(descriptor); Map> unsupportedParameters = unsupportedParameterFinder.findUnsupportedParameters(descriptor, diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java index cb05d9db28..a34126acbb 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStep.java @@ -22,14 +22,16 @@ public class ResolveHookTaskTargetAppStep extends SyncFlowableStep { private static final String PHASE_KEY = "phase"; private static final String TARGET_APP_KEY = "target-app"; - private static final String APPLICATION_PHASE_SUBSTRING = ".application."; @Override protected StepPhase executeStep(ProcessContext context) { CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); - Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); - String resolvedAppName = resolveTargetAppName(context, app, hook); + String resolvedAppName = resolveTargetAppName(context, app); + if (resolvedAppName == null) { + context.setVariable(Variables.TASKS_TO_EXECUTE, List.of()); + return StepPhase.DONE; + } if (!resolvedAppName.equals(app.getName())) { context.setVariable(Variables.APP_TO_PROCESS, buildAppWithName(app, resolvedAppName)); } @@ -37,7 +39,8 @@ protected StepPhase executeStep(ProcessContext context) { return StepPhase.DONE; } - private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app, Hook hook) { + private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app) { + Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); if (hook == null) { return app.getName(); } @@ -72,68 +75,46 @@ private List> getPhasesConfig(Hook hook) { } private String buildCurrentPhaseString(ProcessContext context, Hook hook) { - String hookExecutionPhase = context.getVariable(Variables.HOOK_EXECUTION_PHASE); + List hookExecutionPhases = context.getVariable(Variables.HOOK_EXECUTION_PHASES); return hook.getPhases() .stream() - .filter(p -> p.equals(hookExecutionPhase)) + .filter(hookExecutionPhases::contains) .findFirst() - .orElseGet(() -> hook.getPhases() - .stream() - .filter(p -> p.contains(APPLICATION_PHASE_SUBSTRING)) - .findFirst() - .orElse("")); + .orElse(""); } private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { - ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); - ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); - - if (idleColor == null || liveColor == null) { - return resolveAppNameWithLiveIdleSuffix(app.getName(), targetApp); - } - - if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { - return swapColorSuffix(app.getName(), liveColor, idleColor); - } - if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { - return swapColorSuffix(app.getName(), idleColor, liveColor); - } - return app.getName(); - } - - private String resolveAppNameWithLiveIdleSuffix(String appName, String targetApp) { - String liveSuffix = BlueGreenApplicationNameSuffix.LIVE.asSuffix(); - String idleSuffix = BlueGreenApplicationNameSuffix.IDLE.asSuffix(); - - if (HookPhaseProcessType.HookProcessPhase.IDLE.getType().equals(targetApp)) { - if (appName.endsWith(liveSuffix)) { - return appName.substring(0, appName.length() - liveSuffix.length()); - } - if (!appName.endsWith(idleSuffix)) { - return appName + idleSuffix; + if (HookPhaseProcessType.HookProcessPhase.LIVE.getType() + .equals(targetApp)) { + if (isInitialDeploy(context)) { + getStepLogger().warn(Messages.SKIPPING_HOOK_TASK_NO_LIVE_APP, app.getName()); + return null; } + ApplicationColor liveColor = context.getVariable(Variables.LIVE_MTA_COLOR); + String suffix = liveColor != null ? liveColor.asSuffix() : BlueGreenApplicationNameSuffix.LIVE.asSuffix(); + return BlueGreenApplicationNameSuffix.removeSuffix(app.getName()) + suffix; } - if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { - if (appName.endsWith(idleSuffix)) { - return appName.substring(0, appName.length() - idleSuffix.length()); + if (HookPhaseProcessType.HookProcessPhase.IDLE.getType() + .equals(targetApp)) { + String baseName = BlueGreenApplicationNameSuffix.removeSuffix(app.getName()); + ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); + if (idleColor != null) { + return baseName + idleColor.asSuffix(); } - if (!appName.endsWith(liveSuffix)) { - return appName + liveSuffix; + if (isAfterRenamePhase(context)) { + return baseName; } + return baseName + BlueGreenApplicationNameSuffix.IDLE.asSuffix(); } - return appName; + return app.getName(); } - private String swapColorSuffix(String appName, ApplicationColor fromColor, ApplicationColor toColor) { - String fromSuffix = fromColor.asSuffix(); - String toSuffix = toColor.asSuffix(); - if (appName.endsWith(fromSuffix)) { - return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; - } - if (!appName.endsWith(toSuffix)) { - return appName + toSuffix; - } - return appName; + private boolean isAfterRenamePhase(ProcessContext context) { + return context.getVariable(Variables.PHASE) != null; + } + + private boolean isInitialDeploy(ProcessContext context) { + return context.getVariable(Variables.DEPLOYED_MTA) == null; } private CloudApplicationExtended buildAppWithName(CloudApplicationExtended app, String resolvedAppName) { @@ -144,7 +125,8 @@ private CloudApplicationExtended buildAppWithName(CloudApplicationExtended app, @Override protected String getStepErrorMessage(ProcessContext context) { return MessageFormat.format(Messages.ERROR_EXECUTING_HOOK, - context.getVariable(Variables.HOOK_FOR_EXECUTION).getName()); + context.getVariable(Variables.HOOK_FOR_EXECUTION) + .getName()); } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java index c4cf10fe61..f4889b1c3c 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/DeploymentTypeDeterminer.java @@ -20,8 +20,4 @@ public ProcessType determineDeploymentType(ProcessContext context) { return processTypeParser.getProcessType(context.getExecution()); } - public ProcessType determineDeploymentTypeSafely(ProcessContext context) { - return processTypeParser.getProcessType(context.getExecution(), false); - } - } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java new file mode 100644 index 0000000000..5957b7df21 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.text.MessageFormat; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Hook; + +public class HookPhasesConfigValidator { + + private static final String PHASE_KEY = "phase"; + + public void validate(DeploymentDescriptor descriptor) { + descriptor.getModules() + .stream() + .flatMap(module -> module.getHooks().stream()) + .forEach(this::validateHookHasNoDuplicatePhaseConfigs); + } + + @SuppressWarnings("unchecked") + private void validateHookHasNoDuplicatePhaseConfigs(Hook hook) { + Object phasesConfigValue = hook.getParameters().get(SupportedParameters.PHASES_CONFIG); + if (phasesConfigValue == null) { + return; + } + if (!(phasesConfigValue instanceof List)) { + throw new SLException(MessageFormat.format(Messages.INVALID_PHASES_CONFIG_NOT_A_LIST, hook.getName())); + } + List> phasesConfig = (List>) phasesConfigValue; + Set seenPhases = new HashSet<>(); + for (Map phaseConfig : phasesConfig) { + String phase = phaseConfig.get(PHASE_KEY); + if (phase != null && !seenPhases.add(phase)) { + throw new SLException(MessageFormat.format(Messages.DUPLICATE_PHASE_IN_PHASES_CONFIG, phase, hook.getName())); + } + } + } + +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java index 8ccf349c28..ffc393a143 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HooksExecutor.java @@ -50,11 +50,10 @@ private List executeHooks(StepPhase currentStepPhase) { moduleToDeploy.getName()); updateExecutedHooksForModule(alreadyExecutedHooksForModule, hooksWithPhases.getHookPhases(), hooksWithPhases.getHooks()); context.setVariable(Variables.HOOKS_FOR_EXECUTION, hooksWithPhases.getHooks()); - context.setVariable(Variables.HOOK_EXECUTION_PHASE, hooksWithPhases.getHookPhases() + context.setVariable(Variables.HOOK_EXECUTION_PHASES, hooksWithPhases.getHookPhases() .stream() - .findFirst() .map(HookPhase::getValue) - .orElse(null)); + .toList()); return hooksWithPhases.getHooks(); } 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 ff895a7232..043ee1a1d4 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 @@ -580,9 +580,10 @@ public interface Variables { .type(Variable.typeReference(Hook.class)) .defaultValue(Collections.emptyList()) .build(); - Variable HOOK_EXECUTION_PHASE = ImmutableSimpleVariable. builder() - .name("hookExecutionPhase") - .build(); + Variable> HOOK_EXECUTION_PHASES = ImmutableSimpleVariable.> builder() + .name("hookExecutionPhases") + .defaultValue(Collections.emptyList()) + .build(); Variable> MODULES_TO_DEPLOY = ImmutableJsonBinaryListVariable. builder() .name("modulesToDeploy") .type(Variable.typeReference(Module.class)) diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index d9724a2826..ecbecf7e21 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -26,7 +26,7 @@ - + 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 bae2ad3664..895d0f0fc9 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 @@ -117,7 +117,7 @@ - + @@ -150,7 +150,7 @@ - + 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 index 68ffddb76e..f4f5296b52 100644 --- 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 @@ -58,7 +58,7 @@ - + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index e431438330..1e5e5eb9cb 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -27,7 +27,7 @@ - + @@ -62,7 +62,7 @@ - + diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java index ae96a5414c..915ab970cd 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ResolveHookTaskTargetAppStepTest.java @@ -6,12 +6,16 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.model.ApplicationColor; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMta; +import org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.cloudfoundry.multiapps.mta.model.Hook; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class ResolveHookTaskTargetAppStepTest extends SyncFlowableStepTest { @@ -30,6 +34,12 @@ class ResolveHookTaskTargetAppStepTest extends SyncFlowableStepTest Date: Mon, 27 Apr 2026 16:04:18 +0300 Subject: [PATCH 10/10] Log a warning when hooks use the unreachable before-unmap-routes.idle phase --- .../controller/process/Messages.java | 1 + .../util/HookPhasesConfigValidator.java | 19 ++++++++++++++++++- .../process/backup-existing-app.bpmn | 1 + .../controller/process/deploy-app.bpmn | 2 ++ .../process/stop-dependent-modules.bpmn | 1 + .../controller/process/undeploy-app.bpmn | 2 ++ 6 files changed, 25 insertions(+), 1 deletion(-) 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 79b4700e73..b5158ca1d5 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 @@ -215,6 +215,7 @@ public class Messages { public static final String CANNOT_RETRIEVE_PARAMETERS_OF_BINDING_BETWEEN_APPLICATION_0_AND_SERVICE_INSTANCE_1 = "Cannot retrieve parameters of binding between application \"{0}\" and service instance \"{1}\""; public static final String CANNOT_RETRIEVE_PARAMETERS_OF_BINDING_BETWEEN_APPLICATION_0_AND_SERVICE_INSTANCE_1_FIX = "Cannot retrieve parameters of binding between application \"{0}\" and service instance \"{1}\". Got 502."; public static final String CANNOT_RETRIEVE_INSTANCE_OF_SERVICE = "Cannot retrieve service instance of service \"{0}\""; + public static final String HOOK_PHASE_BLUE_GREEN_BEFORE_UNMAP_ROUTES_IDLE_USED = "Hook \"{0}\" uses deprecated phase \"blue-green.application.before-unmap-routes.idle\" which is unreachable and will be removed in a future version"; public static final String COULD_NOT_DELETE_PROVIDED_DEPENDENCY = "Could not delete published provided dependency \"{0}\" from configuration registry"; public static final String COULD_NOT_DELETE_SERVICE = "Could not delete service \"{0}\", as it does not exist"; public static final String COULD_NOT_DELETE_SUBSCRIPTION = "Could not delete subscription for application \"{0}\" and resource \"{1}\""; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java index 5957b7df21..8353f09627 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidator.java @@ -7,20 +7,37 @@ import java.util.Set; import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.core.model.HookPhase; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.cloudfoundry.multiapps.mta.model.Hook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HookPhasesConfigValidator { + private static final Logger LOGGER = LoggerFactory.getLogger(HookPhasesConfigValidator.class); private static final String PHASE_KEY = "phase"; public void validate(DeploymentDescriptor descriptor) { descriptor.getModules() .stream() .flatMap(module -> module.getHooks().stream()) - .forEach(this::validateHookHasNoDuplicatePhaseConfigs); + .forEach(hook -> { + warnIfDeprecatedPhaseUsed(hook); + validateHookHasNoDuplicatePhaseConfigs(hook); + }); + } + + private void warnIfDeprecatedPhaseUsed(Hook hook) { + boolean usesDeprecatedPhase = hook.getPhases() + .stream() + .anyMatch(phase -> HookPhase.BLUE_GREEN_APPLICATION_BEFORE_UNMAP_ROUTES_IDLE.getValue() + .equals(phase)); + if (usesDeprecatedPhase) { + LOGGER.warn(Messages.HOOK_PHASE_BLUE_GREEN_BEFORE_UNMAP_ROUTES_IDLE_USED, hook.getName()); + } } @SuppressWarnings("unchecked") diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index ecbecf7e21..f220b0d72a 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -27,6 +27,7 @@ + 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 895d0f0fc9..db163d450d 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 @@ -118,6 +118,7 @@ + @@ -151,6 +152,7 @@ + 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 index f4f5296b52..46a2558e93 100644 --- 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 @@ -59,6 +59,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index 1e5e5eb9cb..9b393908d4 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -28,6 +28,7 @@ + @@ -63,6 +64,7 @@ +