diff options
Diffstat (limited to 'config-model-api')
5 files changed, 441 insertions, 124 deletions
diff --git a/config-model-api/abi-spec.json b/config-model-api/abi-spec.json index 5004aebc593..315b03c301a 100644 --- a/config-model-api/abi-spec.json +++ b/config-model-api/abi-spec.json @@ -226,7 +226,8 @@ "public void <init>(boolean, boolean, com.yahoo.config.application.api.TimeWindow)", "public boolean blocksRevisions()", "public boolean blocksVersions()", - "public com.yahoo.config.application.api.TimeWindow window()" + "public com.yahoo.config.application.api.TimeWindow window()", + "public java.lang.String toString()" ], "fields": [] }, @@ -264,7 +265,8 @@ "public void <init>(java.time.Duration)", "public java.time.Duration duration()", "public java.time.Duration delay()", - "public boolean deploysTo(com.yahoo.config.provision.Environment, java.util.Optional)" + "public boolean deploysTo(com.yahoo.config.provision.Environment, java.util.Optional)", + "public java.lang.String toString()" ], "fields": [] }, @@ -280,7 +282,8 @@ "public java.util.List steps()", "public boolean deploysTo(com.yahoo.config.provision.Environment, java.util.Optional)", "public boolean equals(java.lang.Object)", - "public int hashCode()" + "public int hashCode()", + "public java.lang.String toString()" ], "fields": [] }, @@ -296,7 +299,8 @@ "public final boolean deploysTo(com.yahoo.config.provision.Environment)", "public abstract boolean deploysTo(com.yahoo.config.provision.Environment, java.util.Optional)", "public java.util.List zones()", - "public java.time.Duration delay()" + "public java.time.Duration delay()", + "public java.util.List steps()" ], "fields": [] }, diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java index 1fc3a8af2eb..df611d66b87 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -46,7 +46,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Step { Notifications notifications, List<Endpoint> endpoints) { this.name = name; - this.steps = List.copyOf(completeSteps(new ArrayList<>(steps))); + this.steps = steps; this.upgradePolicy = upgradePolicy; this.changeBlockers = changeBlockers; this.globalServiceId = globalServiceId; @@ -61,44 +61,6 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Step { public InstanceName name() { return name; } - /** Adds missing required steps and reorders steps to a permissible order */ - private static List<DeploymentSpec.Step> completeSteps(List<DeploymentSpec.Step> steps) { - // Add staging if required and missing - if (steps.stream().anyMatch(step -> step.deploysTo(Environment.prod)) && - steps.stream().noneMatch(step -> step.deploysTo(Environment.staging))) { - steps.add(new DeploymentSpec.DeclaredZone(Environment.staging)); - } - - // Add test if required and missing - if (steps.stream().anyMatch(step -> step.deploysTo(Environment.staging)) && - steps.stream().noneMatch(step -> step.deploysTo(Environment.test))) { - steps.add(new DeploymentSpec.DeclaredZone(Environment.test)); - } - - // Enforce order test, staging, prod - DeploymentSpec.DeclaredZone testStep = remove(Environment.test, steps); - if (testStep != null) - steps.add(0, testStep); - DeploymentSpec.DeclaredZone stagingStep = remove(Environment.staging, steps); - if (stagingStep != null) - steps.add(1, stagingStep); - - return steps; - } - - /** - * Removes the first occurrence of a deployment step to the given environment and returns it. - * - * @return the removed step, or null if it is not present - */ - private static DeploymentSpec.DeclaredZone remove(Environment environment, List<DeploymentSpec.Step> steps) { - for (int i = 0; i < steps.size(); i++) { - if (steps.get(i).deploysTo(environment)) - return (DeploymentSpec.DeclaredZone)steps.remove(i); - } - return null; - } - /** Throw an IllegalArgumentException if any production zone is declared multiple times */ private void validateZones(List<DeploymentSpec.Step> steps) { Set<DeploymentSpec.DeclaredZone> zones = new HashSet<>(); @@ -187,6 +149,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Step { } /** Returns the deployment steps inside this in the order they will be performed */ + @Override public List<DeploymentSpec.Step> steps() { return steps; } /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */ @@ -210,7 +173,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Step { .noneMatch(block -> block.window().includes(instant)); } - /** Returns all the DeclaredZone deployment steps in the order they are declared */ + /** Returns all the deployment steps which are zones in the order they are declared */ public List<DeploymentSpec.DeclaredZone> zones() { return steps.stream() .flatMap(step -> step.zones().stream()) @@ -251,6 +214,18 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Step { return false; } + DeploymentInstanceSpec withSteps(List<DeploymentSpec.Step> steps) { + return new DeploymentInstanceSpec(name, + steps, + upgradePolicy, + changeBlockers, + globalServiceId, + athenzDomain, + athenzService, + notifications, + endpoints); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index db383a1bea5..e604f27a33a 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -13,6 +13,7 @@ import java.io.FileReader; import java.io.Reader; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -52,7 +53,13 @@ public class DeploymentSpec { public DeploymentSpec(List<Step> steps, Optional<Integer> majorVersion, String xmlForm) { - this.steps = steps; + if (singleInstance(steps)) { // TODO: Remove this clause after November 2019 + var singleInstance = (DeploymentInstanceSpec)steps.get(0); + this.steps = List.of(singleInstance.withSteps(completeSteps(singleInstance.steps()))); + } + else { + this.steps = List.copyOf(completeSteps(steps)); + } this.majorVersion = majorVersion; this.xmlForm = xmlForm; validateTotalDelay(steps); @@ -77,6 +84,50 @@ public class DeploymentSpec { xmlForm); } + /** Adds missing required steps and reorders steps to a permissible order */ + private static List<DeploymentSpec.Step> completeSteps(List<DeploymentSpec.Step> inputSteps) { + List<Step> steps = new ArrayList<>(inputSteps); + + // Add staging if required and missing + if (steps.stream().anyMatch(step -> step.deploysTo(Environment.prod)) && + steps.stream().noneMatch(step -> step.deploysTo(Environment.staging))) { + steps.add(new DeploymentSpec.DeclaredZone(Environment.staging)); + } + + // Add test if required and missing + if (steps.stream().anyMatch(step -> step.deploysTo(Environment.staging)) && + steps.stream().noneMatch(step -> step.deploysTo(Environment.test))) { + steps.add(new DeploymentSpec.DeclaredZone(Environment.test)); + } + + // Enforce order test, staging, prod + DeploymentSpec.DeclaredZone testStep = remove(Environment.test, steps); + if (testStep != null) + steps.add(0, testStep); + DeploymentSpec.DeclaredZone stagingStep = remove(Environment.staging, steps); + if (stagingStep != null) + steps.add(1, stagingStep); + + return steps; + } + + /** + * Removes the first occurrence of a deployment step to the given environment and returns it. + * + * @return the removed step, or null if it is not present + */ + private static DeploymentSpec.DeclaredZone remove(Environment environment, List<DeploymentSpec.Step> steps) { + for (int i = 0; i < steps.size(); i++) { + if ( ! (steps.get(i) instanceof DeploymentSpec.DeclaredZone)) continue; + DeploymentSpec.DeclaredZone zoneStep = (DeploymentSpec.DeclaredZone)steps.get(i); + if (zoneStep.environment() == environment) { + steps.remove(i); + return zoneStep; + } + } + return null; + } + /** Throw an IllegalArgumentException if the total delay exceeds 24 hours */ private void validateTotalDelay(List<Step> steps) { long totalDelaySeconds = steps.stream().mapToLong(step -> (step.delay().getSeconds())).sum(); @@ -87,7 +138,7 @@ public class DeploymentSpec { // TODO: Remove after October 2019 private DeploymentInstanceSpec defaultInstance() { - if (instances().size() == 1) return (DeploymentInstanceSpec)steps.get(0); + if (singleInstance(steps)) return (DeploymentInstanceSpec)steps.get(0); throw new IllegalArgumentException("This deployment spec does not support the legacy API " + "as it has multiple instances: " + instances().stream().map(Step::toString).collect(Collectors.joining(","))); @@ -113,7 +164,7 @@ public class DeploymentSpec { /** Returns the deployment steps of this in the order they will be performed */ public List<Step> steps() { - if (steps.size() == 1) return defaultInstance().steps(); // TODO: Remove line after November 2019 + if (singleInstance(steps)) return defaultInstance().steps(); // TODO: Remove line after November 2019 return steps; } @@ -146,6 +197,11 @@ public class DeploymentSpec { return defaultInstance().deploysTo(environment, region); } + // TODO: Remove after November 2019 + private static boolean singleInstance(List<DeploymentSpec.Step> steps) { + return steps.size() == 1 && steps.get(0) instanceof DeploymentInstanceSpec; + } + /** Returns the instance step containing the given instance name, or null if not present */ public DeploymentInstanceSpec instance(String name) { return instance(InstanceName.from(name)); @@ -281,6 +337,9 @@ public class DeploymentSpec { /** The delay introduced by this step (beyond the time it takes to execute the step). Default is zero. */ public Duration delay() { return Duration.ZERO; } + /** Returns all the steps nested in this. This default implementatiino returns an empty list. */ + public List<Step> steps() { return List.of(); } + } /** A deployment step which is to wait for some time before progressing to the next step */ @@ -301,6 +360,11 @@ public class DeploymentSpec { @Override public boolean deploysTo(Environment environment, Optional<RegionName> region) { return false; } + @Override + public String toString() { + return "delay " + duration; + } + } /** A deployment step which is to run deployment in a particular zone */ @@ -400,11 +464,12 @@ public class DeploymentSpec { } /** Returns all the steps nested in this */ + @Override public List<Step> steps() { return steps; } @Override public boolean deploysTo(Environment environment, Optional<RegionName> region) { - return zones().stream().anyMatch(zone -> zone.deploysTo(environment, region)); + return steps().stream().anyMatch(zone -> zone.deploysTo(environment, region)); } @Override @@ -420,6 +485,11 @@ public class DeploymentSpec { return Objects.hash(steps); } + @Override + public String toString() { + return steps.size() + " parallel steps"; + } + } /** Controls when this application will be upgraded to new Vespa versions */ @@ -448,6 +518,11 @@ public class DeploymentSpec { public boolean blocksRevisions() { return revision; } public boolean blocksVersions() { return version; } public TimeWindow window() { return window; } + + @Override + public String toString() { + return "change blocker revision=" + revision + " version=" + version + " window=" + window; + } } diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 4d6495482ea..5395459b9dc 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -44,6 +44,7 @@ public class DeploymentSpecXmlReader { private static final String majorVersionTag = "major-version"; private static final String testTag = "test"; private static final String stagingTag = "staging"; + private static final String upgradeTag = "upgrade"; private static final String blockChangeTag = "block-change"; private static final String prodTag = "prod"; private static final String regionTag = "region"; @@ -51,7 +52,9 @@ public class DeploymentSpecXmlReader { private static final String parallelTag = "parallel"; private static final String endpointsTag = "endpoints"; private static final String endpointTag = "endpoint"; + private static final String notificationsTag = "notifications"; + private static final String idAttribute = "id"; private static final String athenzServiceAttribute = "athenz-service"; private static final String athenzDomainAttribute = "athenz-domain"; private static final String testerFlavorAttribute = "tester-flavor"; @@ -86,18 +89,18 @@ public class DeploymentSpecXmlReader { Element root = XML.getDocument(xmlForm).getDocumentElement(); List<Step> steps = new ArrayList<>(); - if ( ! hasChildTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance - steps.addAll(readInstanceContent("default", root, root)); + if ( ! containsTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance + steps.addAll(readInstanceContent("default", root, new MutableOptional<>(), root)); } else { - if (hasChildTag(prodTag, root)) + if (XML.getChildren(root).stream().anyMatch(child -> child.getTagName().equals(prodTag))) throw new IllegalArgumentException("A deployment spec cannot have both a <prod> tag and an " + "<instance> tag under the root: " + "Wrap the prod tags inside the appropriate instance"); for (Element topLevelTag : XML.getChildren(root)) { if (topLevelTag.getTagName().equals(instanceTag)) - steps.addAll(readInstanceContent(topLevelTag.getAttribute("id"), topLevelTag, root)); + steps.addAll(readInstanceContent(topLevelTag.getAttribute(idAttribute), topLevelTag, new MutableOptional<>(), root)); else steps.addAll(readNonInstanceSteps(topLevelTag, new MutableOptional<>(), topLevelTag)); // (No global service id here) } @@ -116,36 +119,53 @@ public class DeploymentSpecXmlReader { * @param parentTag the parent of instanceTag (or the same, if this instances is implicitly defined which means instanceTag is the root) * @return the instances specified, one for each instance name element */ - private List<DeploymentInstanceSpec> readInstanceContent(String instanceNameString, Element instanceTag, Element parentTag) { + private List<DeploymentInstanceSpec> readInstanceContent(String instanceNameString, + Element instanceTag, + MutableOptional<String> globalServiceId, + Element parentTag) { if (validate) validateTagOrder(instanceTag); - MutableOptional<String> globalServiceId = new MutableOptional<>(); // Deprecated: Set of prod, but belongs to instance + // Values where the parent may provide a default + DeploymentSpec.UpgradePolicy upgradePolicy = readUpgradePolicy(instanceTag, parentTag); + List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceTag, parentTag); Optional<AthenzDomain> athenzDomain = stringAttribute(athenzDomainAttribute, instanceTag) .or(() -> stringAttribute(athenzDomainAttribute, parentTag)) .map(AthenzDomain::from); Optional<AthenzService> athenzService = stringAttribute(athenzServiceAttribute, instanceTag) .or(() -> stringAttribute(athenzServiceAttribute, parentTag)) .map(AthenzService::from); + Notifications notifications = readNotifications(instanceTag, parentTag); + // Values where there is no default List<Step> steps = new ArrayList<>(); for (Element instanceChild : XML.getChildren(instanceTag)) steps.addAll(readNonInstanceSteps(instanceChild, globalServiceId, instanceChild)); + List<Endpoint> endpoints = readEndpoints(instanceTag); + // Build and return instances with these values return Arrays.stream(instanceNameString.split("'")) .map(name -> new DeploymentInstanceSpec(InstanceName.from(name), steps, - readUpgradePolicy(instanceTag), - readChangeBlockers(instanceTag), + upgradePolicy, + changeBlockers, globalServiceId.asOptional(), athenzDomain, athenzService, - readNotifications(instanceTag), - readEndpoints(instanceTag))) + notifications, + endpoints)) .collect(Collectors.toList()); } - // Consume the give tag as 0-N steps. 0 if it is not a step, >1 if it contains multiple nested steps that should be flattened + private List<Step> readSteps(Element stepTag, MutableOptional<String> globalServiceId, Element parentTag) { + if (stepTag.getTagName().equals(instanceTag)) + return new ArrayList<>(readInstanceContent(stepTag.getAttribute(idAttribute), stepTag, globalServiceId, parentTag)); + else + return readNonInstanceSteps(stepTag, globalServiceId, parentTag); + + } + + // Consume the given tag as 0-N steps. 0 if it is not a step, >1 if it contains multiple nested steps that should be flattened private List<Step> readNonInstanceSteps(Element stepTag, MutableOptional<String> globalServiceId, Element parentTag) { Optional<AthenzService> athenzService = stringAttribute(athenzServiceAttribute, stepTag) .or(() -> stringAttribute(athenzServiceAttribute, parentTag)) @@ -171,8 +191,8 @@ public class DeploymentSpecXmlReader { longAttribute("seconds", stepTag)))); case parallelTag: // regions and instances may be nested within return List.of(new ParallelZones(XML.getChildren(stepTag).stream() - .flatMap(child -> readNonInstanceSteps(child, globalServiceId, stepTag).stream()) - .collect(Collectors.toList()))); + .flatMap(child -> readSteps(child, globalServiceId, stepTag).stream()) + .collect(Collectors.toList()))); case regionTag: return List.of(readDeclaredZone(Environment.prod, athenzService, testerFlavor, stepTag)); default: @@ -180,12 +200,18 @@ public class DeploymentSpecXmlReader { } } - private boolean hasChildTag(String childTagName, Element parent) { - return XML.getChildren(parent).stream().anyMatch(child -> child.getTagName().equals(childTagName)); + private boolean containsTag(String childTagName, Element parent) { + for (Element child : XML.getChildren(parent)) { + if (child.getTagName().equals(childTagName) || containsTag(childTagName, child)) + return true; + } + return false; } - private Notifications readNotifications(Element root) { - Element notificationsElement = XML.getChild(root, "notifications"); + private Notifications readNotifications(Element parent, Element fallbackParent) { + Element notificationsElement = XML.getChild(parent, notificationsTag); + if (notificationsElement == null) + notificationsElement = XML.getChild(fallbackParent, notificationsTag); if (notificationsElement == null) return Notifications.none(); @@ -210,9 +236,10 @@ public class DeploymentSpecXmlReader { return Notifications.of(emailAddresses, emailRoles); } - private List<Endpoint> readEndpoints(Element root) { - var endpointsElement = XML.getChild(root, endpointsTag); - if (endpointsElement == null) { return Collections.emptyList(); } + private List<Endpoint> readEndpoints(Element parent) { + var endpointsElement = XML.getChild(parent, endpointsTag); + if (endpointsElement == null) + return Collections.emptyList(); var endpoints = new LinkedHashMap<String, Endpoint>(); @@ -315,44 +342,44 @@ public class DeploymentSpecXmlReader { private Optional<String> readGlobalServiceId(Element environmentTag) { String globalServiceId = environmentTag.getAttribute("global-service-id"); - if (globalServiceId == null || globalServiceId.isEmpty()) { - return Optional.empty(); - } - else { - return Optional.of(globalServiceId); - } + if (globalServiceId == null || globalServiceId.isEmpty()) return Optional.empty(); + return Optional.of(globalServiceId); } - private List<DeploymentSpec.ChangeBlocker> readChangeBlockers(Element root) { + private List<DeploymentSpec.ChangeBlocker> readChangeBlockers(Element parent, Element globalBlockersParent) { List<DeploymentSpec.ChangeBlocker> changeBlockers = new ArrayList<>(); - for (Element tag : XML.getChildren(root)) { - if (!blockChangeTag.equals(tag.getTagName())) continue; - - boolean blockVersions = trueOrMissing(tag.getAttribute("version")); - boolean blockRevisions = trueOrMissing(tag.getAttribute("revision")); - - String daySpec = tag.getAttribute("days"); - String hourSpec = tag.getAttribute("hours"); - String zoneSpec = tag.getAttribute("time-zone"); - if (zoneSpec.isEmpty()) { // Default to UTC time zone - zoneSpec = "UTC"; - } - changeBlockers.add(new DeploymentSpec.ChangeBlocker(blockRevisions, blockVersions, - TimeWindow.from(daySpec, hourSpec, zoneSpec))); + if (globalBlockersParent != parent) { + for (Element tag : XML.getChildren(globalBlockersParent, blockChangeTag)) + changeBlockers.add(readChangeBlocker(tag)); } + for (Element tag : XML.getChildren(parent, blockChangeTag)) + changeBlockers.add(readChangeBlocker(tag)); return Collections.unmodifiableList(changeBlockers); } - /** - * Returns true if the given value is "true", or if it is missing - */ + private DeploymentSpec.ChangeBlocker readChangeBlocker(Element tag) { + boolean blockVersions = trueOrMissing(tag.getAttribute("version")); + boolean blockRevisions = trueOrMissing(tag.getAttribute("revision")); + + String daySpec = tag.getAttribute("days"); + String hourSpec = tag.getAttribute("hours"); + String zoneSpec = tag.getAttribute("time-zone"); + if (zoneSpec.isEmpty()) zoneSpec = "UTC"; // default + return new DeploymentSpec.ChangeBlocker(blockRevisions, blockVersions, + TimeWindow.from(daySpec, hourSpec, zoneSpec)); + } + + /** Returns true if the given value is "true", or if it is missing */ private boolean trueOrMissing(String value) { return value == null || value.isEmpty() || value.equals("true"); } - private DeploymentSpec.UpgradePolicy readUpgradePolicy(Element root) { - Element upgradeElement = XML.getChild(root, "upgrade"); - if (upgradeElement == null) return DeploymentSpec.UpgradePolicy.defaultPolicy; + private DeploymentSpec.UpgradePolicy readUpgradePolicy(Element parent, Element fallbackParent) { + Element upgradeElement = XML.getChild(parent, upgradeTag); + if (upgradeElement == null) + upgradeElement = XML.getChild(fallbackParent, upgradeTag); + if (upgradeElement == null) + return DeploymentSpec.UpgradePolicy.defaultPolicy; String policy = upgradeElement.getAttribute("policy"); switch (policy) { diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 4923ea396a0..6497ef074a8 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import static com.yahoo.config.application.api.Notifications.Role.author; import static com.yahoo.config.application.api.Notifications.When.failing; @@ -127,43 +126,79 @@ public class DeploymentSpecTest { @Test public void maximalProductionSpec() { StringReader r = new StringReader( - "<deployment version='1.0'>" + - " <instance id='default'>" + - " <test/>" + - " <staging/>" + - " <prod>" + - " <region active='false'>us-east1</region>" + - " <delay hours='3' minutes='30'/>" + - " <region active='true'>us-west1</region>" + - " </prod>" + - " </instance>" + - "</deployment>" + "<deployment version='1.0'>" + + " <instance id='default'>" + // The block checked by assertCorrectFirstInstance + " <test/>" + + " <staging/>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <delay hours='3' minutes='30'/>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" ); DeploymentSpec spec = DeploymentSpec.fromXml(r); - assertEquals(5, spec.instance("default").steps().size()); - assertEquals(4, spec.instance("default").zones().size()); + assertCorrectFirstInstance(spec.instance("default")); + } - assertTrue(spec.instance("default").steps().get(0).deploysTo(Environment.test)); + @Test + public void maximalProductionSpecMultipleInstances() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <instance id='instance1'>" + // The block checked by assertCorrectFirstInstance + " <test/>" + + " <staging/>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <delay hours='3' minutes='30'/>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + " <instance id='instance2'>" + + " <prod>" + + " <region active='true'>us-central1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); - assertTrue(spec.instance("default").steps().get(1).deploysTo(Environment.staging)); + DeploymentSpec spec = DeploymentSpec.fromXml(r); - assertTrue(spec.instance("default").steps().get(2).deploysTo(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertFalse(((DeploymentSpec.DeclaredZone)spec.instance("default").steps().get(2)).active()); + assertCorrectFirstInstance(spec.instance("instance1")); - assertTrue(spec.instance("default").steps().get(3) instanceof DeploymentSpec.Delay); - assertEquals(3 * 60 * 60 + 30 * 60, spec.instance("default").steps().get(3).delay().getSeconds()); + DeploymentInstanceSpec instance2 = spec.instance("instance2"); + assertEquals(1, instance2.steps().size()); + assertEquals(1, instance2.zones().size()); - assertTrue(spec.instance("default").steps().get(4).deploysTo(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertTrue(((DeploymentSpec.DeclaredZone)spec.instance("default").steps().get(4)).active()); + assertTrue(instance2.steps().get(0).deploysTo(Environment.prod, Optional.of(RegionName.from("us-central1")))); + } - assertTrue(spec.instance("default").includes(Environment.test, Optional.empty())); - assertFalse(spec.instance("default").includes(Environment.test, Optional.of(RegionName.from("region1")))); - assertTrue(spec.instance("default").includes(Environment.staging, Optional.empty())); - assertTrue(spec.instance("default").includes(Environment.prod, Optional.of(RegionName.from("us-east1")))); - assertTrue(spec.instance("default").includes(Environment.prod, Optional.of(RegionName.from("us-west1")))); - assertFalse(spec.instance("default").includes(Environment.prod, Optional.of(RegionName.from("no-such-region")))); - assertFalse(spec.instance("default").globalServiceId().isPresent()); + private void assertCorrectFirstInstance(DeploymentInstanceSpec instance) { + assertEquals(5, instance.steps().size()); + assertEquals(4, instance.zones().size()); + + assertTrue(instance.steps().get(0).deploysTo(Environment.test)); + + assertTrue(instance.steps().get(1).deploysTo(Environment.staging)); + + assertTrue(instance.steps().get(2).deploysTo(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertFalse(((DeploymentSpec.DeclaredZone)instance.steps().get(2)).active()); + + assertTrue(instance.steps().get(3) instanceof DeploymentSpec.Delay); + assertEquals(3 * 60 * 60 + 30 * 60, instance.steps().get(3).delay().getSeconds()); + + assertTrue(instance.steps().get(4).deploysTo(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertTrue(((DeploymentSpec.DeclaredZone)instance.steps().get(4)).active()); + + assertTrue(instance.includes(Environment.test, Optional.empty())); + assertFalse(instance.includes(Environment.test, Optional.of(RegionName.from("region1")))); + assertTrue(instance.includes(Environment.staging, Optional.empty())); + assertTrue(instance.includes(Environment.prod, Optional.of(RegionName.from("us-east1")))); + assertTrue(instance.includes(Environment.prod, Optional.of(RegionName.from("us-west1")))); + assertFalse(instance.includes(Environment.prod, Optional.of(RegionName.from("no-such-region")))); + assertFalse(instance.globalServiceId().isPresent()); } @Test @@ -247,6 +282,24 @@ public class DeploymentSpecTest { } @Test + public void upgradePolicyDefault() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <upgrade policy='canary'/>" + + " <instance id='instance1'>" + + " <upgrade policy='conservative'/>" + + " </instance>" + + " <instance id='instance2'>" + + " </instance>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals("conservative", spec.instance("instance1").upgradePolicy().toString()); + assertEquals("canary", spec.instance("instance2").upgradePolicy().toString()); + } + + @Test public void maxDelayExceeded() { try { StringReader r = new StringReader( @@ -303,6 +356,105 @@ public class DeploymentSpecTest { } @Test + public void testTestAndStagingOutsideAndInsideInstance() { + StringReader r = new StringReader( + "<deployment>" + + " <test/>" + + " <staging/>" + + " <instance id='instance0'>" + + " <prod>" + + " <region active='true'>us-west-1</region>" + + " </prod>" + + " </instance>" + + " <instance id='instance1'>" + + " <test/>" + + " <staging/>" + + " <prod>" + + " <region active='true'>us-west-1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + List<DeploymentSpec.Step> 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<DeploymentSpec.Step> instance0Steps = ((DeploymentInstanceSpec)steps.get(2)).steps(); + assertEquals(1, instance0Steps.size()); + assertEquals("prod.us-west-1", instance0Steps.get(0).toString()); + + List<DeploymentSpec.Step> 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 testParallelInstances() { + StringReader r = new StringReader( + "<deployment>" + + " <parallel>" + + " <instance id='instance0'>" + + " <prod>" + + " <region active='true'>us-west-1</region>" + + " </prod>" + + " </instance>" + + " <instance id='instance1'>" + + " <prod>" + + " <region active='true'>us-east-3</region>" + + " </prod>" + + " </instance>" + + " </parallel>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + List<DeploymentSpec.Step> steps = spec.steps(); + assertEquals(3, steps.size()); + assertEquals("test", steps.get(0).toString()); + assertEquals("staging", steps.get(1).toString()); + assertEquals("2 parallel steps", steps.get(2).toString()); + + List<DeploymentSpec.Step> parallelSteps = steps.get(2).steps(); + assertEquals("instance 'instance0'", parallelSteps.get(0).toString()); + assertEquals("instance 'instance1'", parallelSteps.get(1).toString()); + } + + @Test + public void testInstancesWithDelay() { + StringReader r = new StringReader( + "<deployment>" + + " <instance id='instance0'>" + + " <prod>" + + " <region active='true'>us-west-1</region>" + + " </prod>" + + " </instance>" + + " <delay hours='12'/>" + + " <instance id='instance1'>" + + " <prod>" + + " <region active='true'>us-east-3</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + List<DeploymentSpec.Step> steps = spec.steps(); + assertEquals(5, steps.size()); + assertEquals("test", steps.get(0).toString()); + assertEquals("staging", steps.get(1).toString()); + assertEquals("instance 'instance0'", steps.get(2).toString()); + assertEquals("delay PT12H", steps.get(3).toString()); + assertEquals("instance 'instance1'", steps.get(4).toString()); + } + + @Test public void productionSpecWithDuplicateRegions() { StringReader r = new StringReader( "<deployment>" + @@ -392,6 +544,32 @@ public class DeploymentSpecTest { } @Test + public void testChangeBlockerInheritance() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <block-change revision='false' days='mon,tue' hours='15-16'/>" + + " <instance id='instance1'>" + + " <block-change days='sat' hours='10' time-zone='CET'/>" + + " </instance>" + + " <instance id='instance2'>" + + " </instance>" + + "</deployment>" + ); + + 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 UTC"; + + assertEquals(2, spec.instance("instance1").changeBlocker().size()); + assertEquals(inheritedChangeBlocker, spec.instance("instance1").changeBlocker().get(0).toString()); + assertEquals("change blocker revision=true version=true window=time window for hour(s) [10] on [saturday] in CET", + spec.instance("instance1").changeBlocker().get(1).toString()); + + assertEquals(1, spec.instance("instance2").changeBlocker().size()); + assertEquals(inheritedChangeBlocker, spec.instance("instance2").changeBlocker().get(0).toString()); + } + + @Test public void athenz_config_is_read_from_deployment() { StringReader r = new StringReader( "<deployment athenz-domain='domain' athenz-service='service'>" + @@ -504,6 +682,64 @@ public class DeploymentSpecTest { } @Test + public void notificationsWithMultipleInstances() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <instance id='instance1'>" + + " <notifications when=\"failing\">" + + " <email role=\"author\"/>" + + " <email address=\"john@operator\"/>" + + " </notifications>" + + " </instance>" + + " <instance id='instance2'>" + + " <notifications when=\"failing-commit\">" + + " <email role=\"author\"/>" + + " <email address=\"mary@dev\"/>" + + " </notifications>" + + " </instance>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + DeploymentInstanceSpec instance1 = spec.instance("instance1"); + assertEquals(Set.of(author), instance1.notifications().emailRolesFor(failing)); + assertEquals(Set.of("john@operator"), instance1.notifications().emailAddressesFor(failing)); + + DeploymentInstanceSpec instance2 = spec.instance("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( + "<deployment version='1.0'>" + + " <notifications when=\"failing-commit\">" + + " <email role=\"author\"/>" + + " <email address=\"mary@dev\"/>" + + " </notifications>" + + " <instance id='instance1'>" + + " <notifications when=\"failing\">" + + " <email role=\"author\"/>" + + " <email address=\"john@operator\"/>" + + " </notifications>" + + " </instance>" + + " <instance id='instance2'>" + + " </instance>" + + "</deployment>" + ); + + DeploymentSpec spec = DeploymentSpec.fromXml(r); + DeploymentInstanceSpec instance1 = spec.instance("instance1"); + assertEquals(Set.of(author), instance1.notifications().emailRolesFor(failing)); + assertEquals(Set.of("john@operator"), instance1.notifications().emailAddressesFor(failing)); + + DeploymentInstanceSpec instance2 = spec.instance("instance2"); + 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("<deployment>" + " <instance id='default'>" + |