diff options
Diffstat (limited to 'config-model-api/src')
7 files changed, 396 insertions, 140 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java b/config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java index 7464373df9e..bfd39fb66a5 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java @@ -6,6 +6,7 @@ import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -87,6 +88,19 @@ public class Bcp { public static Bcp empty() { return empty; } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Bcp bcp = (Bcp) o; + return defaultDeadline.equals(bcp.defaultDeadline) && groups.equals(bcp.groups); + } + + @Override + public int hashCode() { + return Objects.hash(defaultDeadline, groups); + } + + @Override public String toString() { if (isEmpty()) return "empty BCP"; return "BCP of " + @@ -117,6 +131,19 @@ public class Bcp { public Duration deadline() { return deadline; } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Group group = (Group) o; + return members.equals(group.members) && memberRegions.equals(group.memberRegions) && deadline.equals(group.deadline); + } + + @Override + public int hashCode() { + return Objects.hash(members, memberRegions, deadline); + } + + @Override public String toString() { return "BCP group of " + members; } diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java index bd5056deec6..a4be547fe70 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -1,8 +1,10 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.application.api; +import ai.vespa.validation.Validation; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; @@ -31,6 +33,7 @@ import static ai.vespa.validation.Validation.requireAtLeast; import static ai.vespa.validation.Validation.requireInRange; import static com.yahoo.config.application.api.DeploymentSpec.RevisionChange.whenClear; import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.next; +import static com.yahoo.config.application.api.DeploymentSpec.illegal; import static com.yahoo.config.provision.Environment.prod; /** @@ -57,7 +60,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { private final List<DeploymentSpec.ChangeBlocker> changeBlockers; private final Optional<String> globalServiceId; private final Optional<AthenzService> athenzService; - private final Optional<CloudAccount> cloudAccount; + private final Map<CloudName, CloudAccount> cloudAccounts; + private final Optional<Duration> hostTTL; private final Notifications notifications; private final List<Endpoint> endpoints; private final Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints; @@ -74,7 +78,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { List<DeploymentSpec.ChangeBlocker> changeBlockers, Optional<String> globalServiceId, Optional<AthenzService> athenzService, - Optional<CloudAccount> cloudAccount, + Map<CloudName, CloudAccount> cloudAccounts, + Optional<Duration> hostTTL, Notifications notifications, List<Endpoint> endpoints, Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints, @@ -97,7 +102,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { this.changeBlockers = Objects.requireNonNull(changeBlockers); this.globalServiceId = Objects.requireNonNull(globalServiceId); this.athenzService = Objects.requireNonNull(athenzService); - this.cloudAccount = Objects.requireNonNull(cloudAccount); + this.cloudAccounts = Map.copyOf(cloudAccounts); + this.hostTTL = Objects.requireNonNull(hostTTL); this.notifications = Objects.requireNonNull(notifications); this.endpoints = List.copyOf(Objects.requireNonNull(endpoints)); Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpointsCopy = new HashMap<>(); @@ -108,6 +114,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { validateEndpoints(globalServiceId, this.endpoints); validateChangeBlockers(changeBlockers, now); validateBcp(bcp); + hostTTL.filter(Duration::isNegative).ifPresent(ttl -> illegal("Host TTL cannot be negative")); } public InstanceName name() { return name; } @@ -257,16 +264,25 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { .filter(zone -> zone.concerns(environment, Optional.of(region))) .findFirst() .flatMap(DeploymentSpec.DeclaredZone::athenzService) - .or(() -> this.athenzService); + .or(() -> athenzService); } - /** Returns the cloud account to use for given environment and region, if any */ - public Optional<CloudAccount> cloudAccount(Environment environment, Optional<RegionName> region) { + /** Returns the cloud accounts to use for given environment and region, if any */ + public Map<CloudName, CloudAccount> cloudAccounts(Environment environment, RegionName region) { + return zones().stream() + .filter(zone -> zone.concerns(environment, Optional.of(region))) + .findFirst() + .map(DeploymentSpec.DeclaredZone::cloudAccounts) + .orElse(cloudAccounts); + } + + /** Returns the host TTL to use for given environment and region, if any */ + public Optional<Duration> hostTTL(Environment environment, Optional<RegionName> region) { return zones().stream() .filter(zone -> zone.concerns(environment, region)) .findFirst() - .flatMap(DeploymentSpec.DeclaredZone::cloudAccount) - .or(() -> cloudAccount); + .flatMap(DeploymentSpec.DeclaredZone::hostTTL) + .or(() -> hostTTL); } /** Returns the notification configuration of these instances */ @@ -315,22 +331,27 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { steps().equals(other.steps()) && athenzService.equals(other.athenzService) && notifications.equals(other.notifications) && - endpoints.equals(other.endpoints); + endpoints.equals(other.endpoints) && + zoneEndpoints.equals(other.zoneEndpoints) && + bcp.equals(other.bcp) && + tags.equals(other.tags); } @Override public int hashCode() { - return Objects.hash(globalServiceId, upgradePolicy, revisionTarget, upgradeRollout, changeBlockers, steps(), athenzService, notifications, endpoints); + return Objects.hash(globalServiceId, upgradePolicy, revisionTarget, upgradeRollout, changeBlockers, steps(), athenzService, notifications, endpoints, zoneEndpoints, bcp, tags); } int deployableHashCode() { List<DeploymentSpec.DeclaredZone> zones = zones().stream().filter(zone -> zone.concerns(prod)).toList(); - Object[] toHash = new Object[zones.size() + 4]; + Object[] toHash = new Object[zones.size() + 6]; int i = 0; toHash[i++] = name; toHash[i++] = endpoints; + toHash[i++] = zoneEndpoints; toHash[i++] = globalServiceId; toHash[i++] = tags; + toHash[i++] = bcp; for (DeploymentSpec.DeclaredZone zone : zones) toHash[i++] = Objects.hash(zone, zone.athenzService()); diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index 1f44e599e11..f355a61fa8a 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -1,11 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.application.api; +import ai.vespa.validation.Validation; import com.yahoo.collections.Comparables; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; @@ -44,6 +46,7 @@ public class DeploymentSpec { Optional.empty(), Optional.empty(), Optional.empty(), + Map.of(), Optional.empty(), List.of(), "<deployment version='1.0'/>", @@ -55,7 +58,8 @@ public class DeploymentSpec { private final Optional<Integer> majorVersion; private final Optional<AthenzDomain> athenzDomain; private final Optional<AthenzService> athenzService; - private final Optional<CloudAccount> cloudAccount; + private final Map<CloudName, CloudAccount> cloudAccounts; + private final Optional<Duration> hostTTL; private final List<Endpoint> endpoints; private final List<DeprecatedElement> deprecatedElements; @@ -65,7 +69,8 @@ public class DeploymentSpec { Optional<Integer> majorVersion, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService, - Optional<CloudAccount> cloudAccount, + Map<CloudName, CloudAccount> cloudAccounts, + Optional<Duration> hostTTL, List<Endpoint> endpoints, String xmlForm, List<DeprecatedElement> deprecatedElements) { @@ -73,7 +78,8 @@ public class DeploymentSpec { this.majorVersion = Objects.requireNonNull(majorVersion); this.athenzDomain = Objects.requireNonNull(athenzDomain); this.athenzService = Objects.requireNonNull(athenzService); - this.cloudAccount = Objects.requireNonNull(cloudAccount); + this.cloudAccounts = Map.copyOf(cloudAccounts); + this.hostTTL = Objects.requireNonNull(hostTTL); this.xmlForm = Objects.requireNonNull(xmlForm); this.endpoints = List.copyOf(Objects.requireNonNull(endpoints)); this.deprecatedElements = List.copyOf(Objects.requireNonNull(deprecatedElements)); @@ -81,6 +87,7 @@ public class DeploymentSpec { validateUpgradePoliciesOfIncreasingConservativeness(steps); validateAthenz(); validateApplicationEndpoints(); + hostTTL.filter(Duration::isNegative).ifPresent(ttl -> illegal("Host TTL cannot be negative")); } public boolean isEmpty() { return this == empty; } @@ -180,8 +187,33 @@ public class DeploymentSpec { // to have environment, instance or region variants on those. public Optional<AthenzService> athenzService() { return athenzService; } - /** Cloud account set on the deployment root; see discussion for {@link #athenzService}. */ - public Optional<CloudAccount> cloudAccount() { return cloudAccount; } + /** The most specific Athenz service for the given arguments. */ + public Optional<AthenzService> athenzService(InstanceName instance, Environment environment, RegionName region) { + return instance(instance).flatMap(spec -> spec.athenzService(environment, region)) + .or(this::athenzService); + } + + /** The most specific Cloud account for the given arguments. */ + public CloudAccount cloudAccount(CloudName cloud, InstanceName instance, ZoneId zone) { + return instance(instance).map(spec -> spec.cloudAccounts(zone.environment(), zone.region())) + .orElse(cloudAccounts) + .getOrDefault(cloud, CloudAccount.empty); + } + + public Map<CloudName, CloudAccount> cloudAccounts() { return cloudAccounts; } + + /** + * Additional host time-to-live for this application. Requires a custom cloud account to be set. + * This also applies only to zones with dynamic provisioning, and is then the time hosts are + * allowed remain empty, before being deprovisioned. This is useful for applications which frequently + * deploy to, e.g., test and staging zones, and want to avoid the delay of having to provision hosts. + */ + public Optional<Duration> hostTTL(InstanceName instance, Environment environment, RegionName region) { + return instance(instance).flatMap(spec -> spec.hostTTL(environment, Optional.of(region))) + .or(this::hostTTL); + } + + public Optional<Duration> hostTTL() { return hostTTL; } /** * Returns the most specific zone endpoint, where specificity is given, in decreasing order: @@ -262,7 +294,7 @@ public class DeploymentSpec { } - private static void illegal(String message) { + static void illegal(String message) { throw new IllegalArgumentException(message); } @@ -370,6 +402,8 @@ public class DeploymentSpec { return true; } + public Optional<Duration> hostTTL() { return Optional.empty(); } + } /** A deployment step which is to wait for some time before progressing to the next step */ @@ -402,25 +436,28 @@ public class DeploymentSpec { private final boolean active; private final Optional<AthenzService> athenzService; private final Optional<String> testerFlavor; - private final Optional<CloudAccount> cloudAccount; + private final Map<CloudName, CloudAccount> cloudAccounts; + private final Optional<Duration> hostTTL; public DeclaredZone(Environment environment) { - this(environment, Optional.empty(), false, Optional.empty(), Optional.empty(), Optional.empty()); + this(environment, Optional.empty(), false, Optional.empty(), Optional.empty(), Map.of(), Optional.empty()); } public DeclaredZone(Environment environment, Optional<RegionName> region, boolean active, Optional<AthenzService> athenzService, Optional<String> testerFlavor, - Optional<CloudAccount> cloudAccount) { + Map<CloudName, CloudAccount> cloudAccounts, Optional<Duration> hostTTL) { if (environment != Environment.prod && region.isPresent()) illegal("Non-prod environments cannot specify a region"); if (environment == Environment.prod && region.isEmpty()) illegal("Prod environments must be specified with a region"); + hostTTL.filter(Duration::isNegative).ifPresent(ttl -> illegal("Host TTL cannot be negative")); this.environment = Objects.requireNonNull(environment); this.region = Objects.requireNonNull(region); this.active = active; this.athenzService = Objects.requireNonNull(athenzService); this.testerFlavor = Objects.requireNonNull(testerFlavor); - this.cloudAccount = Objects.requireNonNull(cloudAccount); + this.cloudAccounts = Map.copyOf(cloudAccounts); + this.hostTTL = Objects.requireNonNull(hostTTL); } public Environment environment() { return environment; } @@ -433,11 +470,9 @@ public class DeploymentSpec { public Optional<String> testerFlavor() { return testerFlavor; } - public Optional<AthenzService> athenzService() { return athenzService; } + Optional<AthenzService> athenzService() { return athenzService; } - public Optional<CloudAccount> cloudAccount() { - return cloudAccount; - } + Map<CloudName, CloudAccount> cloudAccounts() { return cloudAccounts; } @Override public List<DeclaredZone> zones() { return List.of(this); } @@ -472,15 +507,23 @@ public class DeploymentSpec { return environment + (region.map(regionName -> "." + regionName).orElse("")); } + @Override + public Optional<Duration> hostTTL() { + return hostTTL; + } + } /** A declared production test */ public static class DeclaredTest extends Step { private final RegionName region; + private final Optional<Duration> hostTTL; - public DeclaredTest(RegionName region) { + public DeclaredTest(RegionName region, Optional<Duration> hostTTL) { this.region = Objects.requireNonNull(region); + this.hostTTL = Objects.requireNonNull(hostTTL); + hostTTL.filter(Duration::isNegative).ifPresent(ttl -> illegal("Host TTL cannot be negative")); } @Override @@ -497,6 +540,11 @@ public class DeploymentSpec { } @Override + public Optional<Duration> hostTTL() { + return hostTTL; + } + + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 89373d8bca0..38562eefb03 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -25,6 +25,7 @@ import com.yahoo.config.application.api.TimeWindow; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; @@ -94,6 +95,7 @@ public class DeploymentSpecXmlReader { private static final String majorVersionAttribute = "major-version"; private static final String globalServiceIdAttribute = "global-service-id"; private static final String cloudAccountAttribute = "cloud-account"; + private static final String hostTTLAttribute = "empty-host-ttl"; private final boolean validate; private final Clock clock; @@ -164,7 +166,8 @@ public class DeploymentSpecXmlReader { optionalIntegerAttribute(majorVersionAttribute, root), stringAttribute(athenzDomainAttribute, root).map(AthenzDomain::from), stringAttribute(athenzServiceAttribute, root).map(AthenzService::from), - stringAttribute(cloudAccountAttribute, root).map(CloudAccount::from), + readCloudAccounts(root), + stringAttribute(hostTTLAttribute, root).map(s -> toDuration(s, "empty host TTL")), applicationEndpoints, xmlForm, deprecatedElements); @@ -203,7 +206,8 @@ public class DeploymentSpecXmlReader { int maxIdleHours = getWithFallback(instanceElement, parentTag, upgradeTag, "max-idle-hours", Integer::parseInt, 8); List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceElement, parentTag); Optional<AthenzService> athenzService = mostSpecificAttribute(instanceElement, athenzServiceAttribute).map(AthenzService::from); - Optional<CloudAccount> cloudAccount = mostSpecificAttribute(instanceElement, cloudAccountAttribute).map(CloudAccount::from); + Map<CloudName, CloudAccount> cloudAccounts = readCloudAccounts(instanceElement); + Optional<Duration> hostTTL = mostSpecificAttribute(instanceElement, hostTTLAttribute).map(s -> toDuration(s, "empty host TTL")); Notifications notifications = readNotifications(instanceElement, parentTag); // Values where there is no default @@ -232,7 +236,8 @@ public class DeploymentSpecXmlReader { changeBlockers, Optional.ofNullable(prodAttributes.get(globalServiceIdAttribute)), athenzService, - cloudAccount, + cloudAccounts, + hostTTL, notifications, endpoints, zoneEndpoints, @@ -258,6 +263,7 @@ public class DeploymentSpecXmlReader { } // Consume the given tag as 0-N steps. 0 if it is not a step, >1 if it contains multiple nested steps that should be flattened + @SuppressWarnings("fallthrough") private List<Step> readNonInstanceSteps(Element stepTag, Map<String, String> prodAttributes, Element parentTag, Bcp defaultBcp) { Optional<AthenzService> athenzService = mostSpecificAttribute(stepTag, athenzServiceAttribute).map(AthenzService::from); Optional<String> testerFlavor = mostSpecificAttribute(stepTag, testerFlavorAttribute); @@ -272,12 +278,10 @@ public class DeploymentSpecXmlReader { case testTag: if (Stream.iterate(stepTag, Objects::nonNull, Node::getParentNode) .anyMatch(node -> prodTag.equals(node.getNodeName()))) { - // A production test - return List.of(new DeclaredTest(RegionName.from(XML.getValue(stepTag).trim()))); + return List.of(new DeclaredTest(RegionName.from(XML.getValue(stepTag).trim()), readHostTTL(stepTag))); // A production test } - return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor, readCloudAccount(stepTag))); - case devTag, perfTag, stagingTag: - return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor, readCloudAccount(stepTag))); + case devTag, perfTag, stagingTag: // Intentional fallthrough from test tag. + return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor, readCloudAccounts(stepTag), readHostTTL(stepTag))); case prodTag: // regions, delay and parallel may be nested within, but we can flatten them return XML.getChildren(stepTag).stream() .flatMap(child -> readNonInstanceSteps(child, prodAttributes, stepTag, defaultBcp).stream()) @@ -667,8 +671,13 @@ public class DeploymentSpecXmlReader { /** Returns the given non-blank attribute of tag as a string, if any */ private static Optional<String> stringAttribute(String attributeName, Element tag) { + return stringAttribute(attributeName, tag, true); + } + + /** Returns the given non-blank attribute of tag as a string, if any */ + private static Optional<String> stringAttribute(String attributeName, Element tag, boolean ignoreBlanks) { String value = tag.getAttribute(attributeName); - return Optional.of(value).filter(s -> !s.isBlank()); + return Optional.of(value).filter(s -> (tag.getAttributeNode(attributeName) != null && ! ignoreBlanks || ! s.isBlank())); } /** Returns the given non-blank attribute of tag or throw */ @@ -682,11 +691,27 @@ public class DeploymentSpecXmlReader { Optional<String> testerFlavor, Element regionTag) { return new DeclaredZone(environment, Optional.of(RegionName.from(XML.getValue(regionTag).trim())), readActive(regionTag), athenzService, testerFlavor, - readCloudAccount(regionTag)); + readCloudAccounts(regionTag), readHostTTL(regionTag)); + } + + private Map<CloudName, CloudAccount> readCloudAccounts(Element tag) { + return mostSpecificAttribute(tag, cloudAccountAttribute, false) + .map(value -> { + Map<CloudName, CloudAccount> accounts = new HashMap<>(); + for (String part : value.split(",")) { + CloudAccount account = CloudAccount.from(part); + accounts.merge(account.cloudName(), account, (o, n) -> { + throw illegal("both '" + o.account() + "' and '" + n.account() + "' " + + "are declared for cloud '" + o.cloudName() + "', in '" + value + "'"); + }); + } + return accounts; + }) + .orElse(Map.of()); } - private Optional<CloudAccount> readCloudAccount(Element tag) { - return mostSpecificAttribute(tag, cloudAccountAttribute).map(CloudAccount::from); + private Optional<Duration> readHostTTL(Element tag) { + return mostSpecificAttribute(tag, hostTTLAttribute).map(s -> toDuration(s, "empty host TTL")); } private Optional<String> readGlobalServiceId(Element environmentTag) { @@ -795,17 +820,22 @@ public class DeploymentSpecXmlReader { } /** Returns the given attribute from the given tag or its closest ancestor with the attribute. */ - private static Optional<String> mostSpecificAttribute(Element tag, String attributeName) { + private static Optional<String> mostSpecificAttribute(Element tag, String attributeName, boolean ignoreBlanks) { return Stream.iterate(tag, Objects::nonNull, Node::getParentNode) .filter(Element.class::isInstance) .map(Element.class::cast) - .flatMap(element -> stringAttribute(attributeName, element).stream()) + .flatMap(element -> stringAttribute(attributeName, element, ignoreBlanks).stream()) .findFirst(); } + /** Returns the given attribute from the given tag or its closest ancestor with the attribute. */ + private static Optional<String> mostSpecificAttribute(Element tag, String attributeName) { + return mostSpecificAttribute(tag, attributeName, true); + } + /** - * Returns a string consisting of a number followed by "m" or "h" to a duration given in that unit, - * or zero duration if null of blank. + * Returns a string consisting of a number followed by "m", "h" or "d" to a duration given in that unit, + * or zero duration if null or blank. */ private static Duration toDuration(String durationSpec, String sourceDescription) { try { @@ -844,7 +874,7 @@ public class DeploymentSpecXmlReader { } } - private static void illegal(String message) { + private static IllegalArgumentException illegal(String message) { throw new IllegalArgumentException(message); } diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index 0d42df88d04..a9cbe82895f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -144,6 +144,7 @@ public interface ModelContext { /** Warning: As elsewhere in this package, do not make backwards incompatible changes that will break old config models! */ interface Properties { + FeatureFlags featureFlags(); boolean multitenant(); ApplicationId applicationId(); diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 89b7318739e..d4312a0e54e 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -6,6 +6,7 @@ import com.yahoo.config.application.api.Endpoint.Level; import com.yahoo.config.application.api.Endpoint.Target; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; @@ -25,6 +26,7 @@ import java.time.ZoneId; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -32,6 +34,13 @@ import java.util.stream.Collectors; import static com.yahoo.config.application.api.Notifications.Role.author; import static com.yahoo.config.application.api.Notifications.When.failing; import static com.yahoo.config.application.api.Notifications.When.failingCommit; +import static com.yahoo.config.provision.CloudName.AWS; +import static com.yahoo.config.provision.CloudName.GCP; +import static com.yahoo.config.provision.Environment.dev; +import static com.yahoo.config.provision.Environment.perf; +import static com.yahoo.config.provision.Environment.prod; +import static com.yahoo.config.provision.Environment.staging; +import static com.yahoo.config.provision.Environment.test; import static com.yahoo.config.provision.zone.ZoneId.defaultId; import static com.yahoo.config.provision.zone.ZoneId.from; import static org.junit.Assert.assertEquals; @@ -60,11 +69,11 @@ public class DeploymentSpecTest { assertEquals(specXml, spec.xmlForm()); assertEquals(1, spec.requireInstance("default").steps().size()); assertFalse(spec.majorVersion().isPresent()); - assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.test)); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.of(RegionName.from("region1")))); // test steps specify no region - assertFalse(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.empty())); + assertTrue(spec.requireInstance("default").steps().get(0).concerns(test)); + assertTrue(spec.requireInstance("default").concerns(test, Optional.empty())); + assertTrue(spec.requireInstance("default").concerns(test, Optional.of(RegionName.from("region1")))); // test steps specify no region + assertFalse(spec.requireInstance("default").concerns(staging, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.empty())); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); } @@ -97,10 +106,10 @@ public class DeploymentSpecTest { DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals(1, spec.steps().size()); assertEquals(1, spec.requireInstance("default").steps().size()); - assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.staging)); - assertFalse(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.empty())); + assertTrue(spec.requireInstance("default").steps().get(0).concerns(staging)); + assertFalse(spec.requireInstance("default").concerns(test, Optional.empty())); + assertTrue(spec.requireInstance("default").concerns(staging, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.empty())); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); } @@ -121,17 +130,17 @@ public class DeploymentSpecTest { assertEquals(1, spec.steps().size()); assertEquals(2, spec.requireInstance("default").steps().size()); - assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").steps().get(0).concerns(prod, Optional.of(RegionName.from("us-east1")))); assertFalse(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(0)).active()); - assertTrue(spec.requireInstance("default").steps().get(1).concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertTrue(spec.requireInstance("default").steps().get(1).concerns(prod, Optional.of(RegionName.from("us-west1")))); assertTrue(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(1)).active()); - assertFalse(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); - assertFalse(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("no-such-region")))); + assertFalse(spec.requireInstance("default").concerns(test, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(staging, Optional.empty())); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-west1")))); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("no-such-region")))); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, spec.requireInstance("default").upgradePolicy()); @@ -293,7 +302,7 @@ public class DeploymentSpecTest { assertEquals(1, instance2.steps().size()); assertEquals(1, instance2.zones().size()); - assertTrue(instance2.steps().get(0).concerns(Environment.prod, Optional.of(RegionName.from("us-central1")))); + assertTrue(instance2.steps().get(0).concerns(prod, Optional.of(RegionName.from("us-central1")))); } @Test @@ -322,25 +331,25 @@ public class DeploymentSpecTest { assertEquals(5, instance.steps().size()); assertEquals(4, instance.zones().size()); - assertTrue(instance.steps().get(0).concerns(Environment.test)); + assertTrue(instance.steps().get(0).concerns(test)); - assertTrue(instance.steps().get(1).concerns(Environment.staging)); + assertTrue(instance.steps().get(1).concerns(staging)); - assertTrue(instance.steps().get(2).concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(instance.steps().get(2).concerns(prod, Optional.of(RegionName.from("us-east1")))); assertFalse(((DeploymentSpec.DeclaredZone)instance.steps().get(2)).active()); assertTrue(instance.steps().get(3) instanceof DeploymentSpec.Delay); assertEquals(3 * 60 * 60 + 30 * 60, instance.steps().get(3).delay().getSeconds()); - assertTrue(instance.steps().get(4).concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertTrue(instance.steps().get(4).concerns(prod, Optional.of(RegionName.from("us-west1")))); assertTrue(((DeploymentSpec.DeclaredZone)instance.steps().get(4)).active()); - assertTrue(instance.concerns(Environment.test, Optional.empty())); - assertTrue(instance.concerns(Environment.test, Optional.of(RegionName.from("region1")))); // test steps specify no region - assertTrue(instance.concerns(Environment.staging, Optional.empty())); - assertTrue(instance.concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertTrue(instance.concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertFalse(instance.concerns(Environment.prod, Optional.of(RegionName.from("no-such-region")))); + assertTrue(instance.concerns(test, Optional.empty())); + assertTrue(instance.concerns(test, Optional.of(RegionName.from("region1")))); // test steps specify no region + assertTrue(instance.concerns(staging, Optional.empty())); + assertTrue(instance.concerns(prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(instance.concerns(prod, Optional.of(RegionName.from("us-west1")))); + assertFalse(instance.concerns(prod, Optional.of(RegionName.from("no-such-region")))); assertFalse(instance.globalServiceId().isPresent()); } @@ -563,7 +572,7 @@ public class DeploymentSpecTest { DeploymentInstanceSpec instance = spec.instances().get(0); assertEquals("default", instance.name().value()); - assertEquals("service", instance.athenzService(Environment.prod, RegionName.defaultName()).get().value()); + assertEquals("service", instance.athenzService(prod, RegionName.defaultName()).get().value()); } @Test @@ -695,9 +704,9 @@ public class DeploymentSpecTest { List<DeploymentSpec.Step> innerParallelSteps = secondSerialSteps.get(2).steps(); assertEquals(3, innerParallelSteps.size()); assertEquals("prod.ap-northeast-1", innerParallelSteps.get(0).toString()); - assertEquals("no-service", spec.requireInstance("instance").athenzService(Environment.prod, RegionName.from("ap-northeast-1")).get().value()); + assertEquals("no-service", spec.requireInstance("instance").athenzService(prod, RegionName.from("ap-northeast-1")).get().value()); assertEquals("prod.ap-southeast-2", innerParallelSteps.get(1).toString()); - assertEquals("in-service", spec.requireInstance("instance").athenzService(Environment.prod, RegionName.from("ap-southeast-2")).get().value()); + assertEquals("in-service", spec.requireInstance("instance").athenzService(prod, RegionName.from("ap-southeast-2")).get().value()); assertEquals("tests for prod.aws-us-east-1a", innerParallelSteps.get(2).toString()); } @@ -956,7 +965,7 @@ public class DeploymentSpecTest { DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("domain", spec.athenzDomain().get().value()); assertEquals("service", spec.athenzService().get().value()); - assertEquals("service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-west-1")).get().value()); } @@ -979,11 +988,11 @@ public class DeploymentSpecTest { assertEquals("domain", spec.athenzDomain().get().value()); assertEquals("service", spec.athenzService().get().value()); - assertEquals("prod-service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-central-1")).get().value()); - assertEquals("prod-service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-west-1")).get().value()); - assertEquals("prod-service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-east-3")).get().value()); } @@ -1014,11 +1023,11 @@ public class DeploymentSpecTest { """; DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("domain", spec.athenzDomain().get().value()); - assertEquals("service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-west-1")).get().value()); - assertEquals("service", spec.requireInstance("instance1").athenzService(Environment.prod, + assertEquals("service", spec.requireInstance("instance1").athenzService(prod, RegionName.from("us-east-3")).get().value()); - assertEquals("service", spec.requireInstance("instance2").athenzService(Environment.prod, + assertEquals("service", spec.requireInstance("instance2").athenzService(prod, RegionName.from("us-east-3")).get().value()); } @@ -1036,7 +1045,7 @@ public class DeploymentSpecTest { DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("domain", spec.athenzDomain().get().value()); assertEquals(Optional.empty(), spec.athenzService()); - assertEquals("service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-west-1")).get().value()); + assertEquals("service", spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")).get().value()); } @Test @@ -1054,13 +1063,13 @@ public class DeploymentSpecTest { ); DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("service", - spec.requireInstance("default").athenzService(Environment.test, + spec.requireInstance("default").athenzService(test, RegionName.from("us-east-1")).get().value()); assertEquals("staging-service", - spec.requireInstance("default").athenzService(Environment.staging, + spec.requireInstance("default").athenzService(staging, RegionName.from("us-north-1")).get().value()); assertEquals("prod-service", - spec.requireInstance("default").athenzService(Environment.prod, + spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")).get().value()); } @@ -1273,8 +1282,8 @@ public class DeploymentSpecTest { assertEquals(List.of(RegionName.from("us-east")), spec.requireInstance("default").endpoints().get(0).regions()); - var zone = from(Environment.prod, RegionName.from("us-east")); - var testZone = from(Environment.test, RegionName.from("us-east")); + var zone = from(prod, RegionName.from("us-east")); + var testZone = from(test, RegionName.from("us-east")); assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.from("custom"), zone, ClusterSpec.Id.from("bax"))); assertEquals(ZoneEndpoint.defaultEndpoint, @@ -1752,18 +1761,19 @@ public class DeploymentSpecTest { public void cloudAccount() { String r = """ - <deployment version='1.0' cloud-account='100000000000'> + <deployment version='1.0' cloud-account='100000000000,gcp:foobar'> <instance id='alpha'> <prod cloud-account='800000000000'> <region>us-east-1</region> </prod> </instance> <instance id='beta' cloud-account='200000000000'> - <staging cloud-account='600000000000'/> + <staging cloud-account='gcp:barbaz'/> <perf cloud-account='700000000000'/> <prod> <region>us-west-1</region> <region cloud-account='default'>us-west-2</region> + <region cloud-account=''>us-west-3</region> </prod> </instance> <instance id='main'> @@ -1777,22 +1787,103 @@ public class DeploymentSpecTest { </deployment> """; DeploymentSpec spec = DeploymentSpec.fromXml(r); - assertEquals(Optional.of(CloudAccount.from("100000000000")), spec.cloudAccount()); - assertCloudAccount("800000000000", spec.requireInstance("alpha"), Environment.prod, "us-east-1"); - assertCloudAccount("200000000000", spec.requireInstance("beta"), Environment.prod, "us-west-1"); - assertCloudAccount("600000000000", spec.requireInstance("beta"), Environment.staging, ""); - assertCloudAccount("700000000000", spec.requireInstance("beta"), Environment.perf, ""); - assertCloudAccount("200000000000", spec.requireInstance("beta"), Environment.dev, ""); - assertCloudAccount("300000000000", spec.requireInstance("main"), Environment.prod, "us-east-1"); - assertCloudAccount("100000000000", spec.requireInstance("main"), Environment.prod, "eu-west-1"); - assertCloudAccount("400000000000", spec.requireInstance("main"), Environment.dev, ""); - assertCloudAccount("500000000000", spec.requireInstance("main"), Environment.test, ""); - assertCloudAccount("100000000000", spec.requireInstance("main"), Environment.staging, ""); - assertCloudAccount("default", spec.requireInstance("beta"), Environment.prod, "us-west-2"); - } - - private void assertCloudAccount(String expected, DeploymentInstanceSpec instance, Environment environment, String region) { - assertEquals(Optional.of(expected).map(CloudAccount::from), instance.cloudAccount(environment, Optional.of(region).filter(s -> !s.isEmpty()).map(RegionName::from))); + assertEquals(Map.of(AWS, CloudAccount.from("100000000000"), + GCP, CloudAccount.from("gcp:foobar")), spec.cloudAccounts()); + assertCloudAccount("800000000000", spec, AWS, "alpha", prod, "us-east-1"); + assertCloudAccount("", spec, GCP, "alpha", prod, "us-east-1"); + assertCloudAccount("200000000000", spec, AWS, "beta", prod, "us-west-1"); + assertCloudAccount("", spec, AWS, "beta", staging, "default"); + assertCloudAccount("gcp:barbaz", spec, GCP, "beta", staging, "default"); + assertCloudAccount("700000000000", spec, AWS, "beta", perf, "default"); + assertCloudAccount("200000000000", spec, AWS, "beta", dev, "default"); + assertCloudAccount("300000000000", spec, AWS, "main", prod, "us-east-1"); + assertCloudAccount("100000000000", spec, AWS, "main", prod, "eu-west-1"); + assertCloudAccount("400000000000", spec, AWS, "main", dev, "default"); + assertCloudAccount("500000000000", spec, AWS, "main", test, "default"); + assertCloudAccount("100000000000", spec, AWS, "main", staging, "default"); + assertCloudAccount("default", spec, AWS, "beta", prod, "us-west-2"); + assertCloudAccount("", spec, GCP, "beta", prod, "us-west-2"); + assertCloudAccount("", spec, AWS, "beta", prod, "us-west-3"); + assertCloudAccount("", spec, GCP, "beta", prod, "us-west-3"); + } + + @Test + public void hostTTL() { + String r = + """ + <deployment version='1.0' cloud-account='100000000000' empty-host-ttl='1h'> + <instance id='alpha'> + <staging /> + <prod empty-host-ttl='1m'> + <region>us-east</region> + <region empty-host-ttl='2m'>us-west</region> + <test>us-east</test> + <test empty-host-ttl='3m'>us-west</test> + </prod> + </instance> + <instance id='beta'> + <staging empty-host-ttl='3d'/> + <perf empty-host-ttl='4h'/> + <prod> + <region>us-east</region> + <region empty-host-ttl='0d'>us-west</region> + </prod> + </instance> + <instance id='gamma' empty-host-ttl='6h'> + <dev empty-host-ttl='7d'/> + <prod> + <region>us-east</region> + </prod> + </instance> + </deployment> + """; + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Map.of(AWS, CloudAccount.from("100000000000")), spec.cloudAccounts()); + + assertHostTTL(Duration.ofHours(1), spec, "alpha", test, null); + assertHostTTL(Duration.ofHours(1), spec, "alpha", staging, null); + assertHostTTL(Duration.ofHours(1), spec, "alpha", dev, null); + assertHostTTL(Duration.ofHours(1), spec, "alpha", perf, null); + assertHostTTL(Duration.ofMinutes(1), spec, "alpha", prod, "us-east"); + assertHostTTL(Duration.ofMinutes(2), spec, "alpha", prod, "us-west"); + assertEquals(Optional.of(Duration.ofMinutes(1)), spec.requireInstance("alpha").steps().stream() + .filter(step -> step.concerns(prod, Optional.of(RegionName.from("us-east"))) && step.isTest()) + .findFirst().orElseThrow() + .hostTTL()); + assertEquals(Optional.of(Duration.ofMinutes(3)), spec.requireInstance("alpha").steps().stream() + .filter(step -> step.concerns(prod, Optional.of(RegionName.from("us-west"))) && step.isTest()) + .findFirst().orElseThrow() + .hostTTL()); + + assertHostTTL(Duration.ofHours(1), spec, "beta", test, null); + assertHostTTL(Duration.ofDays(3), spec, "beta", staging, null); + assertHostTTL(Duration.ofHours(1), spec, "beta", dev, null); + assertHostTTL(Duration.ofHours(4), spec, "beta", perf, null); + assertHostTTL(Duration.ofHours(1), spec, "beta", prod, "us-east"); + assertHostTTL(Duration.ZERO, spec, "beta", prod, "us-west"); + + assertHostTTL(Duration.ofHours(6), spec, "gamma", test, null); + assertHostTTL(Duration.ofHours(6), spec, "gamma", staging, null); + assertHostTTL(Duration.ofDays(7), spec, "gamma", dev, null); + assertHostTTL(Duration.ofHours(6), spec, "gamma", perf, null); + assertHostTTL(Duration.ofHours(6), spec, "gamma", prod, "us-east"); + assertHostTTL(Duration.ofHours(6), spec, "gamma", prod, "us-west"); + + assertHostTTL(Duration.ofHours(1), spec, "nope", test, null); + assertHostTTL(Duration.ofHours(1), spec, "nope", staging, null); + assertHostTTL(Duration.ofHours(1), spec, "nope", dev, null); + assertHostTTL(Duration.ofHours(1), spec, "nope", perf, null); + assertHostTTL(Duration.ofHours(1), spec, "nope", prod, "us-east"); + assertHostTTL(Duration.ofHours(1), spec, "nope", prod, "us-west"); + } + + private void assertCloudAccount(String expected, DeploymentSpec spec, CloudName cloud, String instance, Environment environment, String region) { + assertEquals(CloudAccount.from(expected), + spec.cloudAccount(cloud, InstanceName.from(instance), com.yahoo.config.provision.zone.ZoneId.from(environment, RegionName.from(region)))); + } + + private void assertHostTTL(Duration expected, DeploymentSpec spec, String instance, Environment environment, String region) { + assertEquals(Optional.of(expected), spec.hostTTL(InstanceName.from(instance), environment, region == null ? RegionName.defaultName() : RegionName.from(region))); } private static void assertInvalid(String deploymentSpec, String errorMessagePart) { diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java index 38410cc5b37..e5578723612 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java @@ -18,6 +18,7 @@ import java.time.Instant; import java.time.ZoneId; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -25,6 +26,10 @@ import java.util.stream.Collectors; import static com.yahoo.config.application.api.Notifications.Role.author; import static com.yahoo.config.application.api.Notifications.When.failing; import static com.yahoo.config.application.api.Notifications.When.failingCommit; +import static com.yahoo.config.provision.CloudName.AWS; +import static com.yahoo.config.provision.Environment.dev; +import static com.yahoo.config.provision.Environment.prod; +import static com.yahoo.config.provision.Environment.test; import static com.yahoo.config.provision.zone.ZoneId.defaultId; import static com.yahoo.config.provision.zone.ZoneId.from; import static org.junit.Assert.assertEquals; @@ -49,11 +54,11 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(specXml, spec.xmlForm()); assertEquals(1, spec.steps().size()); assertFalse(spec.majorVersion().isPresent()); - assertTrue(spec.steps().get(0).concerns(Environment.test)); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.of(RegionName.from("region1")))); // test steps specify no region + assertTrue(spec.steps().get(0).concerns(test)); + assertTrue(spec.requireInstance("default").concerns(test, Optional.empty())); + assertTrue(spec.requireInstance("default").concerns(test, Optional.of(RegionName.from("region1")))); // test steps specify no region assertFalse(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.empty())); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); } @@ -83,9 +88,9 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(1, spec.steps().size()); assertEquals(1, spec.requireInstance("default").steps().size()); assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.staging)); - assertFalse(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(test, Optional.empty())); assertTrue(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.empty())); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); } @@ -104,17 +109,17 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(1, spec.steps().size()); assertEquals(2, spec.requireInstance("default").steps().size()); - assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").steps().get(0).concerns(prod, Optional.of(RegionName.from("us-east1")))); assertFalse(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(0)).active()); - assertTrue(spec.requireInstance("default").steps().get(1).concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertTrue(spec.requireInstance("default").steps().get(1).concerns(prod, Optional.of(RegionName.from("us-west1")))); assertTrue(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(1)).active()); - assertFalse(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); + assertFalse(spec.requireInstance("default").concerns(test, Optional.empty())); assertFalse(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("no-such-region")))); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-west1")))); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("no-such-region")))); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, spec.requireInstance("default").upgradePolicy()); @@ -139,25 +144,25 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(5, spec.requireInstance("default").steps().size()); assertEquals(4, spec.requireInstance("default").zones().size()); - assertTrue(spec.requireInstance("default").steps().get(0).concerns(Environment.test)); + assertTrue(spec.requireInstance("default").steps().get(0).concerns(test)); assertTrue(spec.requireInstance("default").steps().get(1).concerns(Environment.staging)); - assertTrue(spec.requireInstance("default").steps().get(2).concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").steps().get(2).concerns(prod, Optional.of(RegionName.from("us-east1")))); assertFalse(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(2)).active()); assertTrue(spec.requireInstance("default").steps().get(3) instanceof DeploymentSpec.Delay); assertEquals(3 * 60 * 60 + 30 * 60, spec.requireInstance("default").steps().get(3).delay().getSeconds()); - assertTrue(spec.requireInstance("default").steps().get(4).concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertTrue(spec.requireInstance("default").steps().get(4).concerns(prod, Optional.of(RegionName.from("us-west1")))); assertTrue(((DeploymentSpec.DeclaredZone)spec.requireInstance("default").steps().get(4)).active()); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.test, Optional.of(RegionName.from("region1")))); // test steps specify no region + assertTrue(spec.requireInstance("default").concerns(test, Optional.empty())); + assertTrue(spec.requireInstance("default").concerns(test, Optional.of(RegionName.from("region1")))); // test steps specify no region assertTrue(spec.requireInstance("default").concerns(Environment.staging, Optional.empty())); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertTrue(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertFalse(spec.requireInstance("default").concerns(Environment.prod, Optional.of(RegionName.from("no-such-region")))); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("us-west1")))); + assertFalse(spec.requireInstance("default").concerns(prod, Optional.of(RegionName.from("no-such-region")))); assertFalse(spec.requireInstance("default").globalServiceId().isPresent()); } @@ -436,9 +441,9 @@ public class DeploymentSpecWithoutInstanceTest { List<DeploymentSpec.Step> innerParallelSteps = secondSerialSteps.get(2).steps(); assertEquals(3, innerParallelSteps.size()); assertEquals("prod.ap-northeast-1", innerParallelSteps.get(0).toString()); - assertEquals("no-service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("ap-northeast-1")).get().value()); + assertEquals("no-service", spec.requireInstance("default").athenzService(prod, RegionName.from("ap-northeast-1")).get().value()); assertEquals("prod.ap-southeast-2", innerParallelSteps.get(1).toString()); - assertEquals("service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("ap-southeast-2")).get().value()); + assertEquals("service", spec.requireInstance("default").athenzService(prod, RegionName.from("ap-southeast-2")).get().value()); assertEquals("tests for prod.aws-us-east-1a", innerParallelSteps.get(2).toString()); } @@ -534,7 +539,7 @@ public class DeploymentSpecWithoutInstanceTest { ); DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals(spec.athenzDomain().get().value(), "domain"); - assertEquals(spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-west-1")).get().value(), "service"); + assertEquals(spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")).get().value(), "service"); } @Test @@ -553,11 +558,11 @@ public class DeploymentSpecWithoutInstanceTest { DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("domain", spec.athenzDomain().get().value()); assertEquals("service", spec.athenzService().get().value()); - assertEquals("prod-service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-central-1")) + assertEquals("prod-service", spec.requireInstance("default").athenzService(prod, RegionName.from("us-central-1")) .get().value()); - assertEquals("prod-service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-west-1")) + assertEquals("prod-service", spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")) .get().value()); - assertEquals("prod-service", spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-east-3")) + assertEquals("prod-service", spec.requireInstance("default").athenzService(prod, RegionName.from("us-east-3")) .get().value()); } @@ -575,9 +580,9 @@ public class DeploymentSpecWithoutInstanceTest { DeploymentSpec spec = DeploymentSpec.fromXml(r); assertEquals("service", spec.athenzService().get().value()); assertEquals(spec.athenzDomain().get().value(), "domain"); - assertEquals(spec.requireInstance("default").athenzService(Environment.test, RegionName.from("us-east-1")).get().value(), "service"); + assertEquals(spec.requireInstance("default").athenzService(test, RegionName.from("us-east-1")).get().value(), "service"); assertEquals(spec.requireInstance("default").athenzService(Environment.staging, RegionName.from("us-north-1")).get().value(), "staging-service"); - assertEquals(spec.requireInstance("default").athenzService(Environment.prod, RegionName.from("us-west-1")).get().value(), "prod-service"); + assertEquals(spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")).get().value(), "prod-service"); } @Test(expected = IllegalArgumentException.class) @@ -694,7 +699,7 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(List.of(RegionName.from("us-east")), spec.requireInstance("default").endpoints().get(0).regions()); - var zone = from(Environment.prod, RegionName.from("us-east")); + var zone = from(prod, RegionName.from("us-east")); assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.from("custom"), zone, ClusterSpec.Id.from("bax"))); assertEquals(ZoneEndpoint.defaultEndpoint, @@ -741,10 +746,10 @@ public class DeploymentSpecWithoutInstanceTest { ); DeploymentSpec spec = DeploymentSpec.fromXml(r); DeploymentInstanceSpec instance = spec.requireInstance("default"); - assertEquals(Optional.of(CloudAccount.from("012345678912")), spec.cloudAccount()); - assertEquals(Optional.of(CloudAccount.from("219876543210")), instance.cloudAccount(Environment.prod, Optional.of(RegionName.from("us-east-1")))); - assertEquals(Optional.of(CloudAccount.from("012345678912")), instance.cloudAccount(Environment.prod, Optional.of(RegionName.from("us-west-1")))); - assertEquals(Optional.of(CloudAccount.from("012345678912")), instance.cloudAccount(Environment.staging, Optional.empty())); + assertEquals(Map.of(AWS, CloudAccount.from("012345678912")), spec.cloudAccounts()); + assertEquals(Map.of(AWS, CloudAccount.from("219876543210")), instance.cloudAccounts(prod, RegionName.from("us-east-1"))); + assertEquals(Map.of(AWS, CloudAccount.from("012345678912")), instance.cloudAccounts(prod, RegionName.from("us-west-1"))); + assertEquals(Map.of(AWS, CloudAccount.from("012345678912")), instance.cloudAccounts(Environment.staging, RegionName.defaultName())); r = new StringReader( "<deployment version='1.0'>" + @@ -755,9 +760,42 @@ public class DeploymentSpecWithoutInstanceTest { "</deployment>" ); spec = DeploymentSpec.fromXml(r); - assertEquals(Optional.empty(), spec.cloudAccount()); - assertEquals(Optional.of(CloudAccount.from("219876543210")), spec.requireInstance("default").cloudAccount(Environment.prod, Optional.of(RegionName.from("us-east-1")))); - assertEquals(Optional.empty(), spec.requireInstance("default").cloudAccount(Environment.prod, Optional.of(RegionName.from("us-west-1")))); + assertEquals(Map.of(), spec.cloudAccounts()); + assertEquals(Map.of(AWS, CloudAccount.from("219876543210")), spec.requireInstance("default").cloudAccounts(prod, RegionName.from("us-east-1"))); + assertEquals(Map.of(), spec.requireInstance("default").cloudAccounts(prod, RegionName.from("us-west-1"))); + } + + @Test + public void productionSpecWithHostTTL() { + String r = """ + <deployment version='1.0' cloud-account='012345678912' empty-host-ttl='1d'> + <prod> + <region empty-host-ttl='1m'>us-east-1</region> + <region>us-west-1</region> + </prod> + </deployment> + """; + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Optional.of(Duration.ofDays(1)), spec.hostTTL()); + DeploymentInstanceSpec instance = spec.requireInstance("default"); + assertEquals(Optional.of(Duration.ofMinutes(1)), instance.hostTTL(prod, Optional.of(RegionName.from("us-east-1")))); + assertEquals(Optional.of(Duration.ofDays(1)), instance.hostTTL(prod, Optional.of(RegionName.from("us-west-1")))); + assertEquals(Optional.of(Duration.ofDays(1)), instance.hostTTL(test, Optional.empty())); + + r = """ + <deployment version='1.0' cloud-account='012345678912'> + <prod empty-host-ttl='1d'> + <region empty-host-ttl='1m'>us-east-1</region> + <region>us-west-1</region> + </prod> + </deployment> + """; + spec = DeploymentSpec.fromXml(r); + assertEquals(Optional.empty(), spec.hostTTL()); + instance = spec.requireInstance("default"); + assertEquals(Optional.of(Duration.ofMinutes(1)), instance.hostTTL(prod, Optional.of(RegionName.from("us-east-1")))); + assertEquals(Optional.of(Duration.ofDays(1)), instance.hostTTL(prod, Optional.of(RegionName.from("us-west-1")))); + assertEquals(Optional.empty(), instance.hostTTL(test, Optional.empty())); } private static Set<String> endpointRegions(String endpointId, DeploymentSpec spec) { |