summaryrefslogtreecommitdiffstats
path: root/config-model-api/src/main/java
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-10-08 09:37:57 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-10-08 09:37:57 +0200
commit07d4f467957373ac25c173cfcd0667206da2733b (patch)
tree33b5cbc153843f802a7344f5c1c6b50038473b78 /config-model-api/src/main/java
parent2e26619762083e46f45006107bf400eb308f6219 (diff)
Revert "Merge pull request #10909 from vespa-engine/revert-10891-bratseth/instances-in-deployment-xml-rebased"
This reverts commit 6474f43ba04731e8bd38d2613ad5098e3cfce90d, reversing changes made to 6ff782a0eb830f2382185a1efd7d0830b3208fae.
Diffstat (limited to 'config-model-api/src/main/java')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java254
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java383
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java278
3 files changed, 623 insertions, 292 deletions
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
new file mode 100644
index 00000000000..df611d66b87
--- /dev/null
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
@@ -0,0 +1,254 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+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;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * The deployment spec for an application instance
+ *
+ * @author bratseth
+ */
+public class DeploymentInstanceSpec extends DeploymentSpec.Step {
+
+ /** The name of the instance this step deploys */
+ private final InstanceName name;
+
+ private final List<DeploymentSpec.Step> steps;
+ private final DeploymentSpec.UpgradePolicy upgradePolicy;
+ private final List<DeploymentSpec.ChangeBlocker> changeBlockers;
+ private final Optional<String> globalServiceId;
+ private final Optional<AthenzDomain> athenzDomain;
+ private final Optional<AthenzService> athenzService;
+ private final Notifications notifications;
+ private final List<Endpoint> endpoints;
+
+ public DeploymentInstanceSpec(InstanceName name,
+ List<DeploymentSpec.Step> steps,
+ DeploymentSpec.UpgradePolicy upgradePolicy,
+ List<DeploymentSpec.ChangeBlocker> changeBlockers,
+ Optional<String> globalServiceId,
+ Optional<AthenzDomain> athenzDomain,
+ Optional<AthenzService> athenzService,
+ Notifications notifications,
+ List<Endpoint> endpoints) {
+ this.name = name;
+ this.steps = steps;
+ this.upgradePolicy = upgradePolicy;
+ this.changeBlockers = changeBlockers;
+ this.globalServiceId = globalServiceId;
+ this.athenzDomain = athenzDomain;
+ this.athenzService = athenzService;
+ this.notifications = notifications;
+ this.endpoints = List.copyOf(validateEndpoints(endpoints, this.steps));
+ validateZones(this.steps);
+ validateEndpoints(this.steps, globalServiceId, this.endpoints);
+ validateAthenz();
+ }
+
+ public InstanceName name() { return name; }
+
+ /** Throw an IllegalArgumentException if any production zone is declared multiple times */
+ private void validateZones(List<DeploymentSpec.Step> steps) {
+ Set<DeploymentSpec.DeclaredZone> zones = new HashSet<>();
+
+ for (DeploymentSpec.Step step : steps)
+ for (DeploymentSpec.DeclaredZone zone : step.zones())
+ ensureUnique(zone, zones);
+ }
+
+ private void ensureUnique(DeploymentSpec.DeclaredZone zone, Set<DeploymentSpec.DeclaredZone> zones) {
+ if ( ! zones.add(zone))
+ throw new IllegalArgumentException(zone + " is listed twice in deployment.xml");
+ }
+
+ /** Validates the endpoints and makes sure default values are respected */
+ private List<Endpoint> validateEndpoints(List<Endpoint> endpoints, List<DeploymentSpec.Step> steps) {
+ Objects.requireNonNull(endpoints, "Missing endpoints parameter");
+
+ var productionRegions = steps.stream()
+ .filter(step -> step.deploysTo(Environment.prod))
+ .flatMap(step -> step.zones().stream())
+ .flatMap(zone -> zone.region().stream())
+ .map(RegionName::value)
+ .collect(Collectors.toSet());
+
+ var rebuiltEndpointsList = new ArrayList<Endpoint>();
+
+ for (var endpoint : endpoints) {
+ if (endpoint.regions().isEmpty()) {
+ var rebuiltEndpoint = endpoint.withRegions(productionRegions);
+ rebuiltEndpointsList.add(rebuiltEndpoint);
+ } else {
+ rebuiltEndpointsList.add(endpoint);
+ }
+ }
+
+ 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
+ * domain configured -> all zones must configure service
+ */
+ private void validateAthenz() {
+ // If athenz domain is not set, athenz service cannot be set on any level
+ if (athenzDomain.isEmpty()) {
+ for (DeploymentSpec.DeclaredZone zone : zones()) {
+ if(zone.athenzService().isPresent()) {
+ throw new IllegalArgumentException("Athenz service configured for zone: " + zone + ", but Athenz domain is not configured");
+ }
+ }
+ // if athenz domain is not set, athenz service must be set implicitly or directly on all zones.
+ } else if (athenzService.isEmpty()) {
+ for (DeploymentSpec.DeclaredZone zone : zones()) {
+ if (zone.athenzService().isEmpty()) {
+ throw new IllegalArgumentException("Athenz domain is configured, but Athenz service not configured for zone: " + zone);
+ }
+ }
+ }
+ }
+
+ @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 */
+ @Override
+ 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 deployment steps which are zones 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 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 the notification configuration of these instances */
+ public Notifications notifications() { return notifications; }
+
+ /** Returns the rotations configuration of these instances */
+ public List<Endpoint> endpoints() { return endpoints; }
+
+ /** 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;
+ }
+
+ 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;
+ if (o == null || getClass() != o.getClass()) return false;
+ DeploymentInstanceSpec other = (DeploymentInstanceSpec) 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) &&
+ notifications.equals(other.notifications) &&
+ endpoints.equals(other.endpoints);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(globalServiceId, upgradePolicy, changeBlockers, steps, athenzDomain, athenzService, notifications, endpoints);
+ }
+
+ @Override
+ public String toString() {
+ return "instance '" + name + "'";
+ }
+
+}
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 efe75d191b8..71af050174b 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;
@@ -14,11 +15,9 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -46,218 +45,195 @@ public class DeploymentSpec {
Optional.empty(),
Notifications.none(),
List.of());
-
- private final Optional<String> globalServiceId;
- private final UpgradePolicy upgradePolicy;
- private final Optional<Integer> majorVersion;
- private final List<ChangeBlocker> changeBlockers;
+
private final List<Step> steps;
+ private final Optional<Integer> majorVersion;
private final String xmlForm;
- private final Optional<AthenzDomain> athenzDomain;
- private final Optional<AthenzService> athenzService;
- private final Notifications notifications;
- private final List<Endpoint> endpoints;
-
- 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) {
- validateTotalDelay(steps);
- this.globalServiceId = globalServiceId;
- this.upgradePolicy = upgradePolicy;
- this.majorVersion = majorVersion;
- this.changeBlockers = changeBlockers;
- this.steps = List.copyOf(completeSteps(new ArrayList<>(steps)));
- this.xmlForm = xmlForm;
- this.athenzDomain = athenzDomain;
- this.athenzService = athenzService;
- this.notifications = notifications;
- this.endpoints = List.copyOf(validateEndpoints(endpoints, this.steps));
- validateZones(this.steps);
- validateAthenz();
- validateEndpoints(this.steps, globalServiceId, this.endpoints);
- }
-
- /** Validates the endpoints and makes sure default values are respected */
- private List<Endpoint> validateEndpoints(List<Endpoint> endpoints, List<Step> steps) {
- Objects.requireNonNull(endpoints, "Missing endpoints parameter");
-
- var productionRegions = steps.stream()
- .filter(step -> step.deploysTo(Environment.prod))
- .flatMap(step -> step.zones().stream())
- .flatMap(zone -> zone.region().stream())
- .map(RegionName::value)
- .collect(Collectors.toSet());
-
- var rebuiltEndpointsList = new ArrayList<Endpoint>();
-
- for (var endpoint : endpoints) {
- if (endpoint.regions().isEmpty()) {
- var rebuiltEndpoint = endpoint.withRegions(productionRegions);
- rebuiltEndpointsList.add(rebuiltEndpoint);
- } else {
- rebuiltEndpointsList.add(endpoint);
- }
- }
-
- return List.copyOf(rebuiltEndpointsList);
- }
-
- /** 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();
- if (totalDelaySeconds > Duration.ofHours(24).getSeconds())
- throw new IllegalArgumentException("The total delay specified is " + Duration.ofSeconds(totalDelaySeconds) +
- " but max 24 hours is allowed");
- }
-
- /** Throw an IllegalArgumentException if any production zone is declared multiple times */
- private void validateZones(List<Step> steps) {
- Set<DeclaredZone> zones = new HashSet<>();
-
- for (Step step : steps)
- for (DeclaredZone zone : step.zones())
- ensureUnique(zone, zones);
- }
-
- /** Throw an IllegalArgumentException if an endpoint refers to a region that is not declared in 'prod' */
- private void validateEndpoints(List<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);
- }
- }
+ public DeploymentSpec(List<Step> steps,
+ Optional<Integer> majorVersion,
+ String xmlForm) {
+ 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())));
}
- }
-
- /*
- * Throw an IllegalArgumentException if Athenz configuration violates:
- * domain not configured -> no zone can configure service
- * domain configured -> all zones must configure service
- */
- private void validateAthenz() {
- // If athenz domain is not set, athenz service cannot be set on any level
- if (athenzDomain.isEmpty()) {
- for (DeclaredZone zone : zones()) {
- if(zone.athenzService().isPresent()) {
- throw new IllegalArgumentException("Athenz service configured for zone: " + zone + ", but Athenz domain is not configured");
- }
- }
- // if athenz domain is not set, athenz service must be set implicitly or directly on all zones.
- } else if (athenzService.isEmpty()) {
- for (DeclaredZone zone : zones()) {
- if (zone.athenzService().isEmpty()) {
- throw new IllegalArgumentException("Athenz domain is configured, but Athenz service not configured for zone: " + zone);
- }
- }
+ else {
+ this.steps = List.copyOf(completeSteps(steps));
}
+ this.majorVersion = majorVersion;
+ this.xmlForm = xmlForm;
+ validateTotalDelay(steps);
}
- private void ensureUnique(DeclaredZone zone, Set<DeclaredZone> zones) {
- if ( ! zones.add(zone))
- throw new IllegalArgumentException(zone + " is listed twice in deployment.xml");
+ // TODO: Remove after October 2019
+ 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 DeploymentInstanceSpec(InstanceName.from("default"),
+ steps,
+ upgradePolicy,
+ changeBlockers,
+ globalServiceId,
+ athenzDomain,
+ athenzService,
+ notifications,
+ endpoints)),
+ majorVersion,
+ xmlForm);
}
/** Adds missing required steps and reorders steps to a permissible order */
- private static List<Step> completeSteps(List<Step> steps) {
+ 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 DeclaredZone(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 DeclaredZone(Environment.test));
+ steps.add(new DeploymentSpec.DeclaredZone(Environment.test));
}
-
+
// Enforce order test, staging, prod
- DeclaredZone testStep = remove(Environment.test, steps);
+ DeploymentSpec.DeclaredZone testStep = remove(Environment.test, steps);
if (testStep != null)
steps.add(0, testStep);
- DeclaredZone stagingStep = remove(Environment.staging, steps);
+ 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 DeclaredZone remove(Environment environment, List<Step> steps) {
+ 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 (DeclaredZone)steps.remove(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;
}
- /** Returns the ID of the service to expose through global routing, if present */
- public Optional<String> globalServiceId() {
- return globalServiceId;
+ /** 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();
+ 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 upgrade policy of this, which is defaultPolicy if none is specified */
- public UpgradePolicy upgradePolicy() { return upgradePolicy; }
+ // TODO: Remove after October 2019
+ private DeploymentInstanceSpec defaultInstance() {
+ 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(",")));
+ }
+
+ // 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 November 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 November 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 November 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 (singleInstance(steps)) return defaultInstance().steps(); // TODO: Remove line after November 2019
+ return steps;
+ }
- /** Returns all the DeclaredZone deployment steps in the order they are declared */
+ // TODO: Remove after November 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; }
+ // TODO: Remove after November 2019
+ public Optional<AthenzDomain> athenzDomain() { return defaultInstance().athenzDomain(); }
- /** Returns the rotations configuration */
- public List<Endpoint> endpoints() { return endpoints; }
+ // TODO: Remove after November 2019
+ public Optional<AthenzService> athenzService(Environment environment, RegionName region) {
+ return defaultInstance().athenzService(environment, region);
+ }
+
+ // TODO: Remove after November 2019
+ public Notifications notifications() { return defaultInstance().notifications(); }
+
+ // TODO: Remove after November 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 whether this deployment spec specifies the given zone, either implicitly or explicitly */
+ // TODO: Remove after November 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);
+ }
+
+ // 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));
+ }
+
+ /** Returns the instance step containing the given instance name, or null if not present */
+ public DeploymentInstanceSpec instance(InstanceName name) {
+ for (DeploymentInstanceSpec instance : instances()) {
+ if (instance.name().equals(name))
+ return instance;
+ }
+ return null;
+ }
+
+ /** Returns the instance step containing the given instance name, or throws an IllegalArgumentException if not present */
+ public DeploymentInstanceSpec requireInstance(String name) {
+ return requireInstance(InstanceName.from(name));
+ }
+
+ public DeploymentInstanceSpec requireInstance(InstanceName name) {
+ DeploymentInstanceSpec instance = instance(name);
+ if (instance == null)
+ throw new IllegalArgumentException("No instance '" + name + "' in deployment.xml'. Instances: " +
+ instances().stream().map(spec -> spec.name().toString()).collect(Collectors.joining(",")));
+ return instance;
+ }
+
+ /** Returns the steps of this which are instances */
+ public List<DeploymentInstanceSpec> instances() {
+ return steps.stream()
+ .filter(step -> step instanceof DeploymentInstanceSpec).map(DeploymentInstanceSpec.class::cast)
+ .collect(Collectors.toList());
}
/**
@@ -304,40 +280,19 @@ 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);
}
@Override
public int hashCode() {
- return Objects.hash(globalServiceId, upgradePolicy, majorVersion, changeBlockers, steps, xmlForm, athenzDomain, athenzService, notifications);
+ return Objects.hash(majorVersion, steps, xmlForm);
}
/** This may be invoked by a continuous build */
@@ -365,7 +320,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());
@@ -377,6 +332,12 @@ 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; }
+
+ /** 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 */
@@ -387,12 +348,21 @@ 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; }
+ @Override
+ public String toString() {
+ return "delay " + duration;
+ }
+
}
/** A deployment step which is to run deployment in a particular zone */
@@ -473,21 +443,31 @@ public class DeploymentSpec {
}
- /** A deployment step which is to run deployment to multiple zones in parallel */
+ /** A deployment step which is to run multiple steps (zones or instances) in parallel */
public static class ParallelZones extends Step {
- private final List<DeclaredZone> zones;
+ private final List<Step> steps;
+
+ public ParallelZones(List<Step> steps) {
+ this.steps = List.copyOf(steps);
+ }
- public ParallelZones(List<DeclaredZone> zones) {
- this.zones = List.copyOf(zones);
+ /** Returns the steps inside this which are zones */
+ @Override
+ public List<DeclaredZone> zones() {
+ return this.steps.stream()
+ .filter(step -> step instanceof DeclaredZone)
+ .map(DeclaredZone.class::cast)
+ .collect(Collectors.toList());
}
+ /** Returns all the steps nested in this */
@Override
- public List<DeclaredZone> zones() { return this.zones; }
+ 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
@@ -495,13 +475,19 @@ public class DeploymentSpec {
if (this == o) return true;
if (!(o instanceof ParallelZones)) return false;
ParallelZones that = (ParallelZones) o;
- return Objects.equals(zones, that.zones);
+ return Objects.equals(steps, that.steps);
}
@Override
public int hashCode() {
- return Objects.hash(zones);
+ return Objects.hash(steps);
}
+
+ @Override
+ public String toString() {
+ return steps.size() + " parallel steps";
+ }
+
}
/** Controls when this application will be upgraded to new Vespa versions */
@@ -530,6 +516,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 72a806bb7be..cb645813290 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
@@ -1,6 +1,7 @@
// 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.xml;
+import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
import com.yahoo.config.application.api.DeploymentSpec.Delay;
@@ -14,6 +15,7 @@ import com.yahoo.config.application.api.TimeWindow;
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 com.yahoo.io.IOUtils;
import com.yahoo.text.XML;
@@ -38,27 +40,36 @@ import java.util.stream.Collectors;
*/
public class DeploymentSpecXmlReader {
+ private static final String instanceTag = "instance";
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";
+ private static final String delayTag = "delay";
+ 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";
private final boolean validate;
- /**
- * Creates a validating reader
- */
+ /** Creates a validating reader */
public DeploymentSpecXmlReader() {
this(true);
}
/**
- * Creates a reader
+ * Creates a deployment spec reader
*
- * @param validate true to validate the input, false to accept any input which can be unabiguously parsed
+ * @param validate true to validate the input, false to accept any input which can be unambiguously parsed
*/
public DeploymentSpecXmlReader(boolean validate) {
this.validate = validate;
@@ -73,67 +84,135 @@ public class DeploymentSpecXmlReader {
}
}
- /**
- * Reads a deployment spec from XML
- */
+ /** Reads a deployment spec from XML */
public DeploymentSpec read(String xmlForm) {
- List<Step> steps = new ArrayList<>();
- Optional<String> globalServiceId = Optional.empty();
Element root = XML.getDocument(xmlForm).getDocumentElement();
- if (validate)
- validateTagOrder(root);
- for (Element environmentTag : XML.getChildren(root)) {
- if (!isEnvironmentName(environmentTag.getTagName())) continue;
-
- Environment environment = Environment.from(environmentTag.getTagName());
- Optional<AthenzService> athenzService = stringAttribute("athenz-service", environmentTag).map(AthenzService::from);
- Optional<String> testerFlavor = stringAttribute("tester-flavor", environmentTag);
-
- if (environment == Environment.prod) {
- for (Element stepTag : XML.getChildren(environmentTag)) {
- if (stepTag.getTagName().equals("delay")) {
- steps.add(new Delay(Duration.ofSeconds(longAttribute("hours", stepTag) * 60 * 60 +
- longAttribute("minutes", stepTag) * 60 +
- longAttribute("seconds", stepTag))));
- }
- else if (stepTag.getTagName().equals("parallel")) {
- List<DeclaredZone> zones = new ArrayList<>();
- for (Element regionTag : XML.getChildren(stepTag)) {
- zones.add(readDeclaredZone(environment, athenzService, testerFlavor, regionTag));
- }
- steps.add(new ParallelZones(zones));
- }
- else { // a region: deploy step
- steps.add(readDeclaredZone(environment, athenzService, testerFlavor, stepTag));
- }
- }
- }
- else {
- steps.add(new DeclaredZone(environment, Optional.empty(), false, athenzService, testerFlavor));
+
+ List<Step> steps = new ArrayList<>();
+ if ( ! containsTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance
+ steps.addAll(readInstanceContent("default", root, new MutableOptional<>(), root));
+ }
+ else {
+ 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(idAttribute), topLevelTag, new MutableOptional<>(), root));
+ else
+ steps.addAll(readNonInstanceSteps(topLevelTag, new MutableOptional<>(), topLevelTag)); // (No global service id here)
}
+ }
+
+ return new DeploymentSpec(steps,
+ optionalIntegerAttribute(majorVersionTag, root),
+ xmlForm);
+ }
+
+ /**
+ * Reads the content of an (implicit or explicit) instance tag producing an instances step
+ *
+ * @param instanceNameString a comma-separated list of the names of the instances this is for
+ * @param instanceTag the element having the content of this instance
+ * @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,
+ MutableOptional<String> globalServiceId,
+ Element parentTag) {
+ if (validate)
+ validateTagOrder(instanceTag);
+
+ // 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 -> name.trim())
+ .map(name -> new DeploymentInstanceSpec(InstanceName.from(name),
+ steps,
+ upgradePolicy,
+ changeBlockers,
+ globalServiceId.asOptional(),
+ athenzDomain,
+ athenzService,
+ notifications,
+ endpoints))
+ .collect(Collectors.toList());
+ }
+
+ 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);
- if (environment == Environment.prod)
- globalServiceId = readGlobalServiceId(environmentTag);
- else if (readGlobalServiceId(environmentTag).isPresent())
- throw new IllegalArgumentException("Attribute 'global-service-id' is only valid on 'prod' tag.");
+ }
+ // 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))
+ .map(AthenzService::from);
+ Optional<String> testerFlavor = stringAttribute(testerFlavorAttribute, stepTag)
+ .or(() -> stringAttribute(testerFlavorAttribute, parentTag));
+
+ if (prodTag.equals(stepTag.getTagName()))
+ globalServiceId.set(readGlobalServiceId(stepTag));
+ else if (readGlobalServiceId(stepTag).isPresent())
+ throw new IllegalArgumentException("Attribute 'global-service-id' is only valid on 'prod' tag.");
+
+ switch (stepTag.getTagName()) {
+ case testTag: case stagingTag:
+ return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor));
+ case prodTag: // regions, delay and parallel may be nested within, but we can flatten them
+ return XML.getChildren(stepTag).stream()
+ .flatMap(child -> readNonInstanceSteps(child, globalServiceId, stepTag).stream())
+ .collect(Collectors.toList());
+ case delayTag:
+ return List.of(new Delay(Duration.ofSeconds(longAttribute("hours", stepTag) * 60 * 60 +
+ longAttribute("minutes", stepTag) * 60 +
+ longAttribute("seconds", stepTag))));
+ case parallelTag: // regions and instances may be nested within
+ return List.of(new ParallelZones(XML.getChildren(stepTag).stream()
+ .flatMap(child -> readSteps(child, globalServiceId, stepTag).stream())
+ .collect(Collectors.toList())));
+ case regionTag:
+ return List.of(readDeclaredZone(Environment.prod, athenzService, testerFlavor, stepTag));
+ default:
+ return List.of();
}
- Optional<AthenzDomain> athenzDomain = stringAttribute("athenz-domain", root).map(AthenzDomain::from);
- Optional<AthenzService> athenzService = stringAttribute("athenz-service", root).map(AthenzService::from);
- return new DeploymentSpec(globalServiceId,
- readUpgradePolicy(root),
- optionalIntegerAttribute(majorVersionTag, root),
- readChangeBlockers(root),
- steps,
- xmlForm,
- athenzDomain,
- athenzService,
- readNotifications(root),
- readEndpoints(root));
}
- private Notifications readNotifications(Element root) {
- Element notificationsElement = XML.getChild(root, "notifications");
+ 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 parent, Element fallbackParent) {
+ Element notificationsElement = XML.getChild(parent, notificationsTag);
+ if (notificationsElement == null)
+ notificationsElement = XML.getChild(fallbackParent, notificationsTag);
if (notificationsElement == null)
return Notifications.none();
@@ -158,16 +237,17 @@ public class DeploymentSpecXmlReader {
return Notifications.of(emailAddresses, emailRoles);
}
- private List<Endpoint> readEndpoints(Element root) {
- final 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();
- final var endpoints = new LinkedHashMap<String, Endpoint>();
+ var endpoints = new LinkedHashMap<String, Endpoint>();
for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) {
- final Optional<String> rotationId = stringAttribute("id", endpointElement);
- final Optional<String> containerId = stringAttribute("container-id", endpointElement);
- final var regions = new HashSet<String>();
+ Optional<String> rotationId = stringAttribute("id", endpointElement);
+ Optional<String> containerId = stringAttribute("container-id", endpointElement);
+ var regions = new HashSet<String>();
if (containerId.isEmpty()) {
throw new IllegalArgumentException("Missing 'container-id' from 'endpoint' tag.");
@@ -255,10 +335,6 @@ public class DeploymentSpecXmlReader {
return Optional.ofNullable(value).filter(s -> !s.equals(""));
}
- private boolean isEnvironmentName(String tagName) {
- return tagName.equals(testTag) || tagName.equals(stagingTag) || tagName.equals(prodTag);
- }
-
private DeclaredZone readDeclaredZone(Environment environment, Optional<AthenzService> athenzService,
Optional<String> testerFlavor, Element regionTag) {
return new DeclaredZone(environment, Optional.of(RegionName.from(XML.getValue(regionTag).trim())),
@@ -267,44 +343,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) {
@@ -324,4 +400,14 @@ public class DeploymentSpecXmlReader {
"to control whether the region should receive production traffic");
}
+ private static class MutableOptional<T> {
+
+ private Optional<T> value = Optional.empty();
+
+ public void set(Optional<T> value) { this.value = value; }
+
+ public Optional<T> asOptional() { return value; }
+
+ }
+
}