/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package software.amazon.cloudformation.stackinstances.util; import lombok.Builder; import lombok.Data; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.stackinstances.DeploymentTargets; import software.amazon.cloudformation.stackinstances.Parameter; import software.amazon.cloudformation.stackinstances.ResourceModel; import software.amazon.cloudformation.stackinstances.StackInstances; import java.util.*; import java.util.stream.Collectors; /** * Utility class to hold {@link StackInstances} that need to be modified during the update */ @Builder @Data public class InstancesAnalyzer { private ResourceModel previousModel; private ResourceModel desiredModel; /** * Aggregates flat {@link StackInstance} to a group of {@link StackInstances} to call * corresponding StackSet APIs * * @param flatStackInstances {@link StackInstance} * @return {@link StackInstances} set */ public static Set<StackInstances> aggregateStackInstances( final Set<StackInstance> flatStackInstances) { final Set<StackInstances> groupedStacks = groupInstancesByTargets(flatStackInstances); return aggregateInstancesByRegions(groupedStacks); } /** * Group regions by {@link DeploymentTargets} and {@link StackInstance#getParameters()} * * @return {@link StackInstances} */ private static Set<StackInstances> groupInstancesByTargets( final Set<StackInstance> flatStackInstances) { final Map<List<Object>, StackInstances> groupedStacksMap = new HashMap<>(); for (final StackInstance stackInstance : flatStackInstances) { final String target = stackInstance.getDeploymentTarget(); final String region = stackInstance.getRegion(); final Set<Parameter> parameterSet = stackInstance.getParameters(); final List<Object> compositeKey = Arrays.asList(target, parameterSet); if (groupedStacksMap.containsKey(compositeKey)) { groupedStacksMap.get(compositeKey).getRegions().add(stackInstance.getRegion()); } else { final DeploymentTargets targets = DeploymentTargets.builder().build(); targets.setAccounts(new HashSet<>(Arrays.asList(target))); final StackInstances stackInstances = StackInstances.builder() .regions(new HashSet<>(Arrays.asList(region))) .deploymentTargets(targets) .parameterOverrides(parameterSet) .build(); groupedStacksMap.put(compositeKey, stackInstances); } } return new HashSet<>(groupedStacksMap.values()); } /** * Aggregates instances with similar {@link StackInstances#getRegions()} * * @param groupedStacks {@link StackInstances} set * @return Aggregated {@link StackInstances} set */ private static Set<StackInstances> aggregateInstancesByRegions( final Set<StackInstances> groupedStacks) { final Map<List<Object>, StackInstances> groupedStacksMap = new HashMap<>(); for (final StackInstances stackInstances : groupedStacks) { final DeploymentTargets target = stackInstances.getDeploymentTargets(); final Set<Parameter> parameterSet = stackInstances.getParameterOverrides(); final List<Object> compositeKey = Arrays.asList(stackInstances.getRegions(), parameterSet); if (groupedStacksMap.containsKey(compositeKey)) { groupedStacksMap.get(compositeKey).getDeploymentTargets() .getAccounts().addAll(target.getAccounts()); } else { groupedStacksMap.put(compositeKey, stackInstances); } } return new HashSet<>(groupedStacksMap.values()); } /** * Compares {@link StackInstance#getParameters()} with previous {@link StackInstance#getParameters()} * Gets the StackInstances need to update * * @param intersection {@link StackInstance} retaining desired stack instances * @param previousStackMap Map contains previous stack instances * @return {@link StackInstance} to update */ private static Set<StackInstance> getUpdatingStackInstances( final Set<StackInstance> intersection, final Map<StackInstance, StackInstance> previousStackMap) { return intersection.stream() .filter(stackInstance -> !Comparator.equals( previousStackMap.get(stackInstance).getParameters(), stackInstance.getParameters())) .collect(Collectors.toSet()); } /** * Since Stack instances are defined across accounts and regions with(out) parameters, * We are expanding all before we tack actions * * @param stackInstances {@link ResourceModel#getStackInstances()} * @return {@link StackInstance} set */ private static Set<StackInstance> flattenStackInstances( final StackInstances stackInstances) { final Set<StackInstance> flatStacks = new HashSet<>(); for (final String region : stackInstances.getRegions()) { final Set<String> targets = stackInstances.getDeploymentTargets().getAccounts(); // Validates expected DeploymentTargets exist in the template if (CollectionUtils.isNullOrEmpty(targets)) { throw new CfnInvalidRequestException( String.format("%s should be specified in DeploymentTargets in [%s] model", "Accounts", "SELF_MANAGED")); } for (final String target : targets) { final StackInstance stackInstance = StackInstance.builder() .region(region).deploymentTarget(target).parameters(stackInstances.getParameterOverrides()) .build(); // Validates no duplicated stack instance is specified if (flatStacks.contains(stackInstance)) { throw new CfnInvalidRequestException( String.format("Stack instance [%s,%s] is duplicated", target, region)); } flatStacks.add(stackInstance); } } return flatStacks; } /** * Analyzes {@link StackInstances} that need to be modified during the update operations * * @param placeHolder {@link software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder} */ public void analyzeForUpdate(final software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder placeHolder) { final boolean isSelfManaged = true; final Set<StackInstance> previousStackInstances = flattenStackInstances(previousModel.getStackInstances()); final Set<StackInstance> desiredStackInstances = flattenStackInstances(desiredModel.getStackInstances()); // Calculates all necessary differences that we need to take actions final Set<StackInstance> stacksToAdd = new HashSet<>(desiredStackInstances); stacksToAdd.removeAll(previousStackInstances); final Set<StackInstance> stacksToDelete = new HashSet<>(previousStackInstances); stacksToDelete.removeAll(desiredStackInstances); final Set<StackInstance> stacksToCompare = new HashSet<>(desiredStackInstances); stacksToCompare.retainAll(previousStackInstances); // Since StackInstance.parameters is excluded for @EqualsAndHashCode, // we needs to construct a key value map to keep track on previous StackInstance objects final Set<StackInstance> stacksToUpdate = getUpdatingStackInstances( stacksToCompare, previousStackInstances.stream().collect(Collectors.toMap(s -> s, s -> s))); // Update the stack lists that need to write of callbackContext holder placeHolder.setCreateStackInstances(stacksToAdd); placeHolder.setDeleteStackInstances(stacksToDelete); placeHolder.setUpdateStackInstances(stacksToUpdate); } /** * Analyzes {@link StackInstances} that need to be modified during create operations * * @param placeHolder {@link software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder} */ public void analyzeForCreate(final software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder placeHolder) { if (desiredModel.getStackInstances() == null) return; final Set<StackInstance> desiredStackInstances = flattenStackInstances(desiredModel.getStackInstances()); placeHolder.setCreateStackInstances(desiredStackInstances); } /** * Analyzes {@link StackInstances} that need to be modified during delete operations * * @param placeHolder {@link software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder} */ public void analyzeForDelete(final software.amazon.cloudformation.stackinstances.util.StackInstancesPlaceHolder placeHolder) { if (desiredModel.getStackInstances() == null) return; final Set<StackInstance> desiredStackInstances = flattenStackInstances(desiredModel.getStackInstances()); placeHolder.setDeleteStackInstances(desiredStackInstances); } }