diff options
Diffstat (limited to 'config-model-api/src/main/java/com')
3 files changed, 290 insertions, 646 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 deleted file mode 100644 index df611d66b87..00000000000 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ /dev/null @@ -1,254 +0,0 @@ -// 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 9b0454cffee..efe75d191b8 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,7 +5,6 @@ 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; @@ -15,9 +14,11 @@ 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; /** @@ -45,218 +46,218 @@ public class DeploymentSpec { Optional.empty(), Notifications.none(), List.of()); - - private final List<Step> steps; - - // Attributes which can be set on the root tag and which must be available outside of any particular instance + + 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 String xmlForm; private final Optional<AthenzDomain> athenzDomain; private final Optional<AthenzService> athenzService; + private final Notifications notifications; + private final List<Endpoint> endpoints; - private final String xmlForm; - - public DeploymentSpec(List<Step> steps, - Optional<Integer> majorVersion, - Optional<AthenzDomain> athenzDomain, - Optional<AthenzService> athenzService, - 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()))); - } - else { - this.steps = List.copyOf(completeSteps(steps)); - } + 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.xmlForm = xmlForm; - validateTotalDelay(steps); + this.notifications = notifications; + this.endpoints = List.copyOf(validateEndpoints(endpoints, this.steps)); + validateZones(this.steps); + validateAthenz(); + validateEndpoints(this.steps, globalServiceId, this.endpoints); } - // 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, - athenzDomain, - athenzService, - xmlForm); + /** 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"); } - /** 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); + /** 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); + } + } + } + } + + /* + * 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); + } + } + } + } + + private void ensureUnique(DeclaredZone zone, Set<DeclaredZone> zones) { + if ( ! zones.add(zone)) + throw new IllegalArgumentException(zone + " is listed twice in deployment.xml"); + } + + /** Adds missing required steps and reorders steps to a permissible order */ + private static List<Step> completeSteps(List<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)); + steps.add(new 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)); + steps.add(new DeclaredZone(Environment.test)); } - + // Enforce order test, staging, prod - DeploymentSpec.DeclaredZone testStep = remove(Environment.test, steps); + DeclaredZone testStep = remove(Environment.test, steps); if (testStep != null) steps.add(0, testStep); - DeploymentSpec.DeclaredZone stagingStep = remove(Environment.staging, steps); + 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) { + private static DeclaredZone remove(Environment environment, List<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; - } + if (steps.get(i).deploysTo(environment)) + return (DeclaredZone)steps.remove(i); } 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(); - if (totalDelaySeconds > Duration.ofHours(24).getSeconds()) - throw new IllegalArgumentException("The total delay specified is " + Duration.ofSeconds(totalDelaySeconds) + - " but max 24 hours is allowed"); - } - - // TODO: Remove after October 2019 - private DeploymentInstanceSpec singleInstance() { - 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(","))); + /** Returns the ID of the service to expose through global routing, if present */ + public Optional<String> globalServiceId() { + return globalServiceId; } - // TODO: Remove after October 2019 - public Optional<String> globalServiceId() { return singleInstance().globalServiceId(); } - - // TODO: Remove after October 2019 - public UpgradePolicy upgradePolicy() { return singleInstance().upgradePolicy(); } + /** Returns the upgrade policy of this, which is defaultPolicy if none is specified */ + public UpgradePolicy upgradePolicy() { return upgradePolicy; } /** Returns the major version this application is pinned to, or empty (default) to allow all major versions */ public Optional<Integer> majorVersion() { return majorVersion; } - // TODO: Remove after November 2019 - public boolean canUpgradeAt(Instant instant) { return singleInstance().canUpgradeAt(instant); } - - // TODO: Remove after November 2019 - public boolean canChangeRevisionAt(Instant instant) { return singleInstance().canChangeRevisionAt(instant); } - - // TODO: Remove after November 2019 - public List<ChangeBlocker> changeBlocker() { return singleInstance().changeBlocker(); } - - /** Returns the deployment steps of this in the order they will be performed */ - public List<Step> steps() { - if (singleInstance(steps)) return singleInstance().steps(); // TODO: Remove line after November 2019 - return steps; + /** 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 List<DeclaredZone> zones() { - return singleInstance().steps().stream() - .flatMap(step -> step.zones().stream()) - .collect(Collectors.toList()); + /** 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)); } - /** Returns the Athenz domain set on the root tag, if any */ - public Optional<AthenzDomain> athenzDomain() { return athenzDomain; } + /** Returns time windows where upgrades are disallowed */ + public List<ChangeBlocker> changeBlocker() { return changeBlockers; } - /** Returns the Athenz service to use for the given environment and region, if any */ - // TODO: Remove after November 2019 - public Optional<AthenzService> athenzService(Environment environment, RegionName region) { - Optional<AthenzService> service = Optional.empty(); - if (singleInstance(steps)) - service = singleInstance().athenzService(environment, region); - if (service.isPresent()) - return service; - return this.athenzService; - } + /** Returns the deployment steps of this in the order they will be performed */ + public List<Step> steps() { return steps; } - /** - * Returns the Athenz service to use for the given instance, environment and region, if any. - * This returns the default set on the deploy tag (if any) if nothing is set for this particular - * combination of instance, environment and region, and also if that combination is not specified - * at all in services. - */ - public Optional<AthenzService> athenzService(InstanceName instanceName, Environment environment, RegionName region) { - Optional<DeploymentInstanceSpec> instance = instance(instanceName); - if (instance.isEmpty()) return this.athenzService; - return instance.get().athenzService(environment, region).or(() -> this.athenzService); + /** Returns all the DeclaredZone deployment steps in the order they are declared */ + public List<DeclaredZone> zones() { + return steps.stream() + .flatMap(step -> step.zones().stream()) + .collect(Collectors.toList()); } - // TODO: Remove after November 2019 - public Notifications notifications() { return singleInstance().notifications(); } + /** Returns the notification configuration */ + public Notifications notifications() { return notifications; } - // TODO: Remove after November 2019 - public List<Endpoint> endpoints() { return singleInstance().endpoints(); } + /** Returns the rotations configuration */ + public List<Endpoint> endpoints() { return 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; } - // TODO: Remove after November 2019 + /** Returns whether this deployment spec specifies the given zone, either implicitly or explicitly */ public boolean includes(Environment environment, Optional<RegionName> region) { - return singleInstance().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 */ - public Optional<DeploymentInstanceSpec> instance(InstanceName name) { - for (DeploymentInstanceSpec instance : instances()) { - if (instance.name().equals(name)) - return Optional.of(instance); - } - return Optional.empty(); - } - - public DeploymentInstanceSpec requireInstance(String name) { - return requireInstance(InstanceName.from(name)); - } - - public DeploymentInstanceSpec requireInstance(InstanceName name) { - Optional<DeploymentInstanceSpec> instance = instance(name); - if (instance.isEmpty()) - throw new IllegalArgumentException("No instance '" + name + "' in deployment.xml'. Instances: " + - instances().stream().map(spec -> spec.name().toString()).collect(Collectors.joining(","))); - return instance.get(); - } - - /** 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()); + for (Step step : steps) + if (step.deploysTo(environment, region)) return true; + return false; } /** @@ -303,19 +304,40 @@ 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 other = (DeploymentSpec) o; - return majorVersion.equals(other.majorVersion) && - steps.equals(other.steps) && - xmlForm.equals(other.xmlForm); + 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); } @Override public int hashCode() { - return Objects.hash(majorVersion, steps, xmlForm); + return Objects.hash(globalServiceId, upgradePolicy, majorVersion, changeBlockers, steps, xmlForm, athenzDomain, athenzService, notifications); } /** This may be invoked by a continuous build */ @@ -343,7 +365,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()); @@ -355,12 +377,6 @@ 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 */ @@ -371,21 +387,12 @@ 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 */ @@ -466,31 +473,21 @@ public class DeploymentSpec { } - /** A deployment step which is to run multiple steps (zones or instances) in parallel */ + /** A deployment step which is to run deployment to multiple zones in parallel */ public static class ParallelZones extends Step { - private final List<Step> steps; + private final List<DeclaredZone> zones; - 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<Step> steps() { return steps; } + public List<DeclaredZone> zones() { return this.zones; } @Override public boolean deploysTo(Environment environment, Optional<RegionName> region) { - return steps().stream().anyMatch(zone -> zone.deploysTo(environment, region)); + return zones.stream().anyMatch(zone -> zone.deploysTo(environment, region)); } @Override @@ -498,19 +495,13 @@ public class DeploymentSpec { if (this == o) return true; if (!(o instanceof ParallelZones)) return false; ParallelZones that = (ParallelZones) o; - return Objects.equals(steps, that.steps); + return Objects.equals(zones, that.zones); } @Override public int hashCode() { - return Objects.hash(steps); - } - - @Override - public String toString() { - return steps.size() + " parallel steps"; + return Objects.hash(zones); } - } /** Controls when this application will be upgraded to new Vespa versions */ @@ -539,11 +530,6 @@ 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 59b31985376..72a806bb7be 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,7 +1,6 @@ // 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; @@ -15,7 +14,6 @@ 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; @@ -40,36 +38,27 @@ 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 deployment spec reader + * Creates a reader * - * @param validate true to validate the input, false to accept any input which can be unambiguously parsed + * @param validate true to validate the input, false to accept any input which can be unabiguously parsed */ public DeploymentSpecXmlReader(boolean validate) { this.validate = validate; @@ -84,137 +73,67 @@ public class DeploymentSpecXmlReader { } } - /** Reads a deployment spec from XML */ - public DeploymentSpec read(String xmlForm) { - Element root = XML.getDocument(xmlForm).getDocumentElement(); - - 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), - stringAttribute(athenzDomainAttribute, root).map(AthenzDomain::from), - stringAttribute(athenzServiceAttribute, root).map(AthenzService::from), - 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 + * Reads a deployment spec from XML */ - 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 + public DeploymentSpec read(String xmlForm) { 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); - - } + 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)); + } - // 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(); - } - } + 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."); - 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; + 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 parent, Element fallbackParent) { - Element notificationsElement = XML.getChild(parent, notificationsTag); - if (notificationsElement == null) - notificationsElement = XML.getChild(fallbackParent, notificationsTag); + private Notifications readNotifications(Element root) { + Element notificationsElement = XML.getChild(root, "notifications"); if (notificationsElement == null) return Notifications.none(); @@ -239,17 +158,16 @@ public class DeploymentSpecXmlReader { return Notifications.of(emailAddresses, emailRoles); } - private List<Endpoint> readEndpoints(Element parent) { - var endpointsElement = XML.getChild(parent, endpointsTag); - if (endpointsElement == null) - return Collections.emptyList(); + private List<Endpoint> readEndpoints(Element root) { + final var endpointsElement = XML.getChild(root, endpointsTag); + if (endpointsElement == null) { return Collections.emptyList(); } - var endpoints = new LinkedHashMap<String, Endpoint>(); + final var endpoints = new LinkedHashMap<String, Endpoint>(); for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) { - Optional<String> rotationId = stringAttribute("id", endpointElement); - Optional<String> containerId = stringAttribute("container-id", endpointElement); - var regions = new HashSet<String>(); + final Optional<String> rotationId = stringAttribute("id", endpointElement); + final Optional<String> containerId = stringAttribute("container-id", endpointElement); + final var regions = new HashSet<String>(); if (containerId.isEmpty()) { throw new IllegalArgumentException("Missing 'container-id' from 'endpoint' tag."); @@ -337,6 +255,10 @@ 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())), @@ -345,44 +267,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(); - return Optional.of(globalServiceId); + if (globalServiceId == null || globalServiceId.isEmpty()) { + return Optional.empty(); + } + else { + return Optional.of(globalServiceId); + } } - private List<DeploymentSpec.ChangeBlocker> readChangeBlockers(Element parent, Element globalBlockersParent) { + private List<DeploymentSpec.ChangeBlocker> readChangeBlockers(Element root) { List<DeploymentSpec.ChangeBlocker> changeBlockers = new ArrayList<>(); - 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); - } + for (Element tag : XML.getChildren(root)) { + if (!blockChangeTag.equals(tag.getTagName())) continue; - private DeploymentSpec.ChangeBlocker readChangeBlocker(Element tag) { - boolean blockVersions = trueOrMissing(tag.getAttribute("version")); - boolean blockRevisions = trueOrMissing(tag.getAttribute("revision")); + 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)); + 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))); + } + return Collections.unmodifiableList(changeBlockers); } - /** Returns true if the given value is "true", or if it is missing */ + /** + * 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 parent, Element fallbackParent) { - Element upgradeElement = XML.getChild(parent, upgradeTag); - if (upgradeElement == null) - upgradeElement = XML.getChild(fallbackParent, upgradeTag); - if (upgradeElement == null) - return DeploymentSpec.UpgradePolicy.defaultPolicy; + private DeploymentSpec.UpgradePolicy readUpgradePolicy(Element root) { + Element upgradeElement = XML.getChild(root, "upgrade"); + if (upgradeElement == null) return DeploymentSpec.UpgradePolicy.defaultPolicy; String policy = upgradeElement.getAttribute("policy"); switch (policy) { @@ -402,14 +324,4 @@ 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; } - - } - } |