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