summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-09-18 10:41:49 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-10-03 11:28:47 +0200
commit997f8e32804cbd0f856ef01a8a4f32fb997b32c4 (patch)
treee424d480ef86e4c338ec77d48dac1a13bf54bc84
parent2d1544accbb6711b426583ee74139d25264d0b74 (diff)
Extract instance settings
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java131
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java146
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecDeprecatedAPITest.java572
3 files changed, 774 insertions, 75 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java
index 819a39af3ad..13e7ba91049 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstancesSpec.java
@@ -4,8 +4,11 @@ package com.yahoo.config.application.api;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -19,30 +22,32 @@ import java.util.stream.Collectors;
*
* @author bratseth
*/
-public class DeploymentInstancesSpec {
+public class DeploymentInstancesSpec extends DeploymentSpec.Step {
- private final Optional<String> globalServiceId;
+ /** The instances deployed in this step */
+ private final List<InstanceName> names;
+
+ private final List<DeploymentSpec.Step> steps;
private final DeploymentSpec.UpgradePolicy upgradePolicy;
- private final Optional<Integer> majorVersion;
private final List<DeploymentSpec.ChangeBlocker> changeBlockers;
- private final List<DeploymentSpec.Step> steps;
+ private final Optional<String> globalServiceId;
private final Optional<AthenzDomain> athenzDomain;
private final Optional<AthenzService> athenzService;
private final List<Endpoint> endpoints;
- public DeploymentInstancesSpec(Optional<String> globalServiceId,
+ public DeploymentInstancesSpec(List<InstanceName> names,
+ List<DeploymentSpec.Step> steps,
DeploymentSpec.UpgradePolicy upgradePolicy,
- Optional<Integer> majorVersion,
List<DeploymentSpec.ChangeBlocker> changeBlockers,
- List<DeploymentSpec.Step> steps,
+ Optional<String> globalServiceId,
Optional<AthenzDomain> athenzDomain,
Optional<AthenzService> athenzService,
List<Endpoint> endpoints) {
- this.globalServiceId = globalServiceId;
+ this.names = List.copyOf(names);
+ this.steps = List.copyOf(completeSteps(new ArrayList<>(steps)));
this.upgradePolicy = upgradePolicy;
- this.majorVersion = majorVersion;
this.changeBlockers = changeBlockers;
- this.steps = List.copyOf(completeSteps(new ArrayList<>(steps)));
+ this.globalServiceId = globalServiceId;
this.athenzDomain = athenzDomain;
this.athenzService = athenzService;
this.endpoints = List.copyOf(validateEndpoints(endpoints, this.steps));
@@ -51,6 +56,8 @@ public class DeploymentInstancesSpec {
validateAthenz();
}
+ public List<InstanceName> names() { return names; }
+
/** 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
@@ -128,6 +135,26 @@ public class DeploymentInstancesSpec {
return List.copyOf(rebuiltEndpointsList);
}
+ /** Throw an IllegalArgumentException if an endpoint refers to a region that is not declared in 'prod' */
+ private void validateEndpoints(List<DeploymentSpec.Step> steps, Optional<String> globalServiceId, List<Endpoint> endpoints) {
+ if (globalServiceId.isPresent() && ! endpoints.isEmpty()) {
+ throw new IllegalArgumentException("Providing both 'endpoints' and 'global-service-id'. Use only 'endpoints'.");
+ }
+
+ var stepZones = steps.stream()
+ .flatMap(s -> s.zones().stream())
+ .flatMap(z -> z.region().stream())
+ .collect(Collectors.toSet());
+
+ for (var endpoint : endpoints){
+ for (var endpointRegion : endpoint.regions()) {
+ if (! stepZones.contains(endpointRegion)) {
+ throw new IllegalArgumentException("Region used in endpoint that is not declared in 'prod': " + endpointRegion);
+ }
+ }
+ }
+ }
+
/**
* Throw an IllegalArgumentException if Athenz configuration violates:
* domain not configured -> no zone can configure service
@@ -151,4 +178,88 @@ public class DeploymentInstancesSpec {
}
}
+ @Override
+ public Duration delay() {
+ return Duration.ofSeconds(steps.stream().mapToLong(step -> (step.delay().getSeconds())).sum());
+ }
+
+ /** Returns the deployment steps inside this in the order they will be performed */
+ public List<DeploymentSpec.Step> steps() { return steps; }
+
+ /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
+ public DeploymentSpec.UpgradePolicy upgradePolicy() { return upgradePolicy; }
+
+ /** Returns time windows where upgrades are disallowed for these instances */
+ public List<DeploymentSpec.ChangeBlocker> changeBlocker() { return changeBlockers; }
+
+ /** Returns the ID of the service to expose through global routing, if present */
+ public Optional<String> globalServiceId() { return globalServiceId; }
+
+ /** Returns whether the instances in this step can upgrade at the given instant */
+ public boolean canUpgradeAt(Instant instant) {
+ return changeBlockers.stream().filter(block -> block.blocksVersions())
+ .noneMatch(block -> block.window().includes(instant));
+ }
+
+ /** Returns whether an application revision change for these instances can occur at the given instant */
+ public boolean canChangeRevisionAt(Instant instant) {
+ return changeBlockers.stream().filter(block -> block.blocksRevisions())
+ .noneMatch(block -> block.window().includes(instant));
+ }
+
+ /** Returns all the DeclaredZone deployment steps in the order they are declared */
+ public List<DeploymentSpec.DeclaredZone> zones() {
+ return steps.stream()
+ .flatMap(step -> step.zones().stream())
+ .collect(Collectors.toList());
+ }
+
+ /** Returns whether this deployment spec specifies the given zone, either implicitly or explicitly */
+ @Override
+ public boolean deploysTo(Environment environment, Optional<RegionName> region) {
+ for (DeploymentSpec.Step step : steps)
+ if (step.deploysTo(environment, region)) return true;
+ return false;
+ }
+
+ /** Returns the rotations configuration of these instances */
+ public List<Endpoint> endpoints() { return endpoints; }
+
+ /** Returns the athenz domain if configured */
+ public Optional<AthenzDomain> athenzDomain() { return athenzDomain; }
+
+ /** Returns the athenz service for environment/region if configured */
+ public Optional<AthenzService> athenzService(Environment environment, RegionName region) {
+ AthenzService athenzService = zones().stream()
+ .filter(zone -> zone.deploysTo(environment, Optional.of(region)))
+ .findFirst()
+ .flatMap(DeploymentSpec.DeclaredZone::athenzService)
+ .orElse(this.athenzService.orElse(null));
+ return Optional.ofNullable(athenzService);
+ }
+ /** Returns whether this instances deployment specifies the given zone, either implicitly or explicitly */
+ public boolean includes(Environment environment, Optional<RegionName> region) {
+ for (DeploymentSpec.Step step : steps)
+ if (step.deploysTo(environment, region)) return true;
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DeploymentInstancesSpec other = (DeploymentInstancesSpec) o;
+ return globalServiceId.equals(other.globalServiceId) &&
+ upgradePolicy == other.upgradePolicy &&
+ changeBlockers.equals(other.changeBlockers) &&
+ steps.equals(other.steps) &&
+ athenzDomain.equals(other.athenzDomain) &&
+ athenzService.equals(other.athenzService);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(globalServiceId, upgradePolicy, changeBlockers, steps, athenzDomain, athenzService);
+ }
+
}
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java
index 4a8d5c1c212..7a554cbb749 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
@@ -5,6 +5,7 @@ import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
import com.yahoo.config.provision.AthenzDomain;
import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import java.io.BufferedReader;
@@ -47,91 +48,119 @@ public class DeploymentSpec {
Notifications.none(),
List.of());
- private final List<DeploymentInstancesSpec> instances;
+ private final List<Step> steps;
private final Notifications notifications;
+ private final Optional<Integer> majorVersion;
private final String xmlForm;
- public DeploymentSpec(List<DeploymentInstancesSpec> instances, Notifications notifications, String xmlForm) {
- this.instances = instances;
+ public DeploymentSpec(List<Step> steps,
+ Notifications notifications,
+ Optional<Integer> majorVersion,
+ String xmlForm) {
+ this.steps = steps;
this.notifications = notifications;
+ this.majorVersion = majorVersion;
this.xmlForm = xmlForm;
validateTotalDelay(steps);
}
// TODO: Remove after October 2019
- @Deprecated
public DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy, Optional<Integer> majorVersion,
List<ChangeBlocker> changeBlockers, List<Step> steps, String xmlForm,
Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService,
Notifications notifications,
List<Endpoint> endpoints) {
- this(List.of(new DeploymentInstancesSpec("default", globalServiceId, upgradePolicy, majorVersion, changeBlockers, steps, athenzDomain, athenzService, endpoints, xmlForm)),
+ this(List.of(new DeploymentInstancesSpec(List.of(InstanceName.from("default")),
+ steps,
+ upgradePolicy,
+ changeBlockers,
+ globalServiceId,
+ athenzDomain,
+ athenzService,
+ endpoints)),
notifications,
+ majorVersion,
xmlForm);
}
/** Throw an IllegalArgumentException if the total delay exceeds 24 hours */
private void validateTotalDelay(List<Step> steps) {
- long totalDelaySeconds = steps.stream().filter(step -> step instanceof Delay)
- .mapToLong(delay -> ((Delay)delay).duration().getSeconds())
- .sum();
+ long totalDelaySeconds = steps.stream().mapToLong(step -> (step.delay().getSeconds())).sum();
if (totalDelaySeconds > Duration.ofHours(24).getSeconds())
throw new IllegalArgumentException("The total delay specified is " + Duration.ofSeconds(totalDelaySeconds) +
" but max 24 hours is allowed");
}
- /** Returns the ID of the service to expose through global routing, if present */
- public Optional<String> globalServiceId() {
- return globalServiceId;
+ // TODO: Remove after October 2019
+ private DeploymentInstancesSpec defaultInstance() {
+ if (hasDefaultInstanceStepOnly()) return (DeploymentInstancesSpec)steps.get(0);
+ throw new IllegalArgumentException("This deployment spec does not support the legacy API " +
+ "as it does not consist only of a default instance");
+ }
+
+ // TODO: Remove after October 2019
+ private boolean hasDefaultInstanceStepOnly() {
+ return steps.size() == 1
+ && (steps.get(0) instanceof DeploymentInstancesSpec)
+ && ((DeploymentInstancesSpec)steps.get(0)).names().equals(List.of(InstanceName.from("default")));
}
- /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
- public UpgradePolicy upgradePolicy() { return upgradePolicy; }
+ // TODO: Remove after October 2019
+ public Optional<String> globalServiceId() { return defaultInstance().globalServiceId(); }
+
+ // TODO: Remove after October 2019
+ public UpgradePolicy upgradePolicy() { return defaultInstance().upgradePolicy(); }
/** Returns the major version this application is pinned to, or empty (default) to allow all major versions */
public Optional<Integer> majorVersion() { return majorVersion; }
- /** Returns whether upgrade can occur at the given instant */
- public boolean canUpgradeAt(Instant instant) {
- return changeBlockers.stream().filter(block -> block.blocksVersions())
- .noneMatch(block -> block.window().includes(instant));
- }
+ // TODO: Remove after October 2019
+ public boolean canUpgradeAt(Instant instant) { return defaultInstance().canUpgradeAt(instant); }
- /** Returns whether an application revision change can occur at the given instant */
- public boolean canChangeRevisionAt(Instant instant) {
- return changeBlockers.stream().filter(block -> block.blocksRevisions())
- .noneMatch(block -> block.window().includes(instant));
- }
+ // TODO: Remove after October 2019
+ public boolean canChangeRevisionAt(Instant instant) { return defaultInstance().canChangeRevisionAt(instant); }
- /** Returns time windows where upgrades are disallowed */
- public List<ChangeBlocker> changeBlocker() { return changeBlockers; }
+ // TODO: Remove after October 2019
+ public List<ChangeBlocker> changeBlocker() { return defaultInstance().changeBlocker(); }
/** Returns the deployment steps of this in the order they will be performed */
- public List<Step> steps() { return steps; }
+ public List<Step> steps() {
+ if (hasDefaultInstanceStepOnly()) return defaultInstance().steps(); // TODO: Remove line after October 2019
+ return steps;
+ }
- /** Returns all the DeclaredZone deployment steps in the order they are declared */
+ // TODO: Remove after October 2019
public List<DeclaredZone> zones() {
- return steps.stream()
- .flatMap(step -> step.zones().stream())
- .collect(Collectors.toList());
+ return defaultInstance().steps().stream()
+ .flatMap(step -> step.zones().stream())
+ .collect(Collectors.toList());
}
/** Returns the notification configuration */
public Notifications notifications() { return notifications; }
/** Returns the rotations configuration */
- public List<Endpoint> endpoints() { return endpoints; }
+ // TODO: Remove after October 2019
+ public List<Endpoint> endpoints() { return defaultInstance().endpoints(); }
- /** Returns the XML form of this spec, or null if it was not created by fromXml, nor is empty */
- public String xmlForm() { return xmlForm; }
+ /** Returns the athenz domain if configured */
+ // TODO: Remove after October 2019
+ public Optional<AthenzDomain> athenzDomain() { return defaultInstance().athenzDomain(); }
- /** Returns whether this deployment spec specifies the given zone, either implicitly or explicitly */
+ /** Returns the athenz service for environment/region if configured */
+ // TODO: Remove after October 2019
+ public Optional<AthenzService> athenzService(Environment environment, RegionName region) {
+ return defaultInstance().athenzService(environment, region);
+ }
+
+ // TODO: Remove after October 2019
public boolean includes(Environment environment, Optional<RegionName> region) {
- for (Step step : steps)
- if (step.deploysTo(environment, region)) return true;
- return false;
+ return defaultInstance().deploysTo(environment, region);
}
+ /** Returns the XML form of this spec, or null if it was not created by fromXml, nor is empty */
+ public String xmlForm() { return xmlForm; }
+
/**
* Creates a deployment spec from XML.
*
@@ -176,40 +205,20 @@ public class DeploymentSpec {
return b.toString();
}
- /** Returns the athenz domain if configured */
- public Optional<AthenzDomain> athenzDomain() {
- return athenzDomain;
- }
-
- /** Returns the athenz service for environment/region if configured */
- public Optional<AthenzService> athenzService(Environment environment, RegionName region) {
- AthenzService athenzService = zones().stream()
- .filter(zone -> zone.deploysTo(environment, Optional.of(region)))
- .findFirst()
- .flatMap(DeclaredZone::athenzService)
- .orElse(this.athenzService.orElse(null));
- return Optional.ofNullable(athenzService);
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- DeploymentSpec that = (DeploymentSpec) o;
- return globalServiceId.equals(that.globalServiceId) &&
- upgradePolicy == that.upgradePolicy &&
- majorVersion.equals(that.majorVersion) &&
- changeBlockers.equals(that.changeBlockers) &&
- steps.equals(that.steps) &&
- xmlForm.equals(that.xmlForm) &&
- athenzDomain.equals(that.athenzDomain) &&
- athenzService.equals(that.athenzService) &&
- notifications.equals(that.notifications);
+ DeploymentSpec other = (DeploymentSpec) o;
+ return majorVersion.equals(other.majorVersion) &&
+ steps.equals(other.steps) &&
+ xmlForm.equals(other.xmlForm) &&
+ notifications.equals(other.notifications);
}
@Override
public int hashCode() {
- return Objects.hash(globalServiceId, upgradePolicy, majorVersion, changeBlockers, steps, xmlForm, athenzDomain, athenzService, notifications);
+ return Objects.hash(majorVersion, steps, xmlForm, notifications);
}
/** This may be invoked by a continuous build */
@@ -237,7 +246,7 @@ public class DeploymentSpec {
/** A deployment step */
public abstract static class Step {
-
+
/** Returns whether this step deploys to the given region */
public final boolean deploysTo(Environment environment) {
return deploysTo(environment, Optional.empty());
@@ -249,6 +258,9 @@ public class DeploymentSpec {
/** Returns the zones deployed to in this step */
public List<DeclaredZone> zones() { return Collections.emptyList(); }
+ /** The delay introduced by this step (beyond the time it takes to execute the step). Default is zero. */
+ public Duration delay() { return Duration.ZERO; }
+
}
/** A deployment step which is to wait for some time before progressing to the next step */
@@ -259,10 +271,14 @@ public class DeploymentSpec {
public Delay(Duration duration) {
this.duration = duration;
}
-
+
+ // TODO: Remove after October 2019
public Duration duration() { return duration; }
@Override
+ public Duration delay() { return duration; }
+
+ @Override
public boolean deploysTo(Environment environment, Optional<RegionName> region) { return false; }
}
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecDeprecatedAPITest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecDeprecatedAPITest.java
new file mode 100644
index 00000000000..dabdd0c4a69
--- /dev/null
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecDeprecatedAPITest.java
@@ -0,0 +1,572 @@
+// Copyright 2017 Yahoo Holdings. 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.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
+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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+// TODO: Remove after October 2019
+public class DeploymentSpecDeprecatedAPITest {
+
+ @Test
+ public void testSpec() {
+ String specXml = "<deployment version='1.0'>" +
+ " <test/>" +
+ "</deployment>";
+
+ StringReader r = new StringReader(specXml);
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(specXml, spec.xmlForm());
+ assertEquals(1, spec.steps().size());
+ assertFalse(spec.majorVersion().isPresent());
+ assertTrue(spec.steps().get(0).deploysTo(Environment.test));
+ assertTrue(spec.includes(Environment.test, Optional.empty()));
+ assertFalse(spec.includes(Environment.test, Optional.of(RegionName.from("region1"))));
+ assertFalse(spec.includes(Environment.staging, Optional.empty()));
+ assertFalse(spec.includes(Environment.prod, Optional.empty()));
+ assertFalse(spec.globalServiceId().isPresent());
+ }
+
+ @Test
+ public void testSpecPinningMajorVersion() {
+ String specXml = "<deployment version='1.0' major-version='6'>" +
+ " <test/>" +
+ "</deployment>";
+
+ StringReader r = new StringReader(specXml);
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(specXml, spec.xmlForm());
+ assertEquals(1, spec.steps().size());
+ assertTrue(spec.majorVersion().isPresent());
+ assertEquals(6, (int)spec.majorVersion().get());
+ }
+
+ @Test
+ public void stagingSpec() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <staging/>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(2, spec.steps().size());
+ assertTrue(spec.steps().get(0).deploysTo(Environment.test));
+ assertTrue(spec.steps().get(1).deploysTo(Environment.staging));
+ assertTrue(spec.includes(Environment.test, Optional.empty()));
+ assertFalse(spec.includes(Environment.test, Optional.of(RegionName.from("region1"))));
+ assertTrue(spec.includes(Environment.staging, Optional.empty()));
+ assertFalse(spec.includes(Environment.prod, Optional.empty()));
+ assertFalse(spec.globalServiceId().isPresent());
+ }
+
+ @Test
+ public void minimalProductionSpec() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <prod>" +
+ " <region active='false'>us-east1</region>" +
+ " <region active='true'>us-west1</region>" +
+ " </prod>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(4, spec.steps().size());
+
+ assertTrue(spec.steps().get(0).deploysTo(Environment.test));
+
+ assertTrue(spec.steps().get(1).deploysTo(Environment.staging));
+
+ assertTrue(spec.steps().get(2).deploysTo(Environment.prod, Optional.of(RegionName.from("us-east1"))));
+ assertFalse(((DeploymentSpec.DeclaredZone)spec.steps().get(2)).active());
+
+ assertTrue(spec.steps().get(3).deploysTo(Environment.prod, Optional.of(RegionName.from("us-west1"))));
+ assertTrue(((DeploymentSpec.DeclaredZone)spec.steps().get(3)).active());
+
+ assertTrue(spec.includes(Environment.test, Optional.empty()));
+ assertFalse(spec.includes(Environment.test, Optional.of(RegionName.from("region1"))));
+ assertTrue(spec.includes(Environment.staging, Optional.empty()));
+ assertTrue(spec.includes(Environment.prod, Optional.of(RegionName.from("us-east1"))));
+ assertTrue(spec.includes(Environment.prod, Optional.of(RegionName.from("us-west1"))));
+ assertFalse(spec.includes(Environment.prod, Optional.of(RegionName.from("no-such-region"))));
+ assertFalse(spec.globalServiceId().isPresent());
+
+ assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, spec.upgradePolicy());
+ }
+
+ @Test
+ public void maximalProductionSpec() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <test/>" +
+ " <staging/>" +
+ " <prod>" +
+ " <region active='false'>us-east1</region>" +
+ " <delay hours='3' minutes='30'/>" +
+ " <region active='true'>us-west1</region>" +
+ " </prod>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(5, spec.steps().size());
+ assertEquals(4, spec.zones().size());
+
+ assertTrue(spec.steps().get(0).deploysTo(Environment.test));
+
+ assertTrue(spec.steps().get(1).deploysTo(Environment.staging));
+
+ assertTrue(spec.steps().get(2).deploysTo(Environment.prod, Optional.of(RegionName.from("us-east1"))));
+ assertFalse(((DeploymentSpec.DeclaredZone)spec.steps().get(2)).active());
+
+ assertTrue(spec.steps().get(3) instanceof DeploymentSpec.Delay);
+ assertEquals(3 * 60 * 60 + 30 * 60, ((DeploymentSpec.Delay)spec.steps().get(3)).duration().getSeconds());
+
+ assertTrue(spec.steps().get(4).deploysTo(Environment.prod, Optional.of(RegionName.from("us-west1"))));
+ assertTrue(((DeploymentSpec.DeclaredZone)spec.steps().get(4)).active());
+
+ assertTrue(spec.includes(Environment.test, Optional.empty()));
+ assertFalse(spec.includes(Environment.test, Optional.of(RegionName.from("region1"))));
+ assertTrue(spec.includes(Environment.staging, Optional.empty()));
+ assertTrue(spec.includes(Environment.prod, Optional.of(RegionName.from("us-east1"))));
+ assertTrue(spec.includes(Environment.prod, Optional.of(RegionName.from("us-west1"))));
+ assertFalse(spec.includes(Environment.prod, Optional.of(RegionName.from("no-such-region"))));
+ assertFalse(spec.globalServiceId().isPresent());
+ }
+
+ @Test
+ public void productionSpecWithGlobalServiceId() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <prod global-service-id='query'>" +
+ " <region active='true'>us-east-1</region>" +
+ " <region active='true'>us-west-1</region>" +
+ " </prod>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(spec.globalServiceId(), Optional.of("query"));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void globalServiceIdInTest() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <test global-service-id='query' />" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void globalServiceIdInStaging() {
+ StringReader r = new StringReader(
+ "<deployment version='1.0'>" +
+ " <staging global-service-id='query' />" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test
+ public void productionSpecWithGlobalServiceIdBeforeStaging() {
+ StringReader r = new StringReader(
+ "<deployment>" +
+ " <test/>" +
+ " <prod global-service-id='qrs'>" +
+ " <region active='true'>us-west-1</region>" +
+ " <region active='true'>us-central-1</region>" +
+ " <region active='true'>us-east-3</region>" +
+ " </prod>" +
+ " <staging/>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals("qrs", spec.globalServiceId().get());
+ }
+
+ @Test
+ public void productionSpecWithUpgradePolicy() {
+ StringReader r = new StringReader(
+ "<deployment>" +
+ " <upgrade policy='canary'/>" +
+ " <prod>" +
+ " <region active='true'>us-west-1</region>" +
+ " <region active='true'>us-central-1</region>" +
+ " <region active='true'>us-east-3</region>" +
+ " </prod>" +
+ "</deployment>"
+ );
+
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals("canary", spec.upgradePolicy().toString());
+ }
+
+ @Test
+ public void maxDelayExceeded() {
+ try {
+ StringReader r = new StringReader(
+ "<deployment>" +
+ " <upgrade policy='canary'/>" +
+ " <prod>" +
+ " <region active='true'>us-west-1</region>" +
+ " <delay hours='23'/>" +
+ " <region active='true'>us-central-1</region>" +
+ " <delay minutes='59' seconds='61'/>" +
+ " <region active='true'>us-east-3</region>" +
+ " </prod>" +
+ "</deployment>"
+ );
+ DeploymentSpec.fromXml(r);
+ fail("Expected exception due to exceeding the max total delay");
+ }
+ catch (IllegalArgumentException e) {
+ // success
+ assertEquals("The total delay specified is PT24H1S but max 24 hours is allowed", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testEmpty() {
+ assertFalse(DeploymentSpec.empty.globalServiceId().isPresent());
+ assertEquals(DeploymentSpec.UpgradePolicy.defaultPolicy, DeploymentSpec.empty.upgradePolicy());
+ assertTrue(DeploymentSpec.empty.steps().isEmpty());
+ assertEquals("<deployment version='1.0'/>", DeploymentSpec.empty.xmlForm());
+ }
+
+ @Test
+ public void productionSpecWithParallelDeployments() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <prod> \n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " <parallel>\n" +
+ " <region active='true'>us-central-1</region>\n" +
+ " <region active='true'>us-east-3</region>\n" +
+ " </parallel>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ DeploymentSpec.ParallelZones parallelZones = ((DeploymentSpec.ParallelZones) spec.steps().get(3));
+ assertEquals(2, parallelZones.zones().size());
+ assertEquals(RegionName.from("us-central-1"), parallelZones.zones().get(0).region().get());
+ assertEquals(RegionName.from("us-east-3"), parallelZones.zones().get(1).region().get());
+ }
+
+ @Test
+ public void productionSpecWithDuplicateRegions() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " <parallel>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " <region active='true'>us-central-1</region>\n" +
+ " <region active='true'>us-east-3</region>\n" +
+ " </parallel>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ 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 deploymentSpecWithIllegallyOrderedDeploymentSpec1() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <block-change days='sat' hours='10' time-zone='CET'/>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ " <block-change days='mon,tue' hours='15-16'/>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deploymentSpecWithIllegallyOrderedDeploymentSpec2() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <block-change days='sat' hours='10' time-zone='CET'/>\n" +
+ " <test/>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test
+ public void deploymentSpecWithChangeBlocker() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <block-change revision='false' days='mon,tue' hours='15-16'/>\n" +
+ " <block-change days='sat' hours='10' time-zone='CET'/>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(2, spec.changeBlocker().size());
+ assertTrue(spec.changeBlocker().get(0).blocksVersions());
+ assertFalse(spec.changeBlocker().get(0).blocksRevisions());
+ assertEquals(ZoneId.of("UTC"), spec.changeBlocker().get(0).window().zone());
+
+ assertTrue(spec.changeBlocker().get(1).blocksVersions());
+ assertTrue(spec.changeBlocker().get(1).blocksRevisions());
+ assertEquals(ZoneId.of("CET"), spec.changeBlocker().get(1).window().zone());
+
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-18T14:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-18T15:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-18T16:15:30.00Z")));
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-18T17:15:30.00Z")));
+
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-23T09:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-23T08:15:30.00Z"))); // 10 in CET
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-23T10:15:30.00Z")));
+ }
+
+ @Test
+ public void athenz_config_is_read_from_deployment() {
+ StringReader r = new StringReader(
+ "<deployment athenz-domain='domain' athenz-service='service'>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(spec.athenzDomain().get().value(), "domain");
+ assertEquals(spec.athenzService(Environment.prod, RegionName.from("us-west-1")).get().value(), "service");
+ }
+
+ @Test
+ public void athenz_service_is_overridden_from_environment() {
+ StringReader r = new StringReader(
+ "<deployment athenz-domain='domain' athenz-service='service'>\n" +
+ " <test/>\n" +
+ " <prod athenz-service='prod-service'>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(spec.athenzDomain().get().value(), "domain");
+ assertEquals(spec.athenzService(Environment.prod, RegionName.from("us-west-1")).get().value(), "prod-service");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void it_fails_when_athenz_service_is_not_defined() {
+ StringReader r = new StringReader(
+ "<deployment athenz-domain='domain'>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void it_fails_when_athenz_service_is_configured_but_not_athenz_domain() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <prod athenz-service='service'>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ }
+
+ @Test
+ public void noNotifications() {
+ assertEquals(Notifications.none(),
+ DeploymentSpec.fromXml("<deployment />").notifications());
+ }
+
+ @Test
+ public void emptyNotifications() {
+ DeploymentSpec spec = DeploymentSpec.fromXml("<deployment>\n" +
+ " <notifications />" +
+ "</deployment>");
+ assertEquals(Notifications.none(),
+ spec.notifications());
+ }
+
+ @Test
+ public void someNotifications() {
+ DeploymentSpec spec = DeploymentSpec.fromXml("<deployment>\n" +
+ " <notifications when=\"failing\">\n" +
+ " <email role=\"author\"/>\n" +
+ " <email address=\"john@dev\" when=\"failing-commit\"/>\n" +
+ " <email address=\"jane@dev\"/>\n" +
+ " </notifications>\n" +
+ "</deployment>");
+ assertEquals(ImmutableSet.of(author), spec.notifications().emailRolesFor(failing));
+ assertEquals(ImmutableSet.of(author), spec.notifications().emailRolesFor(failingCommit));
+ assertEquals(ImmutableSet.of("john@dev", "jane@dev"), spec.notifications().emailAddressesFor(failingCommit));
+ assertEquals(ImmutableSet.of("jane@dev"), spec.notifications().emailAddressesFor(failing));
+ }
+
+ @Test
+ public void customTesterFlavor() {
+ DeploymentSpec spec = DeploymentSpec.fromXml("<deployment>\n" +
+ " <test tester-flavor=\"d-1-4-20\" />\n" +
+ " <prod tester-flavor=\"d-2-8-50\">\n" +
+ " <region active=\"false\">us-north-7</region>\n" +
+ " </prod>\n" +
+ "</deployment>");
+ assertEquals(Optional.of("d-1-4-20"), spec.steps().get(0).zones().get(0).testerFlavor());
+ assertEquals(Optional.empty(), spec.steps().get(1).zones().get(0).testerFlavor());
+ assertEquals(Optional.of("d-2-8-50"), spec.steps().get(2).zones().get(0).testerFlavor());
+ }
+
+ @Test
+ public void noEndpoints() {
+ assertEquals(Collections.emptyList(), DeploymentSpec.fromXml("<deployment />").endpoints());
+ }
+
+ @Test
+ public void emptyEndpoints() {
+ final var spec = DeploymentSpec.fromXml("<deployment><endpoints/></deployment>");
+ assertEquals(Collections.emptyList(), spec.endpoints());
+ }
+
+ @Test
+ public void someEndpoints() {
+ final var spec = DeploymentSpec.fromXml("" +
+ "<deployment>" +
+ " <prod>" +
+ " <region active=\"true\">us-east</region>" +
+ " </prod>" +
+ " <endpoints>" +
+ " <endpoint id=\"foo\" container-id=\"bar\">" +
+ " <region>us-east</region>" +
+ " </endpoint>" +
+ " <endpoint id=\"nalle\" container-id=\"frosk\" />" +
+ " <endpoint container-id=\"quux\" />" +
+ " </endpoints>" +
+ "</deployment>");
+
+ assertEquals(
+ List.of("foo", "nalle", "default"),
+ spec.endpoints().stream().map(Endpoint::endpointId).collect(Collectors.toList())
+ );
+
+ assertEquals(
+ List.of("bar", "frosk", "quux"),
+ spec.endpoints().stream().map(Endpoint::containerId).collect(Collectors.toList())
+ );
+
+ assertEquals(Set.of(RegionName.from("us-east")), spec.endpoints().get(0).regions());
+ }
+ @Test
+ public void invalidEndpoints() {
+ assertInvalid("<endpoint id='FOO' container-id='qrs'/>"); // Uppercase
+ assertInvalid("<endpoint id='123' container-id='qrs'/>"); // Starting with non-character
+ assertInvalid("<endpoint id='foo!' container-id='qrs'/>"); // Non-alphanumeric
+ assertInvalid("<endpoint id='foo.bar' container-id='qrs'/>");
+ assertInvalid("<endpoint id='foo--bar' container-id='qrs'/>"); // Multiple consecutive dashes
+ assertInvalid("<endpoint id='foo-' container-id='qrs'/>"); // Trailing dash
+ assertInvalid("<endpoint id='foooooooooooo' container-id='qrs'/>"); // Too long
+ assertInvalid("<endpoint id='foo' container-id='qrs'/><endpoint id='foo' container-id='qrs'/>"); // Duplicate
+ }
+
+ @Test
+ public void validEndpoints() {
+ assertEquals(List.of("default"), endpointIds("<endpoint container-id='qrs'/>"));
+ assertEquals(List.of("default"), endpointIds("<endpoint id='' container-id='qrs'/>"));
+ assertEquals(List.of("f"), endpointIds("<endpoint id='f' container-id='qrs'/>"));
+ assertEquals(List.of("foo"), endpointIds("<endpoint id='foo' container-id='qrs'/>"));
+ assertEquals(List.of("foo-bar"), endpointIds("<endpoint id='foo-bar' container-id='qrs'/>"));
+ assertEquals(List.of("foo", "bar"), endpointIds("<endpoint id='foo' container-id='qrs'/><endpoint id='bar' container-id='qrs'/>"));
+ assertEquals(List.of("fooooooooooo"), endpointIds("<endpoint id='fooooooooooo' container-id='qrs'/>"));
+ }
+
+ @Test
+ public void endpointDefaultRegions() {
+ var spec = DeploymentSpec.fromXml("" +
+ "<deployment>" +
+ " <prod>" +
+ " <region active=\"true\">us-east</region>" +
+ " <region active=\"true\">us-west</region>" +
+ " </prod>" +
+ " <endpoints>" +
+ " <endpoint id=\"foo\" container-id=\"bar\">" +
+ " <region>us-east</region>" +
+ " </endpoint>" +
+ " <endpoint id=\"nalle\" container-id=\"frosk\" />" +
+ " <endpoint container-id=\"quux\" />" +
+ " </endpoints>" +
+ "</deployment>");
+
+ 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));
+ }
+
+ private static void assertInvalid(String endpointTag) {
+ try {
+ endpointIds(endpointTag);
+ fail("Expected exception for input '" + endpointTag + "'");
+ } catch (IllegalArgumentException ignored) {}
+ }
+
+ private static Set<String> endpointRegions(String endpointId, DeploymentSpec spec) {
+ return spec.endpoints().stream()
+ .filter(endpoint -> endpoint.endpointId().equals(endpointId))
+ .flatMap(endpoint -> endpoint.regions().stream())
+ .map(RegionName::value)
+ .collect(Collectors.toSet());
+ }
+
+ private static List<String> endpointIds(String endpointTag) {
+ var xml = "<deployment>" +
+ " <prod>" +
+ " <region active=\"true\">us-east</region>" +
+ " </prod>" +
+ " <endpoints>" +
+ endpointTag +
+ " </endpoints>" +
+ "</deployment>";
+
+ return DeploymentSpec.fromXml(xml).endpoints().stream()
+ .map(Endpoint::endpointId)
+ .collect(Collectors.toList());
+ }
+
+}