query) {
+ final Rule defaultRedirectRule = createDefaultRedirectRule(hostname, pathWithLeadingSlash, query);
+ addRulesAssigningUnusedPriorities(/* forceContiguous */ false,
+ Util.stream(getRules()).filter(rule->rule.conditions().stream().anyMatch(condition->condition.field().equals("host-header") && condition.hostHeaderConfig().values().contains(hostname))).findFirst(),
+ defaultRedirectRule);
+ return defaultRedirectRule;
+ }
@Override
public Rule getDefaultRedirectRule(String hostName, PlainRedirectDTO redirect) {
diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/LoadBalancerRuleInserter.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/LoadBalancerRuleInserter.java
new file mode 100644
index 00000000000..145f138d151
--- /dev/null
+++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/LoadBalancerRuleInserter.java
@@ -0,0 +1,291 @@
+package com.sap.sse.landscape.aws.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+
+import com.sap.sse.common.Named;
+import com.sap.sse.common.Util;
+import com.sap.sse.common.Util.Pair;
+import com.sap.sse.landscape.aws.ApplicationLoadBalancer;
+
+import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule;
+import software.amazon.awssdk.services.elasticloadbalancingv2.model.RulePriorityPair;
+
+/**
+ * Manages a set of rules from an Application Load Balancer. This includes adding a set of rules either contiguously or
+ * optionally non-contiguously to an existing set of rules that are ordered by their priorities. There are limits on the
+ * maximum number of rules and on the maximum priority that can be used. Trying to add too many rules so that one of
+ * these limits would have to be violated to accommodate will throw an exception.
+ *
+ *
+ * For purposes of testability, the class is designed such that the actual {@link Rule} and {@link RulePriorityPair}
+ * classes are abstracted by an interface. Default adapters are available for the actual ALB-related classes, but tests
+ * may provide their own mocks instead so that it is not required to test this class against a real AWS environment.
+ *
+ * @author Axel Uhl (d043530)
+ *
+ */
+public class LoadBalancerRuleInserter> {
+ private final LoadBalancerAdapter loadBalancerAdapter;
+ private final int maxPriority;
+ private final int maxRulesPerLoadBalancer;
+
+ public static interface RuleAdapter> {
+ boolean isDefault();
+ String priority();
+ String ruleArn();
+ RA copyWithNewPriority(int priorityToUseForRuleCopy);
+ }
+
+ public static interface LoadBalancerAdapter> extends Named {
+ Iterable getRules();
+
+ void updateLoadBalancerListenerRulePriorities(List> newPrioritiesForExistingRules);
+
+ void addRules(List result);
+ }
+
+ public static class ALBRuleAdapter implements RuleAdapter {
+ private final Rule rule;
+
+ private ALBRuleAdapter(Rule rule) {
+ this.rule = rule;
+ }
+
+ @Override
+ public boolean isDefault() {
+ return rule.isDefault();
+ }
+
+ @Override
+ public String priority() {
+ return rule.priority();
+ }
+
+ @Override
+ public String ruleArn() {
+ return rule.ruleArn();
+ }
+
+ @Override
+ public ALBRuleAdapter copyWithNewPriority(int priorityToUseForRuleCopy) {
+ return new ALBRuleAdapter(rule.copy(b->b.priority(""+priorityToUseForRuleCopy).build()));
+ }
+
+ Rule getRule() {
+ return rule;
+ }
+ }
+
+ private static class ALBAdapter implements LoadBalancerAdapter {
+ private static final long serialVersionUID = -4337640786328697695L;
+ private final ApplicationLoadBalancer alb;
+
+ private ALBAdapter(ApplicationLoadBalancer alb) {
+ super();
+ this.alb = alb;
+ }
+
+ @Override
+ public String getName() {
+ return alb.getName();
+ }
+
+ @Override
+ public Iterable getRules() {
+ return Util.map(alb.getRules(), r->createRuleAdapter(r));
+ }
+
+ @Override
+ public void updateLoadBalancerListenerRulePriorities(
+ List> newPrioritiesForExistingRules) {
+ alb.getLandscape().updateLoadBalancerListenerRulePriorities(alb.getRegion(),
+ Util.map(newPrioritiesForExistingRules, p->RulePriorityPair.builder().priority(p.getA()).ruleArn(p.getB().ruleArn()).build()));
+ }
+
+ @Override
+ public void addRules(List rules) {
+ alb.addRules(Util.toArray(Util.map(rules, ra->ra.getRule()), new Rule[0]));
+ }
+
+ }
+
+ public LoadBalancerRuleInserter(LoadBalancerAdapter loadBalancerAdapter, int maxPriority, int maxRulesPerLoadBalancer) {
+ super();
+ this.loadBalancerAdapter = loadBalancerAdapter;
+ this.maxPriority = maxPriority;
+ this.maxRulesPerLoadBalancer = maxRulesPerLoadBalancer;
+ }
+
+ public static LoadBalancerRuleInserter create(
+ ApplicationLoadBalancer alb, int maxPriority, int maxRulesPerLoadBalancer) {
+ return new LoadBalancerRuleInserter<>(createLoadBalancerAdapter(alb), maxPriority, maxRulesPerLoadBalancer);
+ }
+
+ public static ALBRuleAdapter createRuleAdapter(Rule rule) {
+ return new ALBRuleAdapter(rule);
+ }
+
+ static ALBAdapter createLoadBalancerAdapter(ApplicationLoadBalancer alb) {
+ return new ALBAdapter(alb);
+ }
+
+ /**
+ * As the rule {@link Rule#priority() priorities} within a load balancer's listener have to be unique, this method
+ * supports adding rules by assigning yet unused priorities to them. It keeps the order in which the {@code rules}
+ * are passed. If {@code forceContiguous} is {@code false}, for each rule the next available priority is chosen and
+ * assigned by creating a copy of the {@link Rule} object and adding it to the resulting sequence. This can lead to
+ * existing rules interleaving with the rules to add while ensuring that the {@code rules} have priorities in
+ * numerically ascending order, consistent with the order in which they were passed to this method.
+ *
+ *
+ * If {@code forceContiguous} is {@code true}, the rules that result will have contiguously increasing priority
+ * values, hence not interleaving with other existing rules. If there are enough contiguous unused priorities
+ * available, they are selected and assigned by creating copies of the {@link Rule} objects and adding them in their
+ * original order to the resulting sequence. Otherwise, the existing rules are "compressed" by re-numbering their
+ * priorities to make space for the new rules at the end of the list.
+ *
+ * @param forceContiguous
+ * if {@code true}, the {@code rules} must be inserted as a contiguous sequence in the overall sequence
+ * of rules ordered by priority; otherwise it is permissible to look for unused priorities anywhere along
+ * the sequence and to use existing gaps in the priority sequence, possibly separating the {@code rules}
+ * into one or more sub-sequences scattered throughout the resulting rules sequence
+ * @param insertBefore
+ * if empty, the new {@code rules} can be inserted anywhere in the list of rules, of course adhering to
+ * the {@code forceContiguous} rules explained above; if a {@link RuleAdapter} is specified and that rule
+ * is part of this ALB's {@link LoadBalancerAdapter#getRules() rules}, find unused priorities before that
+ * rule according to the {@code forceContiguous} rules, and if necessary
+ * {@link #shiftRulesToMakeSpaceAt(int) make space} at that point and insert the {@code rules} rules not
+ * yet inserted prior to that point there. If the rule specified cannot be found in this ALB's
+ * {@link #getRules()}, an {@link IllegalArgumentException} is thrown. The rules are compared by their
+ * {@link Rule#ruleArn() ARN}, not by their Java object identity.
+ *
+ * @return {@link RuleAdapter#copyWithNewPriority(int) copies} of the original rules, with unused
+ * {@link RuleAdapter#priority() priorities} assigned, as passed already to
+ * {@link LoadBalancerAdapter#addRules(List)}.
+ */
+ public List addRulesAssigningUnusedPriorities(boolean forceContiguous, Optional insertBefore,
+ Iterable rules) {
+ final Iterable existingRules = loadBalancerAdapter.getRules();
+ if (insertBefore.isPresent() && !Util.contains(Util.map(existingRules, RuleAdapter::ruleArn), insertBefore.get().ruleArn())) {
+ throw new IllegalArgumentException("Didn't find rule to insert before: "+insertBefore.get());
+ }
+ final int rulesSize = Util.size(rules);
+ int remainingNumberOfRulesToAdd = rulesSize;
+ if (Util.size(existingRules)-1 + rulesSize > maxPriority) { // -1 due to the default rule being part of existingRules
+ throw new IllegalArgumentException("The "+rulesSize+" new rules won't find enough unused priority numbers because there are already "+
+ (Util.size(existingRules)-1)+" of them and together they would exceed the maximum of "+maxPriority+" by "+
+ (Util.size(existingRules)-1 + rulesSize - maxPriority));
+ }
+ if (Util.size(existingRules) + rulesSize > maxRulesPerLoadBalancer) {
+ throw new IllegalArgumentException("The " + rulesSize + " new rules would make the ALB " + loadBalancerAdapter.getName()
+ + " exceed its maximum number of rules (" + maxPriority + ") by "
+ + (Util.size(existingRules) + rulesSize - maxRulesPerLoadBalancer));
+ }
+ final List result = new ArrayList<>(rulesSize);
+ final List sortedExistingNonDefaultRules = new ArrayList<>(Util.size(existingRules)-1);
+ Util.addAll(Util.filter(existingRules, r->!r.isDefault()), sortedExistingNonDefaultRules);
+ Collections.sort(sortedExistingNonDefaultRules, (r1, r2)->Integer.valueOf(r1.priority()).compareTo(Integer.valueOf(r2.priority())));
+ final int stepwidth;
+ if (forceContiguous) {
+ stepwidth = rulesSize;
+ } else {
+ stepwidth = 1;
+ }
+ final Iterator rulesIterator = rules.iterator();
+ int previousPriority = 0;
+ final Iterator existingRulesIter = sortedExistingNonDefaultRules.iterator();
+ boolean foundRuleToInsertBefore = false;
+ while (rulesIterator.hasNext()) {
+ // find next available slot
+ int nextPriority = maxPriority+1; // if no further rule exists, the usable gap ends after MAX_PRIORITY
+ final RuleAdapter>[] nextRule = new RuleAdapter>[1]; // using an array to make it final so we can access it inside the mapping function below
+ // search for space until
+ // - we reach the end of the existing rules list, or
+ while (existingRulesIter.hasNext() &&
+ // - we find enough space, or
+ ((nextPriority=Integer.valueOf((nextRule[0]=existingRulesIter.next()).priority())) <= previousPriority+stepwidth) &&
+ // - the rule before which all new rules must be inserted is found
+ (!insertBefore.isPresent() || !(foundRuleToInsertBefore=insertBefore.map(rule->rule.ruleArn().equals(nextRule[0].ruleArn())).orElse(false)))) {
+ // not enough space for stepwidth many rules; keep on searching
+ previousPriority = nextPriority;
+ if (!existingRulesIter.hasNext()) {
+ nextPriority = maxPriority+1;
+ }
+ }
+ if (foundRuleToInsertBefore) {
+ // We must insert all remaining rules from rulesIterator here, making space if necessary
+ shiftRulesToMakeSpaceAt(nextPriority, remainingNumberOfRulesToAdd);
+ nextPriority += remainingNumberOfRulesToAdd;
+ }
+ if (previousPriority+stepwidth > maxPriority) {
+ if (forceContiguous) {
+ previousPriority = squeezeExistingRulesAndReturnLastUsedPriority(sortedExistingNonDefaultRules);
+ nextPriority = maxPriority+1;
+ // we previously checked already that there is enough room for the new set of rules
+ assert previousPriority + rulesSize <= maxPriority;
+ } else {
+ throw new IllegalStateException(
+ "The " + rulesSize + " new rules don't fit into the existing rule set of load balancer "
+ + loadBalancerAdapter.getName() + " without exceeding the maximum priority of " + maxPriority);
+ }
+ }
+ while (rulesIterator.hasNext() && ++previousPriority < nextPriority) {
+ final int priorityToUseForNextRule = previousPriority;
+ result.add(rulesIterator.next().copyWithNewPriority(priorityToUseForNextRule));
+ remainingNumberOfRulesToAdd--;
+ }
+ }
+ loadBalancerAdapter.addRules(result);
+ return result;
+ }
+
+ private int squeezeExistingRulesAndReturnLastUsedPriority(final List sortedExistingNonDefaultRules) {
+ final List> newPrioritiesForExistingRules = new LinkedList<>();
+ int priority = 0;
+ for (final RA existingRule : sortedExistingNonDefaultRules) {
+ newPrioritiesForExistingRules.add(new Pair<>(++priority, existingRule));
+ }
+ loadBalancerAdapter.updateLoadBalancerListenerRulePriorities(newPrioritiesForExistingRules);
+ return priority;
+ }
+
+ public Iterable getRulesSortedByPriority() {
+ return Util.stream(loadBalancerAdapter.getRules()).sorted((r1, r2)->Integer.compare(Integer.valueOf(r1.priority()), Integer.valueOf(r2.priority())))::iterator;
+ }
+
+ /**
+ * For inserting rules before some other rule we may need to shift other rules' priorities to make enough space in
+ * the sequence of ascending unique priorities. This function shifts every priority starting at the highest priority
+ * one higher for making the priority at {@code targetPrio} free.
+ *
+ * @param targetPriority
+ * index supposed to be free
+ * @param howManySlots
+ * tells how many free slots are needed; must be greater than or equal to 1
+ * @throws IllegalStateException
+ * gets thrown if shifting exceeds the limit of priorities
+ */
+ public void shiftRulesToMakeSpaceAt(int targetPriority, int howManySlots) throws IllegalStateException {
+ final Iterable rulesSorted = getRulesSortedByPriority();
+ int minimumPriorityForNextRule = targetPriority + howManySlots;
+ final List> result = new ArrayList<>();
+ for (final RA rule : rulesSorted) {
+ final int priority = Integer.valueOf(rule.priority());
+ final int newPriorityForRule;
+ // if targetPriority not reached or rule priority is already greater than required then the rule can be copied unchanged:
+ if (priority < targetPriority || priority >= minimumPriorityForNextRule) {
+ newPriorityForRule = priority;
+ } else {
+ newPriorityForRule = minimumPriorityForNextRule;
+ minimumPriorityForNextRule = newPriorityForRule + 1;
+ }
+ result.add(new Pair<>(newPriorityForRule, rule));
+ }
+ loadBalancerAdapter.updateLoadBalancerListenerRulePriorities(result);
+ }
+}
diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java
index ef269667e1c..f659ca52cee 100755
--- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java
+++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/CreateLoadBalancerMapping.java
@@ -225,7 +225,7 @@ implements ProcedureCreatingLoadBalancerMapping {
getLandscape().addTargetsToTargetGroup(masterTargetGroupCreated, Collections.singleton(getHost()));
getLandscape().addTargetsToTargetGroup(publicTargetGroupCreated, Collections.singleton(getHost()));
getLoadBalancerUsed().addRulesAssigningUnusedPriorities(/* forceContiguous */ true,
- createRules(getLoadBalancerUsed(), getHostName(), masterTargetGroupCreated, publicTargetGroupCreated));
+ Optional.empty(), createRules(getLoadBalancerUsed(), getHostName(), masterTargetGroupCreated, publicTargetGroupCreated));
} catch (Exception e) {
throw new RuntimeException(e);
}
diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/ShardProcedure.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/ShardProcedure.java
index 45b35cc8650..6b7f113a7a1 100644
--- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/ShardProcedure.java
+++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/orchestration/ShardProcedure.java
@@ -7,6 +7,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
@@ -26,6 +27,7 @@ import com.sap.sse.landscape.aws.AwsApplicationReplicaSet;
import com.sap.sse.landscape.aws.AwsLandscape;
import com.sap.sse.landscape.aws.AwsShard;
import com.sap.sse.landscape.aws.TargetGroup;
+import com.sap.sse.landscape.aws.impl.LoadBalancerRuleInserter;
import software.amazon.awssdk.services.elasticloadbalancingv2.model.Action;
import software.amazon.awssdk.services.elasticloadbalancingv2.model.ActionTypeEnum;
@@ -216,7 +218,7 @@ implements ProcedureCreatingLoadBalancerMapping {
Util.addAll(shardingKeys, shardingKeyForConsumption);
final int ruleIdx = alb.getFirstShardingPriority(replicaSet.getHostname());
while (!shardingKeyForConsumption.isEmpty()) {
- alb.shiftRulesToMakeSpaceAt(ruleIdx);
+ LoadBalancerRuleInserter.create(alb, ApplicationLoadBalancer.MAX_PRIORITY, ApplicationLoadBalancer.MAX_RULES_PER_LOADBALANCER).shiftRulesToMakeSpaceAt(ruleIdx, 1);
final Set shardingKeysForNextRule = new HashSet<>();
for (final Iterator i=shardingKeyForConsumption.iterator();
shardingKeysForNextRule.size() < ApplicationLoadBalancer.MAX_CONDITIONS_PER_RULE-NUMBER_OF_STANDARD_CONDITIONS_FOR_SHARDING_RULE && i.hasNext(); ) {
@@ -251,7 +253,7 @@ implements ProcedureCreatingLoadBalancerMapping {
final int requiredRules = numberOfRequiredRules(Util.size(shardingKeys))
+ (existingShardingRules + /* 5 std rules per replica set */ NUMBER_OF_RULES_PER_REPLICA_SET);
final ApplicationLoadBalancer res;
- if (Util.size(replicaSet.getLoadBalancerRules())
+ if (Util.size(replicaSet.getLoadBalancer().getRules())
+ numberOfRequiredRules(Util.size(shardingKeys)) < ApplicationLoadBalancer.MAX_RULES_PER_LOADBALANCER) {
res = replicaSet.getLoadBalancer();
} else {
@@ -261,7 +263,8 @@ implements ProcedureCreatingLoadBalancerMapping {
final Iterable> loadBalancersFiltered = Util.filter(loadBalancers,
t -> {
try {
- return !t.getArn().equals(replicaSet.getLoadBalancer().getArn());
+ return t.getVpcId().equals(replicaSet.getLoadBalancer().getVpcId())
+ && !t.getArn().equals(replicaSet.getLoadBalancer().getArn());
} catch (InterruptedException | ExecutionException e) {
logger.log(Level.WARNING, "Exception while trying to obtain a load balancer's ARN", e);
throw new RuntimeException(e);
@@ -374,8 +377,8 @@ implements ProcedureCreatingLoadBalancerMapping {
Collection> originalTargetGroups,
Map, Iterable> shardingKeysPerTargetGroup) throws Exception {
targetAlb
- .addRulesAssigningUnusedPriorities(true,
- createRules(targetAlb, replicaSet.getHostname(),
+ .addRulesAssigningUnusedPriorities(/* forceContiguous */ true,
+ Optional.empty(), createRules(targetAlb, replicaSet.getHostname(),
targetGroupsToTempTargetgroups.get(replicaSetToMove.getMasterTargetGroup()),
targetGroupsToTempTargetgroups.get(replicaSetToMove.getPublicTargetGroup())))
.forEach(t -> tempRules.add(t));