diff options
Diffstat (limited to 'config-model-api/src')
5 files changed, 127 insertions, 52 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 index 9d90167a0ef..5f6d47fb586 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -1,9 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.application.api; -import ai.vespa.validation.Validation; -import com.yahoo.config.application.api.DeploymentSpec.RevisionTarget; import com.yahoo.config.provision.AthenzService; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; @@ -14,11 +13,9 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -29,7 +26,6 @@ import static com.yahoo.config.application.api.DeploymentSpec.RevisionChange.whe import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.next; import static com.yahoo.config.provision.Environment.prod; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; /** * The deployment spec for an application instance @@ -54,6 +50,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { private final List<DeploymentSpec.ChangeBlocker> changeBlockers; private final Optional<String> globalServiceId; private final Optional<AthenzService> athenzService; + private final Optional<CloudAccount> cloudAccount; private final Notifications notifications; private final List<Endpoint> endpoints; @@ -67,25 +64,29 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { List<DeploymentSpec.ChangeBlocker> changeBlockers, Optional<String> globalServiceId, Optional<AthenzService> athenzService, + Optional<CloudAccount> cloudAccount, Notifications notifications, List<Endpoint> endpoints, Instant now) { super(steps); - this.name = name; - this.upgradePolicy = upgradePolicy; + this.name = Objects.requireNonNull(name); + this.upgradePolicy = Objects.requireNonNull(upgradePolicy); + Objects.requireNonNull(revisionTarget); + Objects.requireNonNull(revisionChange); this.revisionTarget = require(maxRisk == 0 || revisionTarget == next, revisionTarget, "revision-target must be 'next' when max-risk is specified"); this.revisionChange = require(maxRisk == 0 || revisionChange == whenClear, revisionChange, "revision-change must be 'when-clear' when max-risk is specified"); - this.upgradeRollout = upgradeRollout; + this.upgradeRollout = Objects.requireNonNull(upgradeRollout); this.minRisk = requireAtLeast(minRisk, "minimum risk score", 0); this.maxRisk = require(maxRisk >= minRisk, maxRisk, "maximum risk cannot be less than minimum risk score"); this.maxIdleHours = requireInRange(maxIdleHours, "maximum idle hours", 0, 168); - this.changeBlockers = changeBlockers; - this.globalServiceId = globalServiceId; - this.athenzService = athenzService; - this.notifications = notifications; - this.endpoints = List.copyOf(endpoints); + this.changeBlockers = Objects.requireNonNull(changeBlockers); + this.globalServiceId = Objects.requireNonNull(globalServiceId); + this.athenzService = Objects.requireNonNull(athenzService); + this.cloudAccount = Objects.requireNonNull(cloudAccount); + this.notifications = Objects.requireNonNull(notifications); + this.endpoints = List.copyOf(Objects.requireNonNull(endpoints)); validateZones(new HashSet<>(), new HashSet<>(), this); validateEndpoints(steps(), globalServiceId, this.endpoints); validateChangeBlockers(changeBlockers, now); @@ -224,6 +225,15 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { .or(() -> this.athenzService); } + /** Returns the cloud account to use for given environment and region, if any */ + public Optional<CloudAccount> cloudAccount(Environment environment, RegionName region) { + return zones().stream() + .filter(zone -> zone.concerns(environment, Optional.of(region))) + .findFirst() + .flatMap(DeploymentSpec.DeclaredZone::cloudAccount) + .or(() -> cloudAccount); + } + /** Returns the notification configuration of these instances */ public Notifications notifications() { return notifications; } 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 e5ea65b6d4e..22ffdeb7262 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.collections.Comparables; 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.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; @@ -362,22 +363,27 @@ public class DeploymentSpec { private final boolean active; private final Optional<AthenzService> athenzService; private final Optional<String> testerFlavor; + private final Optional<CloudAccount> cloudAccount; public DeclaredZone(Environment environment) { - this(environment, Optional.empty(), false, Optional.empty(), Optional.empty()); + this(environment, Optional.empty(), false, Optional.empty(), Optional.empty(), Optional.empty()); } public DeclaredZone(Environment environment, Optional<RegionName> region, boolean active, - Optional<AthenzService> athenzService, Optional<String> testerFlavor) { + Optional<AthenzService> athenzService, Optional<String> testerFlavor, + Optional<CloudAccount> cloudAccount) { if (environment != Environment.prod && region.isPresent()) illegal("Non-prod environments cannot specify a region"); if (environment == Environment.prod && region.isEmpty()) illegal("Prod environments must be specified with a region"); - this.environment = environment; - this.region = region; + if (environment != Environment.prod && cloudAccount.isPresent()) + illegal("Non-prod environments cannot specify cloud account"); + this.environment = Objects.requireNonNull(environment); + this.region = Objects.requireNonNull(region); this.active = active; - this.athenzService = athenzService; - this.testerFlavor = testerFlavor; + this.athenzService = Objects.requireNonNull(athenzService); + this.testerFlavor = Objects.requireNonNull(testerFlavor); + this.cloudAccount = Objects.requireNonNull(cloudAccount); } public Environment environment() { return environment; } @@ -392,6 +398,10 @@ public class DeploymentSpec { public Optional<AthenzService> athenzService() { return athenzService; } + public Optional<CloudAccount> cloudAccount() { + return cloudAccount; + } + @Override public List<DeclaredZone> zones() { return Collections.singletonList(this); } 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 96cc33d44b4..8166fb33b78 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 @@ -21,6 +21,7 @@ import com.yahoo.config.application.api.Notifications.When; import com.yahoo.config.application.api.TimeWindow; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; @@ -72,6 +73,8 @@ public class DeploymentSpecXmlReader { private static final String athenzDomainAttribute = "athenz-domain"; private static final String testerFlavorAttribute = "tester-flavor"; private static final String majorVersionAttribute = "major-version"; + private static final String globalServiceIdAttribute = "global-service-id"; + private static final String cloudAccountAttribute = "cloud-account"; private final boolean validate; private final Clock clock; @@ -118,7 +121,7 @@ public class DeploymentSpecXmlReader { List<Step> steps = new ArrayList<>(); List<Endpoint> applicationEndpoints = List.of(); if ( ! containsTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance - steps.addAll(readInstanceContent("default", root, new MutableOptional<>(), root)); + steps.addAll(readInstanceContent("default", root, new HashMap<>(), root)); } else { if (XML.getChildren(root).stream().anyMatch(child -> child.getTagName().equals(prodTag))) @@ -129,9 +132,9 @@ public class DeploymentSpecXmlReader { for (Element child : XML.getChildren(root)) { String tagName = child.getTagName(); if (tagName.equals(instanceTag)) { - steps.addAll(readInstanceContent(child.getAttribute(idAttribute), child, new MutableOptional<>(), root)); + steps.addAll(readInstanceContent(child.getAttribute(idAttribute), child, new HashMap<>(), root)); } else { - steps.addAll(readNonInstanceSteps(child, new MutableOptional<>(), root)); // (No global service id here) + steps.addAll(readNonInstanceSteps(child, new HashMap<>(), root)); // (No global service id here) } } applicationEndpoints = readEndpoints(root, Optional.empty(), steps); @@ -156,7 +159,7 @@ public class DeploymentSpecXmlReader { */ private List<DeploymentInstanceSpec> readInstanceContent(String instanceNameString, Element instanceTag, - MutableOptional<String> globalServiceId, + Map<String, String> prodAttributes, Element parentTag) { if (instanceNameString.isBlank()) illegal("<instance> attribute 'id' must be specified, and not be blank"); @@ -183,7 +186,7 @@ public class DeploymentSpecXmlReader { // Values where there is no default List<Step> steps = new ArrayList<>(); for (Element instanceChild : XML.getChildren(instanceTag)) - steps.addAll(readNonInstanceSteps(instanceChild, globalServiceId, instanceChild)); + steps.addAll(readNonInstanceSteps(instanceChild, prodAttributes, instanceChild)); List<Endpoint> endpoints = readEndpoints(instanceTag, Optional.of(instanceNameString), steps); // Build and return instances with these values @@ -198,43 +201,49 @@ public class DeploymentSpecXmlReader { upgradeRollout, minRisk, maxRisk, maxIdleHours, changeBlockers, - globalServiceId.asOptional(), + Optional.ofNullable(prodAttributes.get(globalServiceIdAttribute)), athenzService, + Optional.ofNullable(prodAttributes.get(cloudAccountAttribute)) + .map(CloudAccount::new), notifications, endpoints, now)) .collect(Collectors.toList()); } - private List<Step> readSteps(Element stepTag, MutableOptional<String> globalServiceId, Element parentTag) { + private List<Step> readSteps(Element stepTag, Map<String, String> prodAttributes, Element parentTag) { if (stepTag.getTagName().equals(instanceTag)) - return new ArrayList<>(readInstanceContent(stepTag.getAttribute(idAttribute), stepTag, globalServiceId, parentTag)); + return new ArrayList<>(readInstanceContent(stepTag.getAttribute(idAttribute), stepTag, prodAttributes, parentTag)); else - return readNonInstanceSteps(stepTag, globalServiceId, parentTag); + return readNonInstanceSteps(stepTag, prodAttributes, parentTag); } // Consume the given tag as 0-N steps. 0 if it is not a step, >1 if it contains multiple nested steps that should be flattened - @SuppressWarnings("fallthrough") - private List<Step> readNonInstanceSteps(Element stepTag, MutableOptional<String> globalServiceId, Element parentTag) { + private List<Step> readNonInstanceSteps(Element stepTag, Map<String, String> prodAttributes, Element parentTag) { Optional<AthenzService> athenzService = mostSpecificAttribute(stepTag, athenzServiceAttribute).map(AthenzService::from); Optional<String> testerFlavor = mostSpecificAttribute(stepTag, testerFlavorAttribute); - if (prodTag.equals(stepTag.getTagName())) - globalServiceId.set(readGlobalServiceId(stepTag)); - else if (readGlobalServiceId(stepTag).isPresent()) - illegal("Attribute 'global-service-id' is only valid on 'prod' tag."); + if (prodTag.equals(stepTag.getTagName())) { + readGlobalServiceId(stepTag).ifPresent(id -> prodAttributes.put(globalServiceIdAttribute, id)); + readCloudAccount(stepTag).ifPresent(account -> prodAttributes.put(cloudAccountAttribute, account)); + } else { + if (readGlobalServiceId(stepTag).isPresent()) illegal("Attribute '" + globalServiceIdAttribute + "' is only valid on 'prod' tag"); + if (!regionTag.equals(stepTag.getTagName()) && readCloudAccount(stepTag).isPresent()) illegal("Attribute '" + cloudAccountAttribute + "' is only valid on 'prod' or 'region' tag"); + } switch (stepTag.getTagName()) { case testTag: if (Stream.iterate(stepTag, Objects::nonNull, Node::getParentNode) - .anyMatch(node -> prodTag.equals(node.getNodeName()))) + .anyMatch(node -> prodTag.equals(node.getNodeName()))) { return List.of(new DeclaredTest(RegionName.from(XML.getValue(stepTag).trim()))); + } + return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor, Optional.empty())); case stagingTag: - return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor)); + return List.of(new DeclaredZone(Environment.from(stepTag.getTagName()), Optional.empty(), false, athenzService, testerFlavor, Optional.empty())); 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()) + .flatMap(child -> readNonInstanceSteps(child, prodAttributes, stepTag).stream()) .collect(Collectors.toList()); case delayTag: return List.of(new Delay(Duration.ofSeconds(longAttribute("hours", stepTag) * 60 * 60 + @@ -242,11 +251,11 @@ public class DeploymentSpecXmlReader { longAttribute("seconds", stepTag)))); case parallelTag: // regions and instances may be nested within return List.of(new ParallelSteps(XML.getChildren(stepTag).stream() - .flatMap(child -> readSteps(child, globalServiceId, parentTag).stream()) + .flatMap(child -> readSteps(child, prodAttributes, parentTag).stream()) .collect(Collectors.toList()))); case stepsTag: // regions and instances may be nested within return List.of(new Steps(XML.getChildren(stepTag).stream() - .flatMap(child -> readSteps(child, globalServiceId, parentTag).stream()) + .flatMap(child -> readSteps(child, prodAttributes, parentTag).stream()) .collect(Collectors.toList()))); case regionTag: return List.of(readDeclaredZone(Environment.prod, athenzService, testerFlavor, stepTag)); @@ -425,13 +434,19 @@ public class DeploymentSpecXmlReader { 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())), - readActive(regionTag), athenzService, testerFlavor); + readActive(regionTag), athenzService, testerFlavor, + readCloudAccount(regionTag).map(CloudAccount::new)); + } + + private Optional<String> readCloudAccount(Element tag) { + return Optional.of(tag.getAttribute(cloudAccountAttribute)) + .filter(account -> !account.isEmpty()); } private Optional<String> readGlobalServiceId(Element environmentTag) { - String globalServiceId = environmentTag.getAttribute("global-service-id"); + String globalServiceId = environmentTag.getAttribute(globalServiceIdAttribute); if (globalServiceId.isEmpty()) return Optional.empty(); - deprecate(environmentTag, List.of("global-service-id"), "See https://cloud.vespa.ai/en/reference/routing#deprecated-syntax"); + deprecate(environmentTag, List.of(globalServiceIdAttribute), "See https://cloud.vespa.ai/en/reference/routing#deprecated-syntax"); return Optional.of(globalServiceId); } @@ -546,14 +561,4 @@ public class DeploymentSpecXmlReader { throw new IllegalArgumentException(message); } - 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; } - - } - } diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 5073c6b9fb2..dfe8b324d1c 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -3,6 +3,7 @@ package com.yahoo.config.application.api; import com.google.common.collect.ImmutableSet; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; @@ -1509,6 +1510,27 @@ public class DeploymentSpecTest { "</deployment>").deployableHashCode()); } + @Test + public void cloudAccount() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <instance id='beta'>" + + " <prod cloud-account='219876543210'>" + + " <region>us-west-1</region>" + + " </prod>" + + " </instance>" + + " <instance id='main'>" + + " <prod cloud-account='012345678912'>" + + " <region>us-east-1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Optional.of(new CloudAccount("219876543210")), spec.requireInstance("beta").cloudAccount(Environment.prod, RegionName.from("us-east-1"))); + assertEquals(Optional.of(new CloudAccount("012345678912")), spec.requireInstance("main").cloudAccount(Environment.prod, RegionName.from("us-west-1"))); + } + private static void assertInvalid(String deploymentSpec, String errorMessagePart) { assertInvalid(deploymentSpec, errorMessagePart, new ManualClock()); } diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java index efed8ecc06c..8baeeb79441 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java @@ -2,6 +2,7 @@ package com.yahoo.config.application.api; import com.google.common.collect.ImmutableSet; +import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import org.junit.Test; @@ -702,6 +703,33 @@ public class DeploymentSpecWithoutInstanceTest { assertEquals(Set.of("us-east", "us-west"), endpointRegions("default", spec)); } + @Test + public void productionSpecWithCloudAccount() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <prod cloud-account='012345678912'>" + + " <region cloud-account='219876543210'>us-east-1</region>" + + " <region>us-west-1</region>" + + " </prod>" + + "</deployment>" + ); + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Optional.of(new CloudAccount("219876543210")), spec.requireInstance("default").cloudAccount(Environment.prod, RegionName.from("us-east-1"))); + assertEquals(Optional.of(new CloudAccount("012345678912")), spec.requireInstance("default").cloudAccount(Environment.prod, RegionName.from("us-west-1"))); + + r = new StringReader( + "<deployment version='1.0'>" + + " <prod>" + + " <region cloud-account='219876543210'>us-east-1</region>" + + " <region>us-west-1</region>" + + " </prod>" + + "</deployment>" + ); + spec = DeploymentSpec.fromXml(r); + assertEquals(Optional.of(new CloudAccount("219876543210")), spec.requireInstance("default").cloudAccount(Environment.prod, RegionName.from("us-east-1"))); + assertEquals(Optional.empty(), spec.requireInstance("default").cloudAccount(Environment.prod, RegionName.from("us-west-1"))); + } + private static Set<String> endpointRegions(String endpointId, DeploymentSpec spec) { return spec.requireInstance("default").endpoints().stream() .filter(endpoint -> endpoint.endpointId().equals(endpointId)) |