summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-10-07 09:37:26 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-10-07 09:37:26 +0200
commit3188f79fdad37e3ea30f84f8c3be67b0c645386d (patch)
treefcf11e9ac4476e390262502d6e9536b14d9aee32
parent5188586ce3314efd0fffd52a62403e1b371890dd (diff)
Inherit/use defaults in instances
-rw-r--r--config-model-api/abi-spec.json12
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java55
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java83
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java121
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java294
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java6
7 files changed, 444 insertions, 128 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'>" +
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java
index b9573b21199..012f246a227 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Environment.java
@@ -5,7 +5,6 @@ package com.yahoo.config.provision;
* Environments in hosted Vespa.
*
* @author bratseth
- * @since 5.11
*/
public enum Environment {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
index 43e0a07eae1..6f64237b2c4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
@@ -894,9 +894,9 @@ public class ApplicationController {
* 2. If the principal is given, verify that the principal is tenant admin or admin of the tenant domain
* 3. If the principal is not given, verify that the Athenz domain of the tenant equals Athenz domain given in deployment.xml
*
- * @param tenantName Tenant where application should be deployed
- * @param applicationPackage Application package
- * @param deployer Principal initiating the deployment, possibly empty
+ * @param tenantName tenant where application should be deployed
+ * @param applicationPackage application package
+ * @param deployer principal initiating the deployment, possibly empty
*/
public void verifyApplicationIdentityConfiguration(TenantName tenantName, ApplicationPackage applicationPackage, Optional<Principal> deployer) {
verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec());