aboutsummaryrefslogtreecommitdiffstats
path: root/config-model-api
diff options
context:
space:
mode:
authorjonmv <venstad@gmail.com>2023-01-18 13:29:34 +0100
committerjonmv <venstad@gmail.com>2023-01-18 13:29:34 +0100
commit0cb625f5fd5f94b4b8025a9d4f9b546c7ec94d41 (patch)
tree1f4c98f26d6dc4b339ce26c4f3b550e186881300 /config-model-api
parenta395e87d6073c5b2d09dee8fdd2f5fb43f8192d4 (diff)
Revert "Merge pull request #25614 from vespa-engine/revert-25587-jonmv/private-endpoints"
This reverts commit 7b736f0a09444664cff118eac5b28e608632de72, reversing changes made to 6c457e6dd5993ec2ef15177dab4a16e3d3702b85.
Diffstat (limited to 'config-model-api')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java31
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java26
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java1
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java214
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java2
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java282
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java52
7 files changed, 495 insertions, 113 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 fdde4c38fb8..b36c1409459 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
@@ -3,17 +3,23 @@ package com.yahoo.config.application.api;
import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Tags;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.zone.ZoneId;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -26,7 +32,6 @@ import static ai.vespa.validation.Validation.requireInRange;
import static com.yahoo.config.application.api.DeploymentSpec.RevisionChange.whenClear;
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;
/**
* The deployment spec for an application instance
@@ -55,6 +60,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
private final Optional<CloudAccount> cloudAccount;
private final Notifications notifications;
private final List<Endpoint> endpoints;
+ private final Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints;
public DeploymentInstanceSpec(InstanceName name,
Tags tags,
@@ -70,6 +76,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
Optional<CloudAccount> cloudAccount,
Notifications notifications,
List<Endpoint> endpoints,
+ Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints,
Instant now) {
super(steps);
this.name = Objects.requireNonNull(name);
@@ -91,6 +98,9 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
this.cloudAccount = Objects.requireNonNull(cloudAccount);
this.notifications = Objects.requireNonNull(notifications);
this.endpoints = List.copyOf(Objects.requireNonNull(endpoints));
+ Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpointsCopy = new HashMap<>();
+ for (var entry : zoneEndpoints.entrySet()) zoneEndpointsCopy.put(entry.getKey(), Collections.unmodifiableMap(new HashMap<>(entry.getValue())));
+ this.zoneEndpoints = Collections.unmodifiableMap(zoneEndpointsCopy);
validateZones(new HashSet<>(), new HashSet<>(), this);
validateEndpoints(steps(), globalServiceId, this.endpoints);
validateChangeBlockers(changeBlockers, now);
@@ -251,6 +261,25 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
return zones().stream().anyMatch(zone -> zone.concerns(environment, Optional.of(region)));
}
+ /** Returns the zone endpoint specified for the given region, or empty. */
+ Optional<ZoneEndpoint> zoneEndpoint(ZoneId zone, ClusterSpec.Id cluster) {
+ return Optional.ofNullable(zoneEndpoints.get(cluster))
+ .filter(__ -> deploysTo(zone.environment(), zone.region()))
+ .map(zoneEndpoints -> zoneEndpoints.get(zoneEndpoints.containsKey(zone) ? zone : null));
+ }
+
+ /** Returns the zone endpoint data for this instance. */
+ Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints() {
+ return zoneEndpoints;
+ }
+
+ /** The zone endpoints in the given zone, possibly default values. */
+ public Map<ClusterSpec.Id, ZoneEndpoint> zoneEndpoints(ZoneId zone) {
+ return zoneEndpoints.keySet().stream()
+ .collect(Collectors.toMap(cluster -> cluster,
+ cluster -> zoneEndpoint(zone, cluster).orElse(ZoneEndpoint.defaultEndpoint)));
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java
index 6c519a4656e..d731e09d4e4 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
@@ -6,16 +6,21 @@ 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.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.zone.ZoneId;
import java.io.Reader;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -47,7 +52,7 @@ public class DeploymentSpec {
private final List<Step> steps;
- // Attributes which can be set on the root tag and which must be available outside of any particular instance
+ // Attributes which can be set on the root tag and which must be available outside any particular instance
private final Optional<Integer> majorVersion;
private final Optional<AthenzDomain> athenzDomain;
private final Optional<AthenzService> athenzService;
@@ -145,6 +150,10 @@ public class DeploymentSpec {
illegal(prefix + "targets undeclared region '" + target.region() +
"' in instance '" + target.instance() + "'");
}
+ if (instance.get().zoneEndpoint(ZoneId.from(Environment.prod, target.region()), ClusterSpec.Id.from(endpoint.containerId()))
+ .map(zoneEndpoint -> ! zoneEndpoint.isPublicEndpoint()).orElse(false))
+ illegal(prefix + "targets '" + target.region().value() + "' in '" + target.instance().value() +
+ "', but its zone endpoint has 'enabled' set to 'false'");
}
}
}
@@ -175,6 +184,21 @@ public class DeploymentSpec {
/** Cloud account set on the deployment root; see discussion for {@link #athenzService}. */
public Optional<CloudAccount> cloudAccount() { return cloudAccount; }
+ /**
+ * Returns the most specific zone endpoint, where specificity is given, in decreasing order:
+ * 1. The given instance has declared a zone endpoint for the cluster, for the given region.
+ * 2. The given instance has declared a universal zone endpoint for the cluster.
+ * 3. The application has declared a zone endpoint for the cluster, for the given region.
+ * 4. The application has declared a universal zone endpoint for the cluster.
+ * 5. None of the above apply, and the default of a publicly visible endpoint is used.
+ */
+ public ZoneEndpoint zoneEndpoint(InstanceName instance, ZoneId zone, ClusterSpec.Id cluster) {
+ // TODO: look up endpoints from <dev> tag, or so, if we're to support non-prod settings.
+ if (zone.environment() != Environment.prod) return ZoneEndpoint.defaultEndpoint;
+ return instance(instance).flatMap(spec -> spec.zoneEndpoint(zone, cluster))
+ .orElse(ZoneEndpoint.defaultEndpoint);
+ }
+
/** Returns the XML form of this spec, or null if it was not created by fromXml, nor is empty */
public String xmlForm() { return xmlForm; }
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
index 571cc3c7d5c..b4be99ad20b 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java
@@ -24,6 +24,7 @@ public enum ValidationId {
skipOldConfigModels("skip-old-config-models"), // Internal use
accessControl("access-control"), // Internal use, used in zones where there should be no access-control
globalEndpointChange("global-endpoint-change"), // Changing global endpoints
+ zoneEndpointChange("zone-endpoint-change"), // Changing zone (possibly private) endpoint settings
redundancyIncrease("redundancy-increase"), // Increasing redundancy - may easily cause feed blocked
redundancyOne("redundancy-one"), // redundancy=1 requires a validation override on first deployment
pagedSettingRemoval("paged-setting-removal"), // May cause content nodes to run out of memory
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 8182e697e7e..fb6d834f783 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
@@ -15,6 +15,8 @@ import com.yahoo.config.application.api.DeploymentSpec.Steps;
import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
import com.yahoo.config.application.api.DeploymentSpec.UpgradeRollout;
import com.yahoo.config.application.api.Endpoint;
+import com.yahoo.config.application.api.Endpoint.Level;
+import com.yahoo.config.application.api.Endpoint.Target;
import com.yahoo.config.application.api.Notifications;
import com.yahoo.config.application.api.Notifications.Role;
import com.yahoo.config.application.api.Notifications.When;
@@ -22,10 +24,15 @@ 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.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Tags;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.io.IOUtils;
import com.yahoo.text.XML;
import org.w3c.dom.Element;
@@ -39,16 +46,20 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
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;
+import static java.util.Comparator.comparingInt;
+
/**
* @author bratseth
*/
@@ -123,7 +134,7 @@ public class DeploymentSpecXmlReader {
return DeploymentSpec.empty;
List<Step> steps = new ArrayList<>();
- List<Endpoint> applicationEndpoints = List.of();
+ List<Endpoint> applicationEndpoints = new ArrayList<>();
if ( ! containsTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance
steps.addAll(readInstanceContent("default", root, new HashMap<>(), root));
}
@@ -141,7 +152,7 @@ public class DeploymentSpecXmlReader {
steps.addAll(readNonInstanceSteps(child, new HashMap<>(), root)); // (No global service id here)
}
}
- applicationEndpoints = readEndpoints(root, Optional.empty(), steps);
+ readEndpoints(root, Optional.empty(), steps, applicationEndpoints, Map.of());
}
return new DeploymentSpec(steps,
@@ -190,11 +201,13 @@ public class DeploymentSpecXmlReader {
Notifications notifications = readNotifications(instanceElement, parentTag);
// Values where there is no default
- Tags tags = XML.attribute(tagsTag, instanceElement).map(value -> Tags.fromString(value)).orElse(Tags.empty());
+ Tags tags = XML.attribute(tagsTag, instanceElement).map(Tags::fromString).orElse(Tags.empty());
List<Step> steps = new ArrayList<>();
for (Element instanceChild : XML.getChildren(instanceElement))
steps.addAll(readNonInstanceSteps(instanceChild, prodAttributes, instanceChild));
- List<Endpoint> endpoints = readEndpoints(instanceElement, Optional.of(instanceNameString), steps);
+ List<Endpoint> endpoints = new ArrayList<>();
+ Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints = new LinkedHashMap<>();
+ readEndpoints(instanceElement, Optional.of(instanceNameString), steps, endpoints, zoneEndpoints);
// Build and return instances with these values
Instant now = clock.instant();
@@ -214,6 +227,7 @@ public class DeploymentSpecXmlReader {
cloudAccount,
notifications,
endpoints,
+ zoneEndpoints,
now))
.toList();
}
@@ -306,19 +320,54 @@ public class DeploymentSpecXmlReader {
return Notifications.of(emailAddresses, emailRoles);
}
- private List<Endpoint> readEndpoints(Element parent, Optional<String> instance, List<Step> steps) {
+ private void readEndpoints(Element parent, Optional<String> instance, List<Step> steps, List<Endpoint> endpoints,
+ Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints) {
var endpointsElement = XML.getChild(parent, endpointsTag);
- if (endpointsElement == null) return List.of();
+ if (endpointsElement == null) return;
Endpoint.Level level = instance.isEmpty() ? Endpoint.Level.application : Endpoint.Level.instance;
- Map<String, Endpoint> endpoints = new LinkedHashMap<>();
- for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) {
- String endpointId = stringAttribute("id", endpointElement).orElse(Endpoint.DEFAULT_ID);
+ Map<String, Endpoint> endpointsById = new LinkedHashMap<>();
+ Map<String, Map<RegionName, List<ZoneEndpoint>>> endpointsByZone = new LinkedHashMap<>();
+ XML.getChildren(endpointsElement, endpointTag).stream() // Read zone settings first.
+ .sorted(comparingInt(endpoint -> getZoneEndpointType(endpoint, level).isPresent() ? 0 : 1))
+ .forEach(endpointElement -> {
String containerId = requireStringAttribute("container-id", endpointElement);
+ Optional<String> endpointId = stringAttribute("id", endpointElement);
+ Optional<String> zoneEndpointType = getZoneEndpointType(endpointElement, level);
String msgPrefix = (level == Endpoint.Level.application ? "Application-level" : "Instance-level") +
- " endpoint '" + endpointId + "': ";
+ " endpoint '" + endpointId.orElse(Endpoint.DEFAULT_ID) + "': ";
+
+ if (zoneEndpointType.isPresent() && endpointId.isPresent())
+ illegal(msgPrefix + "cannot declare 'id' with type 'zone' or 'private'");
+
String invalidChild = level == Endpoint.Level.application ? "region" : "instance";
- if (!XML.getChildren(endpointElement, invalidChild).isEmpty()) illegal(msgPrefix + "invalid element '" + invalidChild + "'");
+ if ( ! XML.getChildren(endpointElement, invalidChild).isEmpty())
+ illegal(msgPrefix + "invalid element '" + invalidChild + "'");
+
+ boolean enabled = XML.attribute("enabled", endpointElement)
+ .map(value -> {
+ if (zoneEndpointType.isEmpty() || ! zoneEndpointType.get().equals("zone"))
+ illegal(msgPrefix + "only endpoints of type 'zone' can specify 'enabled'");
+
+ return switch (value) {
+ case "true" -> true;
+ case "false" -> false;
+ default -> throw new IllegalArgumentException(msgPrefix + "invalid 'enabled' value; must be 'true' or 'false'");
+ };
+ }).orElse(true);
+
+ List<AllowedUrn> allowedUrns = new ArrayList<>();
+ for (var allow : XML.getChildren(endpointElement, "allow")) {
+ if (zoneEndpointType.isEmpty() || ! zoneEndpointType.get().equals("private"))
+ illegal(msgPrefix + "only endpoints of type 'private' can specify 'allow' children");
+
+ switch (requireStringAttribute("with", allow)) {
+ case "aws-private-link" -> allowedUrns.add(new AllowedUrn(AccessType.awsPrivateLink, requireStringAttribute("arn", allow)));
+ case "gcp-service-connect" -> allowedUrns.add(new AllowedUrn(AccessType.gcpServiceConnect, requireStringAttribute("project", allow)));
+ default -> illegal("Private endpoint for container-id '" + containerId + "': " +
+ "invalid attribute 'with': '" + requireStringAttribute("with", allow) + "'");
+ }
+ }
List<Endpoint.Target> targets = new ArrayList<>();
if (level == Endpoint.Level.application) {
@@ -345,33 +394,132 @@ public class DeploymentSpecXmlReader {
if (weightSum == 0) illegal(msgPrefix + "sum of all weights must be positive, got " + weightSum);
} else {
if (stringAttribute("region", endpointElement).isPresent()) illegal(msgPrefix + "invalid 'region' attribute");
+ Set<RegionName> regions = new LinkedHashSet<>();
for (var regionElement : XML.getChildren(endpointElement, "region")) {
String region = regionElement.getTextContent();
- if (region == null || region.isBlank()) illegal(msgPrefix + "empty 'region' element");
- targets.add(new Endpoint.Target(RegionName.from(region),
- InstanceName.from(instance.get()),
- 1));
+ if (region == null || region.isBlank())
+ illegal(msgPrefix + "empty 'region' element");
+ if ( zoneEndpointType.isEmpty()
+ && Stream.of(RegionName.from(region), null)
+ .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get)
+ .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream())
+ .anyMatch(endpoint -> ! endpoint.isPublicEndpoint()))
+ illegal(msgPrefix + "targets zone endpoint in '" + region + "' with 'enabled' set to 'false'");
+ if ( ! regions.add(RegionName.from(region)))
+ illegal(msgPrefix + "duplicate 'region' element: '" + region + "'");
+ }
+
+ if (zoneEndpointType.isPresent()) {
+ if (regions.isEmpty()) regions.add(null);
+ ZoneEndpoint endpoint = switch (zoneEndpointType.get()) {
+ case "zone" -> new ZoneEndpoint(enabled, false, List.of());
+ case "private" -> new ZoneEndpoint(true, true, allowedUrns); // Doesn't turn off public visibility.
+ default -> throw new IllegalArgumentException("unsupported zone endpoint type '" + zoneEndpointType.get() + "'");
+ };
+ for (RegionName region : regions) endpointsByZone.computeIfAbsent(containerId, __ -> new LinkedHashMap<>())
+ .computeIfAbsent(region, __ -> new ArrayList<>())
+ .add(endpoint);
+ }
+ else {
+ if (regions.isEmpty()) {
+ // No explicit targets given for instance level endpoint. Include all declared, enabled regions by default
+ List<RegionName> declared =
+ steps.stream()
+ .filter(step -> step.concerns(Environment.prod))
+ .flatMap(step -> step.zones().stream())
+ .flatMap(zone -> zone.region().stream())
+ .toList();
+ if (declared.isEmpty()) illegal(msgPrefix + "no declared regions to target");
+
+ declared.stream().filter(region -> Stream.of(region, null)
+ .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get)
+ .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream())
+ .allMatch(ZoneEndpoint::isPublicEndpoint))
+ .forEach(regions::add);
+ }
+ if (regions.isEmpty()) illegal(msgPrefix + "all eligible zone endpoints have 'enabled' set to 'false'");
+ InstanceName instanceName = instance.map(InstanceName::from).get();
+ for (RegionName region : regions) targets.add(new Target(region, instanceName, 1));
}
- }
- if (targets.isEmpty() && level == Endpoint.Level.instance) {
- // No explicit targets given for instance level endpoint. Include all declared regions by default
- InstanceName instanceName = instance.map(InstanceName::from).get();
- steps.stream()
- .filter(step -> step.concerns(Environment.prod))
- .flatMap(step -> step.zones().stream())
- .flatMap(zone -> zone.region().stream())
- .distinct()
- .map(region -> new Endpoint.Target(region, instanceName, 1))
- .forEach(targets::add);
}
- Endpoint endpoint = new Endpoint(endpointId, containerId, level, targets);
- if (endpoints.containsKey(endpoint.endpointId())) {
- illegal("Endpoint ID '" + endpoint.endpointId() + "' is specified multiple times");
+ if (zoneEndpointType.isEmpty()) {
+ Endpoint endpoint = new Endpoint(endpointId.orElse(Endpoint.DEFAULT_ID), containerId, level, targets);
+ if (endpointsById.containsKey(endpoint.endpointId())) {
+ illegal("Endpoint ID '" + endpoint.endpointId() + "' is specified multiple times");
+ }
+ endpointsById.put(endpoint.endpointId(), endpoint);
}
- endpoints.put(endpoint.endpointId(), endpoint);
- }
- return List.copyOf(endpoints.values());
+ });
+ endpoints.addAll(endpointsById.values());
+ validateAndConsolidate(endpointsByZone, zoneEndpoints);
+ }
+
+ static void validateAndConsolidate(Map<String, Map<RegionName, List<ZoneEndpoint>>> in, Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> out) {
+ in.forEach((cluster, regions) -> {
+ List<ZoneEndpoint> wildcards = regions.remove(null);
+ ZoneEndpoint wildcardZoneEndpoint = null;
+ ZoneEndpoint wildcardPrivateEndpoint = null;
+ if (wildcards != null) {
+ for (ZoneEndpoint endpoint : wildcards) {
+ if (endpoint.isPrivateEndpoint()) {
+ if (wildcardPrivateEndpoint != null) illegal("Multiple private endpoints (for all regions) declared for " +
+ "container id '" + cluster + "'");
+ wildcardPrivateEndpoint = endpoint;
+ }
+ else {
+ if (wildcardZoneEndpoint != null) illegal("Multiple zone endpoints (for all regions) declared " +
+ "for container id '" + cluster + "'");
+ wildcardZoneEndpoint = endpoint;
+ }
+ }
+ }
+ for (RegionName region : regions.keySet()) {
+ ZoneEndpoint zoneEndpoint = null;
+ ZoneEndpoint privateEndpoint = null;
+ for (ZoneEndpoint endpoint : regions.getOrDefault(region, List.of())) {
+ if (endpoint.isPrivateEndpoint()) {
+ if (privateEndpoint != null) illegal("Multiple private endpoints declared for " +
+ "container id '" + cluster + "' in region '" + region + "'");
+ privateEndpoint = endpoint;
+ }
+ else {
+ if (zoneEndpoint != null) illegal("Multiple zone endpoints (without regions) declared " +
+ "for container id '" + cluster + "' in region '" + region + "'");
+ zoneEndpoint = endpoint;
+ }
+ }
+ if (wildcardZoneEndpoint != null && zoneEndpoint != null) illegal("Zone endpoint for container id '" + cluster + "' declared " +
+ "both with region '" + region + "', and for all regions.");
+ if (wildcardPrivateEndpoint != null && privateEndpoint != null) illegal("Private endpoint for container id '" + cluster + "' declared " +
+ "both with region '" + region + "', and for all regions.");
+
+ if (zoneEndpoint == null) zoneEndpoint = wildcardZoneEndpoint;
+ if (privateEndpoint == null) privateEndpoint = wildcardPrivateEndpoint;
+
+ // Gosh, we made it here! Now we'll combine the settings for zone and private types into one ZoneEndpoint to rule them all.
+ out.computeIfAbsent(ClusterSpec.Id.from(cluster), __ -> new LinkedHashMap<>())
+ .put(ZoneId.from(Environment.prod, region), new ZoneEndpoint(zoneEndpoint == null || zoneEndpoint.isPublicEndpoint(),
+ privateEndpoint != null,
+ privateEndpoint != null ? privateEndpoint.allowedUrns() : List.of()));
+ }
+ out.computeIfAbsent(ClusterSpec.Id.from(cluster), __ -> new LinkedHashMap<>())
+ .put(null, new ZoneEndpoint(wildcardZoneEndpoint == null || wildcardZoneEndpoint.isPublicEndpoint(),
+ wildcardPrivateEndpoint != null,
+ wildcardPrivateEndpoint != null ? wildcardPrivateEndpoint.allowedUrns() : List.of()));
+ });
+ }
+
+ /** Returns endpoint type if a private or zone type endpoint, throws if invalid, or otherwise returns empty (global, application). */
+ static Optional<String> getZoneEndpointType(Element endpoint, Level level) {
+ Optional<String> type = XML.attribute("type", endpoint);
+ if (type.isPresent() && ! List.of("zone", "private", "global", "application").contains(type.get()))
+ illegal("Illegal endpoint type '" + type.get() + "'");
+
+ String implied = switch (level) { case instance -> "global"; case application -> "application"; };
+ if (type.isEmpty() || type.get().equals(implied)) return Optional.empty();
+ if (level == Level.instance && (type.get().equals("zone") || type.get().equals("private"))) return type;
+ throw new IllegalArgumentException("Endpoints at " + level + " level cannot be of type '" + type.get() + "'");
}
/**
diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java
index 99bf768f67e..b7969267328 100644
--- a/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java
+++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java
@@ -26,7 +26,7 @@ public class ApplicationClusterEndpoint {
", routingMethod=" + routingMethod +
", weight=" + weight +
", hostNames=" + hostNames +
- ", clusterId='" + clusterId + '\'' +
+ ", clusterId='" + clusterId + "'" +
'}';
}
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 96cd4810ec4..355ce651c34 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
@@ -6,10 +6,14 @@ import com.yahoo.config.application.api.Endpoint.Level;
import com.yahoo.config.application.api.Endpoint.Target;
import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Tags;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.test.ManualClock;
import org.junit.Test;
@@ -19,6 +23,7 @@ import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -27,9 +32,12 @@ 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 com.yahoo.config.provision.zone.ZoneId.defaultId;
+import static com.yahoo.config.provision.zone.ZoneId.from;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -1183,15 +1191,16 @@ public class DeploymentSpecTest {
@Test
public void customTesterFlavor() {
- DeploymentSpec spec = DeploymentSpec.fromXml("<deployment>" +
- " <instance id='default'>" +
- " <test tester-flavor=\"d-1-4-20\" />" +
- " <staging />" +
- " <prod tester-flavor=\"d-2-8-50\">" +
- " <region active=\"false\">us-north-7</region>" +
- " </prod>" +
- " </instance>" +
- "</deployment>");
+ DeploymentSpec spec = DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id='default'>
+ <test tester-flavor="d-1-4-20" />
+ <staging />
+ <prod tester-flavor="d-2-8-50">
+ <region active="false">us-north-7</region>
+ </prod>
+ </instance>
+ </deployment>""");
assertEquals(Optional.of("d-1-4-20"), spec.requireInstance("default").steps().get(0).zones().get(0).testerFlavor());
assertEquals(Optional.empty(), spec.requireInstance("default").steps().get(1).zones().get(0).testerFlavor());
assertEquals(Optional.of("d-2-8-50"), spec.requireInstance("default").steps().get(2).zones().get(0).testerFlavor());
@@ -1199,39 +1208,55 @@ public class DeploymentSpecTest {
@Test
public void noEndpoints() {
- assertEquals(Collections.emptyList(),
- DeploymentSpec.fromXml("<deployment>" +
- " <instance id='default'/>" +
- "</deployment>").requireInstance("default").endpoints());
+ DeploymentSpec spec = DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id='default'/>
+ </deployment>
+ """);
+ assertEquals(Collections.emptyList(), spec.requireInstance("default").endpoints());
+ assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.defaultName(),
+ defaultId(),
+ ClusterSpec.Id.from("cluster")));
}
@Test
public void emptyEndpoints() {
- var spec = DeploymentSpec.fromXml("<deployment>" +
- " <instance id='default'>" +
- " <endpoints/>" +
- " </instance>" +
- "</deployment>");
+ var spec = DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id='default'>
+ <endpoints/>
+ </instance>
+ </deployment>""");
assertEquals(List.of(), spec.requireInstance("default").endpoints());
+ assertEquals(ZoneEndpoint.defaultEndpoint, spec.zoneEndpoint(InstanceName.defaultName(),
+ defaultId(),
+ ClusterSpec.Id.from("cluster")));
}
@Test
public void someEndpoints() {
- var spec = DeploymentSpec.fromXml("" +
- "<deployment>" +
- " <instance id='default'>" +
- " <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>" +
- " </instance>" +
- "</deployment>");
+ var spec = DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id='default'>
+ <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" />
+ <endpoint container-id='bax' type='zone' enabled='true' />
+ <endpoint container-id='froz' type='zone' enabled='false' />
+ <endpoint container-id='froz' type='private'>
+ <region>us-east</region>
+ <allow with='aws-private-link' arn='barn' />
+ <allow with='gcp-service-connect' project='nine' />
+ </endpoint>
+ </endpoints>
+ </instance>
+ </deployment>""");
assertEquals(
List.of("foo", "nalle", "default"),
@@ -1244,18 +1269,59 @@ public class DeploymentSpecTest {
);
assertEquals(List.of(RegionName.from("us-east")), spec.requireInstance("default").endpoints().get(0).regions());
+
+ var zone = from(Environment.prod, RegionName.from("us-east"));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("custom"), zone, ClusterSpec.Id.from("bax")));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("default"), defaultId(), ClusterSpec.Id.from("bax")));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("bax")));
+
+ assertEquals(new ZoneEndpoint(false, true, List.of(new AllowedUrn(AccessType.awsPrivateLink, "barn"),
+ new AllowedUrn(AccessType.gcpServiceConnect, "nine"))),
+ spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("froz")));
}
@Test
public void invalidEndpoints() {
- assertInvalidEndpoints("<endpoint id='FOO' container-id='qrs'/>"); // Uppercase
- assertInvalidEndpoints("<endpoint id='123' container-id='qrs'/>"); // Starting with non-character
- assertInvalidEndpoints("<endpoint id='foo!' container-id='qrs'/>"); // Non-alphanumeric
- assertInvalidEndpoints("<endpoint id='foo.bar' container-id='qrs'/>");
- assertInvalidEndpoints("<endpoint id='foo--bar' container-id='qrs'/>"); // Multiple consecutive dashes
- assertInvalidEndpoints("<endpoint id='foo-' container-id='qrs'/>"); // Trailing dash
- assertInvalidEndpoints("<endpoint id='foooooooooooo' container-id='qrs'/>"); // Too long
- assertInvalidEndpoints("<endpoint id='foo' container-id='qrs'/><endpoint id='foo' container-id='qrs'/>"); // Duplicate
+ assertInvalidEndpoints("<endpoint id='FOO' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'FOO'");
+ assertInvalidEndpoints("<endpoint id='123' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got '123'");
+ assertInvalidEndpoints("<endpoint id='foo!' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo!'");
+ assertInvalidEndpoints("<endpoint id='foo.bar' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo.bar'");
+ assertInvalidEndpoints("<endpoint id='foo--bar' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo--bar'");
+ assertInvalidEndpoints("<endpoint id='foo-' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foo-'");
+ assertInvalidEndpoints("<endpoint id='foooooooooooo' container-id='qrs'/>",
+ "Endpoint ID must be all lowercase, alphanumeric, with no consecutive dashes, of length 1 to 12, and begin with a character; but got 'foooooooooooo'");
+
+ assertInvalidEndpoints("<endpoint id='foo' container-id='qrs'/><endpoint id='foo' container-id='qrs'/>",
+ "Endpoint ID 'foo' is specified multiple times");
+ assertInvalidEndpoints("<endpoint id='default' type='zone' container-id='foo' />",
+ "Instance-level endpoint 'default': cannot declare 'id' with type 'zone' or 'private'");
+ assertInvalidEndpoints("<endpoint id='default' type='private' container-id='foo' />",
+ "Instance-level endpoint 'default': cannot declare 'id' with type 'zone' or 'private'");
+ assertInvalidEndpoints("<endpoint type='zone' />",
+ "Missing required attribute 'container-id' in 'endpoint'");
+ assertInvalidEndpoints("<endpoint type='private' />",
+ "Missing required attribute 'container-id' in 'endpoint'");
+ assertInvalidEndpoints("<endpoint container-id='foo' type='zone'><allow /></endpoint>",
+ "Instance-level endpoint 'default': only endpoints of type 'private' can specify 'allow' children");
+ assertInvalidEndpoints("<endpoint type='private' container-id='foo' enabled='true' />",
+ "Instance-level endpoint 'default': only endpoints of type 'zone' can specify 'enabled'");
+ assertInvalidEndpoints("<endpoint type='zone' container-id='qrs'/><endpoint type='zone' container-id='qrs'/>",
+ "Multiple zone endpoints (for all regions) declared for container id 'qrs'");
+ assertInvalidEndpoints("<endpoint type='private' container-id='qrs'><region>us</region></endpoint>" +
+ "<endpoint type='private' container-id='qrs'><region>us</region></endpoint>",
+ "Multiple private endpoints declared for container id 'qrs' in region 'us'");
+ assertInvalidEndpoints("<endpoint type='zone' container-id='qrs' />" +
+ "<endpoint type='zone' container-id='qrs'><region>us</region></endpoint>",
+ "Zone endpoint for container id 'qrs' declared both with region 'us', and for all regions.");
}
@Test
@@ -1271,25 +1337,44 @@ public class DeploymentSpecTest {
@Test
public void endpointDefaultRegions() {
- var spec = DeploymentSpec.fromXml("<deployment>" +
- " <instance id='default'>" +
- " <prod>" +
- " <region>us-east</region>" +
- " <region>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>" +
- " </instance>" +
- "</deployment>");
+ var spec = DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id='default'>
+ <prod>
+ <region>us-east</region>
+ <region>us-west</region>
+ </prod>
+ <endpoints>
+ <endpoint id="foo" container-id="bar">
+ <region>us-east</region>
+ </endpoint>
+ <endpoint container-id="bar" type='private'>
+ <region>us-east</region>
+ </endpoint>
+ <endpoint id="nalle" container-id="frosk" />
+ <endpoint container-id="quux" />
+ <endpoint container-id="quux" type='private' />
+ </endpoints>
+ </instance>
+ </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));
+ assertEquals(new ZoneEndpoint(true, true, List.of()),
+ spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-east"), ClusterSpec.Id.from("bar")));
+ assertEquals(new ZoneEndpoint(true, false, List.of()),
+ spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-west"), ClusterSpec.Id.from("bar")));
+ assertEquals(new ZoneEndpoint(true, true, List.of()),
+ spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-east"), ClusterSpec.Id.from("quux")));
+ assertEquals(new ZoneEndpoint(true, true, List.of()),
+ spec.zoneEndpoint(InstanceName.from("default"), from("prod", "us-west"), ClusterSpec.Id.from("quux")));
+ assertEquals(new HashSet<>() {{ add(null); add(from("prod", "us-east")); }},
+ spec.requireInstance("default").zoneEndpoints().get(ClusterSpec.Id.from("bar")).keySet());
+ assertEquals(new HashSet<>() {{ add(null); }},
+ spec.requireInstance("default").zoneEndpoints().get(ClusterSpec.Id.from("quux")).keySet());
+ assertEquals(Set.of(ClusterSpec.Id.from("bar"), ClusterSpec.Id.from("quux")),
+ spec.requireInstance("default").zoneEndpoints().keySet());
}
@Test
@@ -1302,14 +1387,16 @@ public class DeploymentSpecTest {
<region active="true">us-west</region>
</prod>
<endpoints>
- <endpoint id="foo" container-id="bar" %s>
+ <endpoint container-id="bar" %s>
%s
</endpoint>
</endpoints>
</instance>
</deployment>""";
- assertInvalid(String.format(xmlForm, "region='us-east'", "<region>us-east</region>"), "Instance-level endpoint 'foo': invalid 'region' attribute");
- assertInvalid(String.format(xmlForm, "", "<instance>us-east</instance>"), "Instance-level endpoint 'foo': invalid element 'instance'");
+ assertInvalid(String.format(xmlForm, "id='foo' region='us-east'", "<region>us-east</region>"), "Instance-level endpoint 'foo': invalid 'region' attribute");
+ assertInvalid(String.format(xmlForm, "id='foo'", "<instance>us-east</instance>"), "Instance-level endpoint 'foo': invalid element 'instance'");
+ assertInvalid(String.format(xmlForm, "type='zone'", "<instance>us-east</instance>"), "Instance-level endpoint 'default': invalid element 'instance'");
+ assertInvalid(String.format(xmlForm, "type='private'", "<instance>us-east</instance>"), "Instance-level endpoint 'default': invalid element 'instance'");
}
@Test
@@ -1343,6 +1430,73 @@ public class DeploymentSpecTest {
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='foo'", "", "main", ""), "Application-level endpoint 'foo': invalid weight value 'foo'");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='1'", "", "main", "<region>us-east-3</region>"), "Application-level endpoint 'foo': invalid element 'region'");
assertInvalid(String.format(xmlForm, "region='us-west-1'", "weight='0'", "", "main", ""), "Application-level endpoint 'foo': sum of all weights must be positive, got 0");
+ assertInvalid(String.format(xmlForm, "type='zone'", "weight='1'", "", "main", ""), "Endpoints at application level cannot be of type 'zone'");
+ assertInvalid(String.format(xmlForm, "type='private'", "weight='1'", "", "main", ""), "Endpoints at application level cannot be of type 'private'");
+ }
+
+ @Test
+ public void cannotTargetDisabledEndpoints() {
+ assertEquals("Instance-level endpoint 'default': all eligible zone endpoints have 'enabled' set to 'false'",
+ assertThrows(IllegalArgumentException.class,
+ () -> DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id="default">
+ <prod>
+ <region>us</region>
+ <region>eu</region>
+ </prod>
+ <endpoints>
+ <endpoint container-id='id' />
+ <endpoint type='zone' container-id='id' enabled='false' />
+ </endpoints>
+ </instance>
+ </deployment>
+ """))
+ .getMessage());
+
+ assertEquals("Instance-level endpoint 'default': targets zone endpoint in 'us' with 'enabled' set to 'false'",
+ assertThrows(IllegalArgumentException.class,
+ () -> DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id="default">
+ <prod>
+ <region>us</region>
+ <region>eu</region>
+ </prod>
+ <endpoints>
+ <endpoint container-id='id'>
+ <region>us</region>
+ </endpoint>
+ <endpoint type='zone' container-id='id' enabled='false' />
+ </endpoints>
+ </instance>
+ </deployment>
+ """))
+ .getMessage());
+
+ assertEquals("Application-level endpoint 'default': targets 'us' in 'default', but its zone endpoint has 'enabled' set to 'false'",
+ assertThrows(IllegalArgumentException.class,
+ () -> DeploymentSpec.fromXml("""
+ <deployment>
+ <instance id="default">
+ <prod>
+ <region>us</region>
+ <region>eu</region>
+ </prod>
+ <endpoints>
+ <endpoint type='zone' container-id='id' enabled='false'>
+ <region>us</region>
+ </endpoint>
+ </endpoints>
+ </instance>
+ <endpoints>
+ <endpoint container-id='id' region='us'>
+ <instance weight='1'>default</instance>
+ </endpoint>
+ </endpoints>
+ </deployment>
+ """))
+ .getMessage());
}
@Test
@@ -1648,11 +1802,11 @@ public class DeploymentSpecTest {
}
}
- private static void assertInvalidEndpoints(String endpointsBody) {
- try {
- endpointIds(endpointsBody);
- fail("Expected exception for input '" + endpointsBody + "'");
- } catch (IllegalArgumentException ignored) {}
+ private static void assertInvalidEndpoints(String endpointsBody, String error) {
+ assertEquals(error,
+ assertThrows(IllegalArgumentException.class,
+ () -> endpointIds(endpointsBody))
+ .getMessage());
}
private static Set<String> endpointRegions(String endpointId, DeploymentSpec spec) {
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 6ff5616a80f..38410cc5b37 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
@@ -3,8 +3,13 @@ package com.yahoo.config.application.api;
import com.google.common.collect.ImmutableSet;
import com.yahoo.config.provision.CloudAccount;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import org.junit.Test;
import java.io.StringReader;
@@ -20,6 +25,8 @@ 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 com.yahoo.config.provision.zone.ZoneId.defaultId;
+import static com.yahoo.config.provision.zone.ZoneId.from;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -654,19 +661,26 @@ public class DeploymentSpecWithoutInstanceTest {
@Test
public void someEndpoints() {
- 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>");
+ 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" />
+ <endpoint container-id='bax' type='zone' enabled='true' />
+ <endpoint container-id='froz' type='zone' enabled='false' />
+ <endpoint container-id='froz' type='private'>
+ <region>us-east</region>
+ <allow with='aws-private-link' arn='barn' />
+ <allow with='gcp-service-connect' project='nine' />
+ </endpoint>
+ </endpoints>
+ </deployment>""");
assertEquals(
List.of("foo", "nalle", "default"),
@@ -679,6 +693,18 @@ public class DeploymentSpecWithoutInstanceTest {
);
assertEquals(List.of(RegionName.from("us-east")), spec.requireInstance("default").endpoints().get(0).regions());
+
+ var zone = from(Environment.prod, RegionName.from("us-east"));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("custom"), zone, ClusterSpec.Id.from("bax")));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("default"), defaultId(), ClusterSpec.Id.from("bax")));
+ assertEquals(ZoneEndpoint.defaultEndpoint,
+ spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("bax")));
+
+ assertEquals(new ZoneEndpoint(false, true, List.of(new AllowedUrn(AccessType.awsPrivateLink, "barn"),
+ new AllowedUrn(AccessType.gcpServiceConnect, "nine"))),
+ spec.zoneEndpoint(InstanceName.from("default"), zone, ClusterSpec.Id.from("froz")));
}
@Test