summaryrefslogtreecommitdiffstats
path: root/config-model-api/src
diff options
context:
space:
mode:
Diffstat (limited to 'config-model-api/src')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/Bcp.java27
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java43
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java78
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java64
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java1
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java213
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java110
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) {