// 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 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(); } }