// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.application.api;
import com.google.common.collect.ImmutableSet;
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;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Tags;
import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import com.yahoo.test.ManualClock;
import org.junit.Test;
import java.io.StringReader;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
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;
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;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* @author bratseth
*/
public class DeploymentSpecTest {
@Test
public void simpleSpec() {
String specXml = "" +
" " +
" " +
" " +
"";
StringReader r = new StringReader(specXml);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(specXml, spec.xmlForm());
assertEquals(1, spec.requireInstance("default").steps().size());
assertFalse(spec.majorVersion().isPresent());
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()));
}
@Test
public void specPinningMajorVersion() {
String specXml = "" +
" " +
" " +
" " +
"";
StringReader r = new StringReader(specXml);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(specXml, spec.xmlForm());
assertEquals(1, spec.requireInstance("default").steps().size());
assertTrue(spec.majorVersion().isPresent());
assertEquals(6, (int)spec.majorVersion().get());
}
@Test
public void stagingSpec() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
""
);
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(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()));
}
@Test
public void minimalProductionSpec() {
StringReader r = new StringReader( """
us-east1
us-west1
""");
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(1, spec.steps().size());
assertEquals(2, spec.requireInstance("default").steps().size());
assertTrue(spec.requireInstance("default").steps().get(0).concerns(prod, Optional.of(RegionName.from("us-east1"))));
assertTrue(spec.requireInstance("default").steps().get(1).concerns(prod, Optional.of(RegionName.from("us-west1"))));
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"))));
assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, spec.requireInstance("default").upgradePolicy());
assertEquals(DeploymentSpec.RevisionTarget.latest, spec.requireInstance("default").revisionTarget());
assertEquals(DeploymentSpec.RevisionChange.whenFailing, spec.requireInstance("default").revisionChange());
assertEquals(DeploymentSpec.UpgradeRollout.separate, spec.requireInstance("default").upgradeRollout());
assertEquals(0, spec.requireInstance("default").minRisk());
assertEquals(0, spec.requireInstance("default").maxRisk());
assertEquals(8, spec.requireInstance("default").maxIdleHours());
}
@Test
public void specWithTags() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-east1" +
" us-west1" +
" " +
" " +
" " +
" " +
" us-east1" +
" us-west1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(Tags.fromString("tag1 tag2"), spec.requireInstance("a").tags());
assertEquals(Tags.fromString("tag3"), spec.requireInstance("b").tags());
}
@Test
public void maximalProductionSpec() {
StringReader r = new StringReader(
"" +
" " + // The block checked by assertCorrectFirstInstance
" " +
" " +
" " +
" us-east1" +
" " +
" us-west1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertCorrectFirstInstance(spec.requireInstance("default"));
}
@Test
public void productionTests() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" us-east-1" +
" us-west-1" +
" " +
" us-west-1" +
" us-east-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
List instanceSteps = spec.steps().get(0).steps();
assertEquals(7, instanceSteps.size());
assertEquals("test", instanceSteps.get(0).toString());
assertEquals("staging", instanceSteps.get(1).toString());
assertEquals("prod.us-east-1", instanceSteps.get(2).toString());
assertEquals("prod.us-west-1", instanceSteps.get(3).toString());
assertEquals("delay PT1H", instanceSteps.get(4).toString());
assertEquals("tests for prod.us-west-1", instanceSteps.get(5).toString());
assertEquals("tests for prod.us-east-1", instanceSteps.get(6).toString());
}
@Test(expected = IllegalArgumentException.class)
public void duplicateProductionTest() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-east1" +
" us-east1" +
" us-east1" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void productionTestBeforeDeployment() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-east1" +
" us-east1" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void productionTestInParallelWithDeployment() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" us-east1" +
" us-east1" +
" " +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test
public void maximalProductionSpecMultipleInstances() {
StringReader r = new StringReader(
"" +
" " + // The block checked by assertCorrectFirstInstance
" " +
" " +
" " +
" us-east1" +
" " +
" us-west1" +
" " +
" " +
" " +
" " +
" us-central1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertCorrectFirstInstance(spec.requireInstance("instance1"));
DeploymentInstanceSpec instance2 = spec.requireInstance("instance2");
assertEquals(1, instance2.steps().size());
assertEquals(1, instance2.zones().size());
assertTrue(instance2.steps().get(0).concerns(prod, Optional.of(RegionName.from("us-central1"))));
}
@Test
public void multipleInstancesShortForm() {
StringReader r = new StringReader(
"" +
" " + // The block checked by assertCorrectFirstInstance
" " +
" " +
" " +
" us-east1" +
" " +
" us-west1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertCorrectFirstInstance(spec.requireInstance("instance1"));
assertCorrectFirstInstance(spec.requireInstance("instance2"));
}
private void assertCorrectFirstInstance(DeploymentInstanceSpec instance) {
assertEquals(5, instance.steps().size());
assertEquals(4, instance.zones().size());
assertTrue(instance.steps().get(0).concerns(test));
assertTrue(instance.steps().get(1).concerns(staging));
assertTrue(instance.steps().get(2).concerns(prod, Optional.of(RegionName.from("us-east1"))));
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(prod, Optional.of(RegionName.from("us-west1"))));
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"))));
}
@Test
public void productionSpecWithUpgradeRevisionSettings() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("next", spec.requireInstance("default").revisionTarget().toString());
assertEquals("latest", spec.requireInstance("custom").revisionTarget().toString());
assertEquals("whenClear", spec.requireInstance("default").revisionChange().toString());
assertEquals("always", spec.requireInstance("custom").revisionChange().toString());
assertEquals(3, spec.requireInstance("default").minRisk());
assertEquals(12, spec.requireInstance("default").maxRisk());
assertEquals(32, spec.requireInstance("default").maxIdleHours());
}
@Test
public void productionSpecsWithIllegalRevisionSettings() {
assertEquals("revision-change must be 'when-clear' when max-risk is specified, but got: 'always'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("" +
" " +
" " +
" " +
""))
.getMessage());
assertEquals("revision-target must be 'next' when max-risk is specified, but got: 'latest'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("" +
" " +
" " +
" " +
""))
.getMessage());
assertEquals("maximum risk cannot be less than minimum risk score, but got: '12'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("" +
" " +
" " +
" " +
""))
.getMessage());
assertEquals("maximum risk cannot be less than minimum risk score, but got: '0'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("" +
" " +
" " +
" " +
""))
.getMessage());
}
@Test
public void productionSpecWithUpgradeRollout() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("leading", spec.requireInstance("default").upgradeRollout().toString());
assertEquals("separate", spec.requireInstance("custom").upgradeRollout().toString());
assertEquals("simultaneous", spec.requireInstance("aggressive").upgradeRollout().toString());
}
@Test
public void productionSpecWithUpgradePolicy() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("canary", spec.requireInstance("default").upgradePolicy().toString());
assertEquals("defaultPolicy", spec.requireInstance("custom").upgradePolicy().toString());
}
@Test
public void upgradePolicyDefault() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("canary", spec.requireInstance("instance1").upgradePolicy().toString());
assertEquals("conservative", spec.requireInstance("instance2").upgradePolicy().toString());
assertEquals("next", spec.requireInstance("instance1").revisionTarget().toString());
assertEquals("latest", spec.requireInstance("instance2").revisionTarget().toString());
assertEquals("whenClear", spec.requireInstance("instance1").revisionChange().toString());
assertEquals("whenFailing", spec.requireInstance("instance2").revisionChange().toString());
assertEquals("leading", spec.requireInstance("instance1").upgradeRollout().toString());
assertEquals("separate", spec.requireInstance("instance2").upgradeRollout().toString());
}
@Test
public void maxDelayExceeded() {
try {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" us-west-1" +
" " +
" us-central-1" +
" " +
" us-east-3" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
fail("Expected exception due to exceeding the max total delay");
}
catch (IllegalArgumentException e) {
// success
assertEquals("The total delay specified is PT48H1S but max 48 hours is allowed", e.getMessage());
}
}
@Test
public void onlyAthenzServiceDefinedInInstance() {
StringReader r = new StringReader(
"" +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("domain", spec.athenzDomain().get().value());
assertEquals(1, spec.instances().size());
DeploymentInstanceSpec instance = spec.instances().get(0);
assertEquals("default", instance.name().value());
assertEquals("service", instance.athenzService(prod, RegionName.defaultName()).get().value());
}
@Test
public void productionSpecWithParallelDeployments() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" us-central-1" +
" us-east-3" +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
DeploymentSpec.ParallelSteps parallelSteps = ((DeploymentSpec.ParallelSteps) spec.requireInstance("default").steps().get(1));
assertEquals(2, parallelSteps.zones().size());
assertEquals(RegionName.from("us-central-1"), parallelSteps.zones().get(0).region().get());
assertEquals(RegionName.from("us-east-3"), parallelSteps.zones().get(1).region().get());
}
@Test
public void testAndStagingOutsideAndInsideInstance() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
List steps = spec.steps();
assertEquals(4, steps.size());
assertEquals("test", steps.get(0).toString());
assertEquals("staging", steps.get(1).toString());
assertEquals("instance 'instance0'", steps.get(2).toString());
assertEquals("instance 'instance1'", steps.get(3).toString());
List instance0Steps = ((DeploymentInstanceSpec)steps.get(2)).steps();
assertEquals(1, instance0Steps.size());
assertEquals("prod.us-west-1", instance0Steps.get(0).toString());
List instance1Steps = ((DeploymentInstanceSpec)steps.get(3)).steps();
assertEquals(3, instance1Steps.size());
assertEquals("test", instance1Steps.get(0).toString());
assertEquals("staging", instance1Steps.get(1).toString());
assertEquals("prod.us-west-1", instance1Steps.get(2).toString());
}
@Test
public void nestedParallelAndSteps() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" us-east-3" +
" " +
" eu-west-1" +
" " +
" " +
" " +
" " +
" aws-us-east-1a" +
" " +
" ap-northeast-1" +
" ap-southeast-2" +
" aws-us-east-1a" +
" " +
" " +
" " +
" " +
" us-north-7" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
List steps = spec.steps();
assertEquals(2, steps.size());
assertEquals("staging", steps.get(0).toString());
assertEquals("instance 'instance'", steps.get(1).toString());
assertEquals(Duration.ofHours(4), steps.get(1).delay());
List instanceSteps = steps.get(1).steps();
assertEquals(2, instanceSteps.size());
assertEquals("4 parallel steps", instanceSteps.get(0).toString());
assertEquals("prod.us-north-7", instanceSteps.get(1).toString());
List parallelSteps = instanceSteps.get(0).steps();
assertEquals(4, parallelSteps.size());
assertEquals("prod.us-west-1", parallelSteps.get(0).toString());
assertEquals("4 steps", parallelSteps.get(1).toString());
assertEquals("3 steps", parallelSteps.get(2).toString());
assertEquals("delay PT3H30M", parallelSteps.get(3).toString());
List firstSerialSteps = parallelSteps.get(1).steps();
assertEquals(4, firstSerialSteps.size());
assertEquals("prod.us-east-3", firstSerialSteps.get(0).toString());
assertEquals("delay PT2H", firstSerialSteps.get(1).toString());
assertEquals("prod.eu-west-1", firstSerialSteps.get(2).toString());
assertEquals("delay PT2H", firstSerialSteps.get(3).toString());
List secondSerialSteps = parallelSteps.get(2).steps();
assertEquals(3, secondSerialSteps.size());
assertEquals("delay PT3H", secondSerialSteps.get(0).toString());
assertEquals("prod.aws-us-east-1a", secondSerialSteps.get(1).toString());
assertEquals("3 parallel steps", secondSerialSteps.get(2).toString());
List 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(prod, RegionName.from("ap-northeast-1")).get().value());
assertEquals("prod.ap-southeast-2", innerParallelSteps.get(1).toString());
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());
}
@Test
public void parallelInstances() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
" " +
" " +
" us-east-3" +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
List steps = spec.steps();
assertEquals(1, steps.size());
assertEquals("2 parallel steps", steps.get(0).toString());
List parallelSteps = steps.get(0).steps();
assertEquals("instance 'instance0'", parallelSteps.get(0).toString());
assertEquals("instance 'instance1'", parallelSteps.get(1).toString());
}
@Test
public void instancesWithDelay() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" " +
" " +
" " +
" " +
" us-east-3" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
List steps = spec.steps();
assertEquals(3, steps.size());
assertEquals("instance 'instance0'", steps.get(0).toString());
assertEquals("delay PT12H", steps.get(1).toString());
assertEquals("instance 'instance1'", steps.get(2).toString());
}
@Test
public void productionSpecWithDuplicateRegions() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" us-west-1" +
" us-central-1" +
" us-east-3" +
" " +
" " +
" " +
""
);
try {
DeploymentSpec.fromXml(r);
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("prod.us-west-1 is listed twice in deployment.xml", e.getMessage());
}
}
@Test(expected = IllegalArgumentException.class)
public void deploymentSpecWithIncreasinglyStrictUpgradePolicies() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void deploymentSpecWithIncreasinglyStrictUpgradePoliciesInParallel() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void deploymentSpecWithIncreasinglyStrictUpgradePoliciesAfterParallel() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test
public void deploymentSpecWithDifferentUpgradePoliciesInParallel() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(DeploymentSpec.UpgradePolicy.conservative, spec.requireInstance("instance1").upgradePolicy());
assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, spec.requireInstance("instance2").upgradePolicy());
}
@Test(expected = IllegalArgumentException.class)
public void deploymentSpecWithIllegallyOrderedDeploymentSpec1() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void deploymentSpecWithIllegallyOrderedDeploymentSpec2() {
StringReader r = new StringReader(
"\n" +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test
public void deploymentSpecWithChangeBlocker() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals(3, spec.requireInstance("default").changeBlocker().size());
assertTrue(spec.requireInstance("default").changeBlocker().get(0).blocksVersions());
assertFalse(spec.requireInstance("default").changeBlocker().get(0).blocksRevisions());
assertEquals(ZoneId.of("UTC"), spec.requireInstance("default").changeBlocker().get(0).window().zone());
assertTrue(spec.requireInstance("default").changeBlocker().get(1).blocksVersions());
assertTrue(spec.requireInstance("default").changeBlocker().get(1).blocksRevisions());
assertEquals(ZoneId.of("CET"), spec.requireInstance("default").changeBlocker().get(1).window().zone());
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-18T14:15:30.00Z")));
assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-18T15:15:30.00Z")));
assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-18T16:15:30.00Z")));
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-18T17:15:30.00Z")));
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T09:15:30.00Z")));
assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T08:15:30.00Z"))); // 10 in CET
assertTrue(spec.requireInstance("default").canUpgradeAt(Instant.parse("2017-09-23T10:15:30.00Z")));
assertFalse(spec.requireInstance("default").canUpgradeAt(Instant.parse("2022-01-15T16:00:00.00Z")));
}
@Test
public void changeBlockerInheritance() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
String inheritedChangeBlocker = "change blocker revision=false version=true window=time window for hour(s) " +
"[15, 16] on [monday, tuesday] in time zone UTC and date range [any date, any date]";
assertEquals(2, spec.requireInstance("instance1").changeBlocker().size());
assertEquals(inheritedChangeBlocker, spec.requireInstance("instance1").changeBlocker().get(0).toString());
assertEquals("change blocker revision=true version=true window=time window for hour(s) [10] on " +
"[saturday] in time zone CET and date range [any date, any date]",
spec.requireInstance("instance1").changeBlocker().get(1).toString());
assertEquals(1, spec.requireInstance("instance2").changeBlocker().size());
assertEquals(inheritedChangeBlocker, spec.requireInstance("instance2").changeBlocker().get(0).toString());
}
@Test
public void athenzConfigIsReadFromDeployment() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("domain", spec.athenzDomain().get().value());
assertEquals("service", spec.athenzService().get().value());
assertEquals("service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-west-1")).get().value());
}
@Test
public void athenzConfigPropagatesThroughParallelZones() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-central-1" +
" " +
" us-west-1" +
" us-east-3" +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("domain", spec.athenzDomain().get().value());
assertEquals("service", spec.athenzService().get().value());
assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-central-1")).get().value());
assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-west-1")).get().value());
assertEquals("prod-service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-east-3")).get().value());
}
@Test
public void athenzConfigPropagatesThroughParallelZonesAndInstances() {
String r =
"""
us-west-1
us-east-3
us-west-1
us-east-3
""";
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("domain", spec.athenzDomain().get().value());
assertEquals("service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-west-1")).get().value());
assertEquals("service", spec.requireInstance("instance1").athenzService(prod,
RegionName.from("us-east-3")).get().value());
assertEquals("service", spec.requireInstance("instance2").athenzService(prod,
RegionName.from("us-east-3")).get().value());
}
@Test
public void athenzConfigIsReadFromInstance() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("domain", spec.athenzDomain().get().value());
assertEquals(Optional.empty(), spec.athenzService());
assertEquals("service", spec.requireInstance("default").athenzService(prod, RegionName.from("us-west-1")).get().value());
}
@Test
public void athenzServiceIsOverriddenFromEnvironment() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
assertEquals("service",
spec.requireInstance("default").athenzService(test,
RegionName.from("us-east-1")).get().value());
assertEquals("staging-service",
spec.requireInstance("default").athenzService(staging,
RegionName.from("us-north-1")).get().value());
assertEquals("prod-service",
spec.requireInstance("default").athenzService(prod,
RegionName.from("us-west-1")).get().value());
}
@Test(expected = IllegalArgumentException.class)
public void missingAthenzServiceFails() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test(expected = IllegalArgumentException.class)
public void athenzServiceWithoutDomainFails() {
StringReader r = new StringReader(
"" +
" " +
" " +
" us-west-1" +
" " +
" " +
""
);
DeploymentSpec.fromXml(r);
}
@Test
public void noNotifications() {
assertEquals(Notifications.none(),
DeploymentSpec.fromXml("" +
" " +
"").requireInstance("default").notifications());
}
@Test
public void emptyNotifications() {
DeploymentSpec spec = DeploymentSpec.fromXml("" +
" " +
" " +
" " +
"");
assertEquals(Notifications.none(), spec.requireInstance("default").notifications());
}
@Test
public void someNotifications() {
DeploymentSpec spec = DeploymentSpec.fromXml("\n" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
"");
assertEquals(ImmutableSet.of(author), spec.requireInstance("default").notifications().emailRolesFor(failing));
assertEquals(ImmutableSet.of(author), spec.requireInstance("default").notifications().emailRolesFor(failingCommit));
assertEquals(ImmutableSet.of("john@dev", "jane@dev"), spec.requireInstance("default").notifications().emailAddressesFor(failingCommit));
assertEquals(ImmutableSet.of("jane@dev"), spec.requireInstance("default").notifications().emailAddressesFor(failing));
}
@Test
public void notificationsWithMultipleInstances() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
DeploymentInstanceSpec instance1 = spec.requireInstance("instance1");
assertEquals(Set.of(author), instance1.notifications().emailRolesFor(failing));
assertEquals(Set.of("john@operator"), instance1.notifications().emailAddressesFor(failing));
DeploymentInstanceSpec instance2 = spec.requireInstance("instance2");
assertEquals(Set.of(author), instance2.notifications().emailRolesFor(failingCommit));
assertEquals(Set.of("mary@dev"), instance2.notifications().emailAddressesFor(failingCommit));
}
@Test
public void notificationsDefault() {
StringReader r = new StringReader(
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
""
);
DeploymentSpec spec = DeploymentSpec.fromXml(r);
DeploymentInstanceSpec instance1 = spec.requireInstance("instance1");
assertEquals(Set.of(author), instance1.notifications().emailRolesFor(failing));
assertEquals(Set.of(), instance1.notifications().emailAddressesFor(failing));
assertEquals(Set.of(author), instance1.notifications().emailRolesFor(failingCommit));
assertEquals(Set.of("john@operator"), instance1.notifications().emailAddressesFor(failingCommit));
DeploymentInstanceSpec instance2 = spec.requireInstance("instance2");
assertEquals(Set.of(author), instance2.notifications().emailRolesFor(failing));
assertEquals(Set.of(), instance2.notifications().emailAddressesFor(failing));
assertEquals(Set.of(author), instance2.notifications().emailRolesFor(failingCommit));
assertEquals(Set.of("mary@dev"), instance2.notifications().emailAddressesFor(failingCommit));
}
@Test
public void customTesterFlavor() {
DeploymentSpec spec = DeploymentSpec.fromXml("""
us-north-7
""");
assertEquals(Optional.of("d-1-4-20"), spec.requireInstance("default").steps().get(0).zones().get(0).testerFlavor());
assertEquals(Optional.empty(), spec.requireInstance("default").steps().get(1).zones().get(0).testerFlavor());
assertEquals(Optional.of("d-2-8-50"), spec.requireInstance("default").steps().get(2).zones().get(0).testerFlavor());
}
@Test
public void noEndpoints() {
DeploymentSpec spec = DeploymentSpec.fromXml("""
""");
assertEquals(Collections.emptyList(), spec.requireInstance("default").endpoints());
assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.defaultName(),
defaultId(),
ClusterSpec.Id.from("cluster")));
assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.defaultName(),
com.yahoo.config.provision.zone.ZoneId.from("test", "us"),
ClusterSpec.Id.from("cluster")));
}
@Test
public void emptyEndpoints() {
var spec = DeploymentSpec.fromXml("""
""");
assertEquals(List.of(), spec.requireInstance("default").endpoints());
assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.defaultName(),
defaultId(),
ClusterSpec.Id.from("cluster")));
}
@Test
public void someEndpoints() {
var spec = DeploymentSpec.fromXml("""
us-east
us-east
us-east
""");
assertEquals(
List.of("foo", "nalle", "default"),
spec.requireInstance("default").endpoints().stream().map(Endpoint::endpointId).toList()
);
assertEquals(
List.of("bar", "frosk", "quux"),
spec.requireInstance("default").endpoints().stream().map(Endpoint::containerId).toList()
);
assertEquals(List.of(RegionName.from("us-east")), spec.requireInstance("default").endpoints().get(0).regions());
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,
spec.zoneEndpoint(InstanceName.from("default"), defaultId(), ClusterSpec.Id.from("bax")));
assertEquals(ZoneEndpoint.defaultEndpoint,
spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("bax")));
assertEquals(ZoneEndpoint.defaultEndpoint,
spec.zoneEndpoint(InstanceName.from("default"), testZone, ClusterSpec.Id.from("bax")));
assertEquals(ZoneEndpoint.privateEndpoint,
spec.zoneEndpoint(InstanceName.from("default"), testZone, ClusterSpec.Id.from("froz")));
assertEquals(new ZoneEndpoint(false, true, List.of(new AllowedUrn(AccessType.awsPrivateLink, "barn"),
new AllowedUrn(AccessType.gcpServiceConnect, "nine"))),
spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("froz")));
}
@Test
public void invalidEndpoints() {
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'FOO'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got '123'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo!'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo.bar'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo--bar'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo-'");
assertInvalidEndpoints("",
"Endpoint id must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foooooooooooo'");
assertInvalidEndpoints("",
"Endpoint id 'foo' is specified multiple times");
assertInvalidEndpoints("",
"Instance-level endpoint 'default': cannot declare 'id' with type 'zone' or 'private'");
assertInvalidEndpoints("",
"Instance-level endpoint 'default': cannot declare 'id' with type 'zone' or 'private'");
assertInvalidEndpoints("",
"Missing required attribute 'container-id' in 'endpoint'");
assertInvalidEndpoints("",
"Missing required attribute 'container-id' in 'endpoint'");
assertInvalidEndpoints("",
"Instance-level endpoint 'default': only endpoints of type 'private' can specify 'allow' children");
assertInvalidEndpoints("",
"Instance-level endpoint 'default': only endpoints of type 'zone' can specify 'enabled'");
assertInvalidEndpoints("",
"Multiple zone endpoints (for all regions) declared for container id 'qrs'");
assertInvalidEndpoints("us" +
"us",
"Multiple private endpoints declared for container id 'qrs' in region 'us'");
assertInvalidEndpoints("" +
"us",
"Zone endpoint for container id 'qrs' declared both with region 'us', and for all regions.");
}
@Test
public void validEndpoints() {
assertEquals(List.of("default"), endpointIds(""));
assertEquals(List.of("default"), endpointIds(""));
assertEquals(List.of("f"), endpointIds(""));
assertEquals(List.of("foo"), endpointIds(""));
assertEquals(List.of("foo-bar"), endpointIds(""));
assertEquals(List.of("foo", "bar"), endpointIds(""));
assertEquals(List.of("fooooooooooo"), endpointIds(""));
}
@Test
public void endpointDefaultRegions() {
var spec = DeploymentSpec.fromXml("""
us-east
us-west
us-east
us-east
""");
assertEquals(Set.of("us-east"), endpointRegions("foo", spec));
assertEquals(Set.of("us-east", "us-west"), endpointRegions("nalle", spec));
assertEquals(Set.of("us-east", "us-west"), endpointRegions("default", spec));
assertEquals(new ZoneEndpoint(true, true, List.of()),
spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-east"), ClusterSpec.Id.from("bar")));
assertEquals(new ZoneEndpoint(true, false, List.of()),
spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-west"), ClusterSpec.Id.from("bar")));
assertEquals(new ZoneEndpoint(true, true, List.of()),
spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-east"), ClusterSpec.Id.from("quux")));
assertEquals(new ZoneEndpoint(true, true, List.of()),
spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-west"), ClusterSpec.Id.from("quux")));
assertEquals(new HashSet<>() {{ add(null); add(from("prod", "us-east")); }},
spec.requireInstance("default").zoneEndpoints().get(ClusterSpec.Id.from("bar")).keySet());
assertEquals(new HashSet<>() {{ add(null); }},
spec.requireInstance("default").zoneEndpoints().get(ClusterSpec.Id.from("quux")).keySet());
assertEquals(Set.of(ClusterSpec.Id.from("bar"), ClusterSpec.Id.from("quux")),
spec.requireInstance("default").zoneEndpoints().keySet());
}
@Test
public void instanceEndpointDisallowsRegionAttributeOrInstanceTag() {
String xmlForm = """
us-east
us-west
%s
""";
assertInvalid(String.format(xmlForm, "id='foo' region='us-east'", "us-east"), "Instance-level endpoint 'foo': invalid 'region' attribute");
assertInvalid(String.format(xmlForm, "id='foo'", "us-east"), "Instance-level endpoint 'foo': invalid element 'instance'");
assertInvalid(String.format(xmlForm, "type='zone'", "us-east"), "Instance-level endpoint 'default': invalid element 'instance'");
assertInvalid(String.format(xmlForm, "type='private'", "us-east"), "Instance-level endpoint 'default': invalid element 'instance'");
}
@Test
public void applicationLevelEndpointValidation() {
String xmlForm = """
us-west-1
us-east-3
us-west-1
us-east-3
%s
%s
""";
assertInvalid(String.format(xmlForm, "", "weight='1'", "", "main", ""), "'region' attribute must be declared on either or tag");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='1'", "region='us-west-1'", "main", ""), "'region' attribute must be declared on either or tag");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "", "", "main", ""), "Missing required attribute 'weight' in 'instance");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='1'", "", "", ""), "Application-level endpoint 'foo': empty 'instance' element");
assertInvalid(String.format(xmlForm, "region='invalid'", "weight='1'", "", "main", ""), "Application-level endpoint 'foo': targets undeclared region 'invalid' in instance 'main'");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='foo'", "", "main", ""), "Application-level endpoint 'foo': invalid weight value 'foo'");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='1'", "", "main", "us-east-3"), "Application-level endpoint 'foo': invalid element 'region'");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='0'", "", "main", ""), "Application-level endpoint 'foo': sum of all weights must be positive, got 0");
assertInvalid(String.format(xmlForm, "type='zone'", "weight='1'", "", "main", ""), "Endpoints at application level cannot be of type 'zone'");
assertInvalid(String.format(xmlForm, "type='private'", "weight='1'", "", "main", ""), "Endpoints at application level cannot be of type 'private'");
}
@Test
public void cannotTargetDisabledEndpoints() {
assertEquals("Instance-level endpoint 'default': all eligible zone endpoints have 'enabled' set to 'false'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("""
us
eu
"""))
.getMessage());
assertEquals("Instance-level endpoint 'default': targets zone endpoint in 'us' with 'enabled' set to 'false'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("""
us
eu
us
"""))
.getMessage());
assertEquals("Application-level endpoint 'default': targets 'us' in 'default', but its zone endpoint has 'enabled' set to 'false'",
assertThrows(IllegalArgumentException.class,
() -> DeploymentSpec.fromXml("""
us
eu
us
default
"""))
.getMessage());
}
@Test
public void applicationLevelEndpoint() {
DeploymentSpec spec = DeploymentSpec.fromXml("""
us-west-1
us-east-3
us-west-1
us-east-3
beta
main
main
main
main
beta
""");
assertEquals(List.of(new Endpoint("foo", "movies", Level.application,
List.of(new Target(RegionName.from("us-west-1"), InstanceName.from("beta"), 2),
new Target(RegionName.from("us-west-1"), InstanceName.from("main"), 8))),
new Endpoint("bar", "music", Level.application,
List.of(new Target(RegionName.from("us-east-3"), InstanceName.from("main"), 10))),
new Endpoint("baz", "moose", Level.application,
List.of(new Target(RegionName.from("us-west-1"), InstanceName.from("main"), 1),
new Target(RegionName.from("us-east-3"), InstanceName.from("main"), 2),
new Target(RegionName.from("us-west-1"), InstanceName.from("beta"), 3)))),
spec.endpoints());
assertEquals(List.of(new Endpoint("glob", "music", Level.instance,
List.of(new Target(RegionName.from("us-west-1"), InstanceName.from("main"), 1),
new Target(RegionName.from("us-east-3"), InstanceName.from("main"), 1)))),
spec.requireInstance("main").endpoints());
}
@Test
public void disallowExcessiveUpgradeBlocking() {
List specs = List.of(
"""
""",
"""
""",
"""
""",
// Convoluted example of blocking too long
"""
"""
);
ManualClock clock = new ManualClock();
clock.setInstant(Instant.parse("2022-01-05T15:00:00.00Z"));
for (var spec : specs) {
assertInvalid(spec, "Cannot block Vespa upgrades for longer than 21 consecutive days", clock);
}
}
@Test
public void testDeployableHash() {
assertEquals(DeploymentSpec.fromXml("""
""").deployableHashCode(),
DeploymentSpec.fromXml("""
""").deployableHashCode());
assertEquals(DeploymentSpec.fromXml("""
name
""").deployableHashCode(),
DeploymentSpec.fromXml("""
name
name
""").deployableHashCode());
String referenceSpec = """
name
""";
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
other
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(),
DeploymentSpec.fromXml("""
name
""").deployableHashCode());
}
@Test
public void cloudAccount() {
String r =
"""
us-east-1
us-west-1
us-west-2
us-west-3
us-east-1
eu-west-1
""";
DeploymentSpec spec = DeploymentSpec.fromXml(r);
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 =
"""
us-east
us-west
us-east
us-west
us-east
us-west
us-east
""";
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) {
assertInvalid(deploymentSpec, errorMessagePart, new ManualClock());
}
private static void assertInvalid(String deploymentSpec, String errorMessagePart, Clock clock) {
if (errorMessagePart.isEmpty()) throw new IllegalArgumentException("Message part must be non-empty");
try {
new DeploymentSpecXmlReader(true, clock).read(deploymentSpec);
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertTrue("\"" + e.getMessage() + "\" contains \"" + errorMessagePart + "\"",
e.getMessage().contains(errorMessagePart));
}
}
private static void assertInvalidEndpoints(String endpointsBody, String error) {
assertEquals(error,
assertThrows(IllegalArgumentException.class,
() -> endpointIds(endpointsBody))
.getMessage());
}
private static Set endpointRegions(String endpointId, DeploymentSpec spec) {
return spec.requireInstance("default").endpoints().stream()
.filter(endpoint -> endpoint.endpointId().equals(endpointId))
.flatMap(endpoint -> endpoint.regions().stream())
.map(RegionName::value)
.collect(Collectors.toSet());
}
private static List endpointIds(String endpointsBody) {
var xml = "" +
" " +
" " +
" us-east" +
" " +
" " +
endpointsBody +
" " +
" " +
"";
return DeploymentSpec.fromXml(xml).requireInstance("default").endpoints().stream()
.map(Endpoint::endpointId)
.toList();
}
}