-
Notifications
You must be signed in to change notification settings - Fork 41
Add phase-configs support to Hook model for target-app resolution #1812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b272617
8e768cd
ceef42a
eab5ad4
9165d29
aab7ff0
695aea6
26e0d65
80b634a
7deb07c
0ecd675
7f3c252
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -149,6 +149,8 @@ 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 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"; | ||
|
|
@@ -213,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"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This log message flows a bit better to me: |
||
| 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}\""; | ||
|
|
@@ -409,6 +412,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}\"..."; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| 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.client.lib.domain.ImmutableCloudApplicationExtended; | ||
| 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; | ||
| 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"; | ||
|
|
||
| @Override | ||
| protected StepPhase executeStep(ProcessContext context) { | ||
| CloudApplicationExtended app = context.getVariable(Variables.APP_TO_PROCESS); | ||
|
|
||
| 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)); | ||
| } | ||
|
|
||
| return StepPhase.DONE; | ||
| } | ||
|
Comment on lines
+32
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here all branches return StepPhase.DONE. Would it be clearer to return once at the end and update the context conditionally (e.g., using an else if)? |
||
|
|
||
| private String resolveTargetAppName(ProcessContext context, CloudApplicationExtended app) { | ||
| Hook hook = context.getVariable(Variables.HOOK_FOR_EXECUTION); | ||
| if (hook == null) { | ||
| return app.getName(); | ||
| } | ||
|
|
||
| List<Map<String, String>> 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); | ||
| } | ||
|
Comment on lines
+43
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small refactoring suggestion: |
||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private List<Map<String, String>> getPhasesConfig(Hook hook) { | ||
| Object phasesConfigValue = hook.getParameters() | ||
| .get(SupportedParameters.PHASES_CONFIG); | ||
| if (phasesConfigValue instanceof List) { | ||
| return (List<Map<String, String>>) phasesConfigValue; | ||
| } | ||
| return List.of(); | ||
| } | ||
|
Comment on lines
+68
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We treat missing or null PHASES_CONFIG as empty, but silently ignore cases where it’s present with the wrong type. Would it make sense to log or validate that here? |
||
|
|
||
| private String buildCurrentPhaseString(ProcessContext context, Hook hook) { | ||
| List<String> hookExecutionPhases = context.getVariable(Variables.HOOK_EXECUTION_PHASES); | ||
| return hook.getPhases() | ||
| .stream() | ||
| .filter(hookExecutionPhases::contains) | ||
| .findFirst() | ||
| .orElse(HookPhaseProcessType.HookProcessPhase.NONE.getType()); | ||
| } | ||
|
|
||
| private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { | ||
|
IvanBorislavovDimitrov marked this conversation as resolved.
|
||
| if (HookPhaseProcessType.HookProcessPhase.LIVE.getType() | ||
| .equals(targetApp)) { | ||
| return resolveLiveAppName(context, app); | ||
| } | ||
| if (HookPhaseProcessType.HookProcessPhase.IDLE.getType() | ||
| .equals(targetApp)) { | ||
| return resolveIdleAppName(context, app); | ||
| } | ||
| return app.getName(); | ||
| } | ||
|
|
||
| private String resolveLiveAppName(ProcessContext context, CloudApplicationExtended app) { | ||
| 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; | ||
| } | ||
|
|
||
| private String resolveIdleAppName(ProcessContext context, CloudApplicationExtended app) { | ||
| String baseName = BlueGreenApplicationNameSuffix.removeSuffix(app.getName()); | ||
| return baseName + resolveIdleSuffix(context); | ||
| } | ||
|
|
||
| private String resolveIdleSuffix(ProcessContext context) { | ||
| ApplicationColor idleColor = context.getVariable(Variables.IDLE_MTA_COLOR); | ||
| if (idleColor != null) { | ||
| return idleColor.asSuffix(); | ||
| } | ||
| if (isAfterRenamePhase(context)) { | ||
| return ""; | ||
| } | ||
| return BlueGreenApplicationNameSuffix.IDLE.asSuffix(); | ||
| } | ||
|
|
||
| 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) { | ||
| return 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()); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| 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.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(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") | ||
| 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<Map<String, String>> phasesConfig = (List<Map<String, String>>) phasesConfigValue; | ||
|
Comment on lines
+43
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we scope @SuppressWarnings("unchecked") to just the cast instead of the whole method? |
||
| Set<String> seenPhases = new HashSet<>(); | ||
| for (Map<String, String> 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())); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move PHASES_CONFIG up with the other public static final declarations.