diff --git a/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml b/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml index 4910794a8eb..bd6f05f04c4 100755 --- a/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml +++ b/java/com.amazon.aws.aws-java-api.updatesite/features/aws-sdk/feature.xml @@ -2,21 +2,21 @@ diff --git a/java/com.amazon.aws.aws-java-api.updatesite/site.xml b/java/com.amazon.aws.aws-java-api.updatesite/site.xml index 91a8c984a3f..59efaa49f54 100755 --- a/java/com.amazon.aws.aws-java-api.updatesite/site.xml +++ b/java/com.amazon.aws.aws-java-api.updatesite/site.xml @@ -1,6 +1,6 @@ - + diff --git a/java/com.amazon.aws.aws-java-api/build.gradle b/java/com.amazon.aws.aws-java-api/build.gradle index 3bd4c29d017..200850cb831 100644 --- a/java/com.amazon.aws.aws-java-api/build.gradle +++ b/java/com.amazon.aws.aws-java-api/build.gradle @@ -5,7 +5,7 @@ repositories { } dependencies { - implementation platform('software.amazon.awssdk:bom:2.18.8') + implementation platform('software.amazon.awssdk:bom:2.20.59') implementation 'software.amazon.awssdk:s3' implementation 'software.amazon.awssdk:lambda' implementation 'software.amazon.awssdk:ec2' diff --git a/java/com.amazon.aws.aws-java-api/pom.xml b/java/com.amazon.aws.aws-java-api/pom.xml index 04874e418c6..9d3b9d04299 100644 --- a/java/com.amazon.aws.aws-java-api/pom.xml +++ b/java/com.amazon.aws.aws-java-api/pom.xml @@ -16,7 +16,7 @@ com.amazon.aws com.amazon.aws.aws-java-api - 2.18.8 + 2.20.59 eclipse-plugin diff --git a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target index b7ee608ab51..614f140ba95 100755 --- a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target +++ b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target @@ -1,6 +1,6 @@ - + @@ -147,7 +147,7 @@ - + diff --git a/java/com.sap.sse.feature.runtime/feature.xml b/java/com.sap.sse.feature.runtime/feature.xml index 8e8c975c240..773f8fd9ff0 100644 --- a/java/com.sap.sse.feature.runtime/feature.xml +++ b/java/com.sap.sse.feature.runtime/feature.xml @@ -742,14 +742,14 @@ id="com.amazon.aws.aws-java-api" download-size="0" install-size="0" - version="2.18.8" + version="2.20.59" unpack="false"/> loadBalancerAdapter; + private LoadBalancerRuleInserter ruleInserter; + + private static class TestRuleAdapter implements RuleAdapter { + private final boolean isDefault; + private final int priority; + private final String ruleArn; + + public TestRuleAdapter(boolean isDefault, int priority, String ruleArn) { + super(); + this.isDefault = isDefault; + this.priority = priority; + this.ruleArn = ruleArn; + } + + @Override + public boolean isDefault() { + return isDefault; + } + + @Override + public String priority() { + return "" + priority; + } + + @Override + public String ruleArn() { + return ruleArn; + } + + @Override + public TestRuleAdapter copyWithNewPriority(int priorityToUseForRuleCopy) { + return new TestRuleAdapter(isDefault, priorityToUseForRuleCopy, ruleArn); + } + } + + private static class TestLoadBalancerAdapter implements LoadBalancerAdapter { + private static final long serialVersionUID = -4853040303405813921L; + private Iterable rules; + + @Override + public String getName() { + return "Test Load Balancer Adapter"; + } + + @Override + public Iterable getRules() { + return rules; + } + + @Override + public void updateLoadBalancerListenerRulePriorities( + List> newPrioritiesForExistingRules) { + rules = Util.map(newPrioritiesForExistingRules, p -> p.getB().copyWithNewPriority(p.getA())); + } + + @Override + public void addRules(List rulesToAdd) { + rules = Util.concat(Arrays.asList(rules, rulesToAdd)); + } + } + + @Before + public void setUp() { + loadBalancerAdapter = new TestLoadBalancerAdapter(); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ true, 0, DEFAULT_RULE_NAME))); + ruleInserter = new LoadBalancerRuleInserter<>(loadBalancerAdapter, ApplicationLoadBalancer.MAX_PRIORITY, + ApplicationLoadBalancer.MAX_RULES_PER_LOADBALANCER); + assertUniqueAscendingPriorities(); + } + + @Test + public void simpleRuleInsertionTest() { + assertEquals(1, Util.size(getRulesSortedByPriority())); + addRules(1, true, 1); + assertEquals(2, Util.size(getRulesSortedByPriority())); + assertUniqueAscendingPriorities(); + } + + @Test + public void additionalRuleInsertionTest() { + assertEquals(1, Util.size(getRulesSortedByPriority())); + addRules(1, true, 1); + assertEquals(2, Util.size(getRulesSortedByPriority())); + addRules(1, true, 2); + assertUniqueAscendingPriorities(); + } + + @Test + public void testMassInsert() { + addRules(1, true, 1, 3, 2); + assertEquals(4, Util.size(getRulesSortedByPriority())); + assertUniqueAscendingPriorities(); + assertRuleOrder(1, 3, 2); + } + + @Test + public void testInsertBefore() { + addRules(1, true, 1, 2); + addRulesBefore(1, true, /* insert before: */ 2, /* insert rule numbers: */ 3, 4); + assertRuleOrder(1, 3, 4, 2); + } + + @Test + public void testContiguousInsertBeforeWithEnoughSpace() { + addRules(1, true, 1, 2); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 5, RULE_NAME_PREFIX+5))); + addRulesBefore(1, true, /* insert before: */ 5, /* insert rule numbers: */ 3, 4); + assertRuleOrder(1, 2, 3, 4, 5); + } + + @Test + public void testNonContiguousInsertBeforeWithoutEnoughSpace() { + addRules(1, true, 1); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 3, RULE_NAME_PREFIX+3))); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 5, RULE_NAME_PREFIX+5))); + addRulesBefore(1, false, /* insert before: */ 5, /* insert rule numbers: */ 2, 4); + assertRuleOrder(1, 2, 3, 4, 5); + } + + @Test + public void testInsertInHole() { + addRules(1, true, 1); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 4, RULE_NAME_PREFIX+4))); + addRules(2, true, 2, 3); + assertRuleOrder(1, 2, 3, 4); + } + + @Test + public void testInsertAfterTooSmallAHole() { + addRules(1, true, 1); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 4, RULE_NAME_PREFIX+4))); + addRules(2, true, 2, 3, 5); + assertRuleOrder(1, 4, 2, 3, 5); + } + + @Test + public void testNonContiguousInsertWithTooSmallAHole() { + addRules(1, true, 1); + loadBalancerAdapter.addRules(Arrays.asList(new TestRuleAdapter(/* isDefault */ false, 4, RULE_NAME_PREFIX+4))); + addRules(2, false, 2, 3, 5); + assertRuleOrder(1, 2, 3, 4, 5); + } + + @Test + public void testExceptionForTooManyRules() { + try { + addRules(1, true, Util.toArray(IntStream.range(0, 101).boxed()::iterator, new Integer[0])); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testExceptionForTooManyRulesWhenAdding() { + addRules(1, true, Util.toArray(IntStream.range(0, 50).boxed()::iterator, new Integer[0])); + try { + addRules(1, true, Util.toArray(IntStream.range(50, 101).boxed()::iterator, new Integer[0])); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testExceptionForRuleToInsertBeforeNotFound() { + addRules(1, true, 1); + try { + addRulesBefore(1, true, Optional.of(new TestRuleAdapter(/* isDefault */ false, 2, RULE_NAME_PREFIX+2)), 2); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + private void addRules(int priority, boolean forceContiguous, Integer... ruleNumbers) { + addRulesBefore(priority, forceContiguous, /* insertBefore */ Optional.empty(), ruleNumbers); + } + + private void addRulesBefore(int priority, boolean forceContiguous, int insertBeforeRuleNumber, Integer... ruleNumbers) { + addRulesBefore(priority, forceContiguous, Util.stream(getRulesSortedByPriority()) + .filter(r -> r.ruleArn().equals(RULE_NAME_PREFIX + insertBeforeRuleNumber)).findFirst(), ruleNumbers); + } + + private void addRulesBefore(int priority, boolean forceContiguous, Optional insertBefore, Integer... ruleNumbers) { + ruleInserter.addRulesAssigningUnusedPriorities(forceContiguous, insertBefore, + Util.map(Arrays.asList(ruleNumbers), ruleNumber->new TestRuleAdapter(/* isDefault */ false, priority, RULE_NAME_PREFIX + ruleNumber))); + } + + private void assertRuleOrder(Integer... ruleNumbers) { + assertEquals(Util.asList(Util.map(Arrays.asList(ruleNumbers), ruleNumber->RULE_NAME_PREFIX+ruleNumber)), + // ignore default rule at index 0 in comparison; look at numbered rules only + Util.asList(Util.map(getRulesSortedByPriority(), TestRuleAdapter::ruleArn)).subList(1, Util.size(getRulesSortedByPriority()))); + } + + private Iterable getRulesSortedByPriority() { + return ruleInserter.getRulesSortedByPriority(); + } + + private void assertUniqueAscendingPriorities() { + int lastPriority = -1; + for (final TestRuleAdapter rule : getRulesSortedByPriority()) { + assertTrue(Integer.valueOf(rule.priority()) > lastPriority); + lastPriority = Integer.valueOf(rule.priority()); + } + } +} diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java index 9039863c26e..47b03c2365e 100755 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ApplicationLoadBalancer.java @@ -53,6 +53,8 @@ public interface ApplicationLoadBalancer extends Named { */ public static final int MAX_PRIORITY = 50000; + AwsLandscape getLandscape(); + /** * The DNS name of this load balancer; can be used, e.g., to set a CNAME DNS record pointing * to this load balancer. @@ -106,21 +108,17 @@ public interface ApplicationLoadBalancer extends Named { * 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 insertBefore + * if empty, the new {@code rules} can be inserted anywhere in the list of rules; if a {@link Rule} is + * specified and that rule is part of this ALB's {@link #getRules() rules}, + * {@link #shiftRulesToMakeSpaceAt(int) make space} at that point and insert the new {@code rules} 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 copies of the original rules, with unused {@link Rule#priority() priorities} assigned, as passed already * to {@link #addRules(Rule...)}. */ - Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Rule... rules); - - /** - * For inserting e.g. Shard at a specific priority, we must ensure that the priority is unique in the ruleset. - * This function shifts every priority starting at the highest priority one higher for making the priority at {@code index} - * free. - * @param index - * index supposed to be free - * @throws IllegalStateException - * gets thrown if shifting exceeds the limit of priorities - */ - void shiftRulesToMakeSpaceAt(int index) throws IllegalStateException; + Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Optional insertBefore, Rule... rules); /** * Returns the priority which should be used as the next sharding priority. diff --git a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java index 1fc5801c736..31404da43d3 100644 --- a/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java +++ b/java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApplicationLoadBalancerImpl.java @@ -1,18 +1,17 @@ package com.sap.sse.landscape.aws.impl; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; import com.sap.sse.common.Duration; import com.sap.sse.common.Util; @@ -21,6 +20,7 @@ import com.sap.sse.landscape.aws.ApplicationLoadBalancer; import com.sap.sse.landscape.aws.AwsLandscape; import com.sap.sse.landscape.aws.TargetGroup; import com.sap.sse.landscape.aws.common.shared.PlainRedirectDTO; +import com.sap.sse.landscape.aws.impl.LoadBalancerRuleInserter.ALBRuleAdapter; import software.amazon.awssdk.services.elasticloadbalancingv2.model.Action; import software.amazon.awssdk.services.elasticloadbalancingv2.model.ActionTypeEnum; @@ -32,7 +32,6 @@ import software.amazon.awssdk.services.elasticloadbalancingv2.model.RedirectActi import software.amazon.awssdk.services.elasticloadbalancingv2.model.RedirectActionStatusCodeEnum; import software.amazon.awssdk.services.elasticloadbalancingv2.model.Rule; import software.amazon.awssdk.services.elasticloadbalancingv2.model.RuleCondition; -import software.amazon.awssdk.services.elasticloadbalancingv2.model.RulePriorityPair; import software.amazon.awssdk.services.elasticloadbalancingv2.model.TargetGroupTuple; public class ApplicationLoadBalancerImpl @@ -52,6 +51,11 @@ implements ApplicationLoadBalancer { this.loadBalancer = loadBalancer; this.landscape = landscape; } + + @Override + public AwsLandscape getLandscape() { + return landscape; + } @Override public String getName() { @@ -104,51 +108,6 @@ implements ApplicationLoadBalancer { return landscape.createLoadBalancerListenerRules(region, getListener(ProtocolEnum.HTTPS), rulesToAdd); } - /** - * Returns the priority if the priority is not higher than {@code MAX_PRIORITY} - * @param priority - * requested priority - * @return - * Priority if it's valid - * @throws IllegalStateException - * If the requested priority is higher than {@code MAX_PRIORITY} - */ - private int checkNewPriority(int priority) throws IllegalStateException { - if (priority < MAX_PRIORITY) { - return priority; - } else { - throw new IllegalStateException("Priority was greater than " + MAX_PRIORITY +"!"); - } - } - - @Override - public void shiftRulesToMakeSpaceAt(int targetPrio) throws IllegalStateException { - final Iterable rules = getRules(); - final TreeMap rulesSorted = getRulesSorted(rules); - int lastPrio = targetPrio; - boolean skipNext = false; - final Collection result = new ArrayList<>(); - if (rulesSorted.get(targetPrio) != null) {// if there is a rule on prio - for (Entry entry : rulesSorted.entrySet()) { - final Integer priority = entry.getKey(); - if (priority >= targetPrio && !skipNext) { - // if prio is higher than target prio and is not supposed to be skipped - if (priority - lastPrio > 0) { - // if there is a gap between current prio and the last one. -> so this one is not supposed to be - // skipped and every rule after this - skipNext = true; - break; - } else { - lastPrio = priority + 1; - result.add(RulePriorityPair.builder().ruleArn(entry.getValue().ruleArn()) - .priority(checkNewPriority(lastPrio)).build()); - } - } - } - landscape.updateLoadBalancerListenerRulePriorities(getRegion(), result); - } - } - @Override public int getFirstShardingPriority(String hostname) throws IllegalStateException { final Iterable rules = getRules(); @@ -202,65 +161,11 @@ implements ApplicationLoadBalancer { } @Override - public Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Rule... rules) { - final Iterable existingRules = getRules(); - if (Util.size(existingRules)-1 + rules.length > MAX_PRIORITY) { // -1 due to the default rule being part of existingRules - throw new IllegalArgumentException("The "+rules.length+" 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 "+MAX_PRIORITY+" by "+ - (Util.size(existingRules)-1 + rules.length - MAX_PRIORITY)); - } - final List result = new ArrayList<>(rules.length); - 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 = rules.length; - } else { - stepwidth = 1; - } - int rulesIndex = 0; - int previousPriority = 0; - final Iterator existingRulesIter = sortedExistingNonDefaultRules.iterator(); - while (rulesIndex < rules.length) { - // find next available slot - int nextPriority = MAX_PRIORITY+1; // if no further rule exists, the usable gap ends after MAX_PRIORITY - while (existingRulesIter.hasNext() && (nextPriority=Integer.valueOf(existingRulesIter.next().priority())) <= previousPriority+stepwidth) { - // not enough space for stepwidth many rules; keep on searching - previousPriority = nextPriority; - if (!existingRulesIter.hasNext()) { - nextPriority = MAX_PRIORITY+1; - } - } - if (previousPriority+stepwidth > MAX_PRIORITY) { - if (forceContiguous) { - previousPriority = squeezeExistingRulesAndReturnLastUsedPriority(sortedExistingNonDefaultRules); - nextPriority = MAX_PRIORITY+1; - // we previously checked already that there is enough room for the new set of rules - assert previousPriority + rules.length <= MAX_PRIORITY; - } else { - throw new IllegalStateException( - "The " + rules.length + " new rules don't fit into the existing rule set of load balancer " - + getName() + " without exceeding the maximum priority of " + MAX_PRIORITY); - } - } - while (rulesIndex < rules.length && ++previousPriority < nextPriority) { - final int priorityToUseForNextRule = previousPriority; - result.add(rules[rulesIndex++].copy(b->b.priority(""+priorityToUseForNextRule))); - } - } - addRules(result.toArray(new Rule[0])); - return result; - } - - private int squeezeExistingRulesAndReturnLastUsedPriority(final List sortedExistingNonDefaultRules) { - final List newPrioritiesForExistingRules = new LinkedList<>(); - int priority = 0; - for (final Rule existingRule : sortedExistingNonDefaultRules) { - newPrioritiesForExistingRules.add(RulePriorityPair.builder().ruleArn(existingRule.ruleArn()).priority(++priority).build()); - } - landscape.updateLoadBalancerListenerRulePriorities(getRegion(), newPrioritiesForExistingRules); - return priority; + public Iterable addRulesAssigningUnusedPriorities(boolean forceContiguous, Optional insertBefore, Rule... rules) { + final List rulesAsList = Arrays.asList(rules); + return Util.map(LoadBalancerRuleInserter.create(this, MAX_PRIORITY, MAX_RULES_PER_LOADBALANCER).addRulesAssigningUnusedPriorities( + forceContiguous, insertBefore.map(LoadBalancerRuleInserter::createRuleAdapter), + Util.map(rulesAsList, LoadBalancerRuleInserter::createRuleAdapter)), ALBRuleAdapter::getRule); } @Override @@ -314,11 +219,26 @@ implements ApplicationLoadBalancer { return Util.stream(getRules()).filter(r->isDefaultRedirectRule(r, hostname)).findAny() .map(defaultRedirectRule->updateDefaultRedirectRule(defaultRedirectRule.ruleArn(), hostname, pathWithLeadingSlash, query)) .orElseGet(()->{ - final Rule defaultRedirectRule = createDefaultRedirectRule(hostname, pathWithLeadingSlash, query); - addRulesAssigningUnusedPriorities(/* forceContiguous */ false, defaultRedirectRule); - return defaultRedirectRule; + return insertAndReturnDefaultRedirectRule(hostname, pathWithLeadingSlash, query); }); } + + /** + * Checks the load balancer rule set size. If already at its maximum, an {@link IllegalStateException} is thrown because + * adding a default rule is not possible, and this method does not handle moving an appplication replica set to a different + * load balancer. If there is space for at least one more rule, searches this load balancer's rule set for the first rule + * for hostname header {@code hostname}. It then uses {@link #shiftRulesToMakeSpaceAt(int)} for that position to make + * space for the new default redirect rule. + * + * @return the new default redirect rule that was inserted into this load balancer's HTTPS listener's rule set + */ + private Rule insertAndReturnDefaultRedirectRule(String hostname, String pathWithLeadingSlash, Optional 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));