-
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
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 |
|---|---|---|
| @@ -1,14 +1,17 @@ | ||
| 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; | ||
| import java.util.Set; | ||
|
|
||
| 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; | ||
|
|
@@ -30,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; | ||
|
|
||
|
|
@@ -59,13 +67,40 @@ protected StepPhase executeStep(ProcessContext context) { | |
| .toList()); | ||
| context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, descriptor); | ||
|
|
||
| validatePhasesConfig(context, descriptor); | ||
| warnForUnsupportedParameters(descriptor); | ||
|
|
||
| backupDeploymentDescriptor(context, descriptor); | ||
| getStepLogger().debug(Messages.DESCRIPTORS_MERGED); | ||
| 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); | ||
|
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. Null handling? |
||
| if (!(phasesConfigValue instanceof List)) { | ||
| return; | ||
|
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. shouldn't the deployment fail if phases-config is not a List? |
||
| } | ||
| List<Map<String, String>> phasesConfig = (List<Map<String, String>>) phasesConfigValue; | ||
| Set<String> seenPhases = new HashSet<>(); | ||
| for (Map<String, String> 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<ReferenceContainer> references = new ReferencesFinder().getAllReferences(descriptor); | ||
| Map<String, List<String>> unsupportedParameters = unsupportedParameterFinder.findUnsupportedParameters(descriptor, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| 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.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"; | ||
| 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); | ||
|
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. Instead of passing it as argument, just get it in resolveTargetAppName |
||
|
|
||
| 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<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); | ||
| } | ||
|
|
||
| @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(); | ||
| } | ||
|
|
||
| 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("")); | ||
|
Comment on lines
+80
to
+84
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. Why are we searching for the first application hook? |
||
| } | ||
|
|
||
| private String resolveAppNameForTarget(ProcessContext context, CloudApplicationExtended app, String targetApp) { | ||
|
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. What will happen if we using hook:
Contributor
Author
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. With the current implementation we hit 404. What’s the right approach here- should the process fail or just log a warning? Also, should the combination of phase: blue-green.application.before-start.idle and target-app: live even be allowed during an initial deployment? |
||
| 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)) { | ||
|
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. When blue-green.application.before-start.idle is used. The apps are still called (for example): |
||
| return appName.substring(0, appName.length() - liveSuffix.length()); | ||
| } | ||
| if (!appName.endsWith(idleSuffix)) { | ||
| return appName + idleSuffix; | ||
| } | ||
| } | ||
| if (HookPhaseProcessType.HookProcessPhase.LIVE.getType().equals(targetApp)) { | ||
|
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 simplify these conditions as they are hard to read? -- |
||
| 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(); | ||
| if (appName.endsWith(fromSuffix)) { | ||
| return appName.substring(0, appName.length() - fromSuffix.length()) + toSuffix; | ||
| } | ||
| if (!appName.endsWith(toSuffix)) { | ||
| return appName + toSuffix; | ||
| } | ||
|
Comment on lines
+127
to
+135
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 you just remove suffix BlueGreenApplicationNameSuffix::removeSuffix then append toColor? |
||
| 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()); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
Comment on lines
+23
to
+25
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. What is this? |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,11 @@ private List<Hook> 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)); | ||
|
Comment on lines
+53
to
+57
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 variable breaks the idea of having multiple phases for one step. While technically: |
||
| return hooksWithPhases.getHooks(); | ||
| } | ||
|
|
||
|
|
||
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.
extract this validation in a new class