summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjonmv <venstad@gmail.com>2023-01-16 15:41:26 +0100
committerjonmv <venstad@gmail.com>2023-01-17 15:49:42 +0100
commit12ef34b0204e5acee25655139e7f6e79cefe983b (patch)
tree96c497e68be57d41f62ba9ae00b60042cf2ce32d
parent69d0f324263f0075a283b66bca6fab2a12b2b66e (diff)
Parse, validate and use new zone endpoint syntax
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java22
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java22
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java176
-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.java167
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecWithoutInstanceTest.java52
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java2
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java8
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java25
-rw-r--r--config-model/src/main/resources/schema/deployment.rnc11
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java105
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java10
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java28
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/LoadBalancerSettings.java20
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/ZoneEndpoint.java131
-rw-r--r--config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java55
-rw-r--r--config-provisioning/src/test/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializerTest.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java8
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java8
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java8
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java50
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java28
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java14
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java12
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/load-balancers.json7
-rw-r--r--vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java4
35 files changed, 763 insertions, 255 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..f72f09232ab 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,16 @@ 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 the default, or {@code null}. */
+ 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; }
+
@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..7d3014c15a3 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;
@@ -175,6 +180,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/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
index 8182e697e7e..a7c7ee3aab4 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;
@@ -41,12 +48,13 @@ import java.util.Arrays;
import java.util.Collections;
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;
/**
@@ -123,7 +131,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 +149,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 +198,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 +224,7 @@ public class DeploymentSpecXmlReader {
cloudAccount,
notifications,
endpoints,
+ zoneEndpoints,
now))
.toList();
}
@@ -306,19 +317,25 @@ 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<>();
+ Map<String, Endpoint> endpointsById = new LinkedHashMap<>();
+ Map<String, Map<RegionName, List<ZoneEndpoint>>> endpointsByZone = new LinkedHashMap<>();
for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) {
- String endpointId = stringAttribute("id", endpointElement).orElse(Endpoint.DEFAULT_ID);
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 + "'");
List<Endpoint.Target> targets = new ArrayList<>();
if (level == Endpoint.Level.application) {
@@ -345,33 +362,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 ( ! regions.add(RegionName.from(region))) illegal(msgPrefix + "duplicate 'region' element: '" + region + "'");
+ }
+
+ if (zoneEndpointType.isPresent()) {
+ if (regions.isEmpty()) regions.add(null);
+ ZoneEndpoint endpoint;
+ Optional<String> enabled = XML.attribute("enabled", endpointElement);
+ if ("zone".equals(zoneEndpointType.get())) {
+ switch (enabled.orElse("true")) {
+ case "true" -> endpoint = new ZoneEndpoint(true, false, List.of());
+ case "false" -> endpoint = new ZoneEndpoint(false, false, List.of());
+ default -> throw new IllegalArgumentException("Zone endpoint for container-id '" + containerId + "': " +
+ "invalid 'enabled' value; must be 'true' or 'false'");
+ }
+ }
+ else {
+ if (enabled.isPresent()) illegal("Private endpoint for container-id '" + containerId + "': " +
+ "only endpoints of type 'zone' can specify 'enabled'");
+ List<AllowedUrn> allowedUrns = new ArrayList<>();
+ for (var allow : XML.getChildren(endpointElement, "allow")) {
+ 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) + "'");
+ }
+ }
+ endpoint = new ZoneEndpoint(true, true, allowedUrns); // Doesn't turn off public visibility.
+ }
+ 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 regions by default
+ steps.stream()
+ .filter(step -> step.concerns(Environment.prod))
+ .flatMap(step -> step.zones().stream())
+ .flatMap(zone -> zone.region().stream())
+ .forEach(regions::add);
+ }
+ 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..0ca2ba89171 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,6 +1269,18 @@ 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
@@ -1256,6 +1293,11 @@ public class DeploymentSpecTest {
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='default' type='zone' container-id='foo' />"); // Zone with ID
+ assertInvalidEndpoints("<endpoint id='default' type='private' container-id='foo' />"); // Private with ID
+ assertInvalidEndpoints("<endpoint type='zone' />"); // Zone without container ID
+ assertInvalidEndpoints("<endpoint type='private' />"); // Zone without container ID
+ assertInvalidEndpoints("<endpoint type='private' container-id='foo' enabled='true' />"); // Private with enabled
}
@Test
@@ -1271,25 +1313,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='zone' enabled='false'>
+ <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(false, false, 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 +1363,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 +1406,8 @@ 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
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
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
index 7cb0672699f..49194a5d1bb 100644
--- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java
@@ -37,7 +37,7 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea
private ApplicationId applicationId = ApplicationId.defaultId();
private List<ConfigServerSpec> configServerSpecs = Collections.emptyList();
private boolean hostedVespa = false;
- private Zone zone;
+ private Zone zone = Zone.defaultZone();
private final Set<ContainerEndpoint> endpoints = Collections.emptySet();
private boolean useDedicatedNodeForLogserver = false;
private double defaultTermwiseLimit = 1.0;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java
index a31e3fcce71..aac968f9038 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.model.builder.xml.dom;
import com.yahoo.collections.Pair;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.CloudAccount;
@@ -11,7 +12,6 @@ import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.text.XML;
import com.yahoo.vespa.model.HostResource;
@@ -256,13 +256,13 @@ public class NodesSpecification {
ClusterSpec.Id clusterId,
DeployLogger logger,
boolean stateful) {
- return provision(hostSystem, clusterType, clusterId, LoadBalancerSettings.empty, logger, stateful);
+ return provision(hostSystem, clusterType, clusterId, ZoneEndpoint.defaultEndpoint, logger, stateful);
}
public Map<HostResource, ClusterMembership> provision(HostSystem hostSystem,
ClusterSpec.Type clusterType,
ClusterSpec.Id clusterId,
- LoadBalancerSettings loadBalancerSettings,
+ ZoneEndpoint zoneEndpoint,
DeployLogger logger,
boolean stateful) {
if (combinedId.isPresent())
@@ -272,7 +272,7 @@ public class NodesSpecification {
.exclusive(exclusive)
.combinedId(combinedId.map(ClusterSpec.Id::from))
.dockerImageRepository(dockerImageRepo)
- .loadBalancerSettings(loadBalancerSettings)
+ .loadBalancerSettings(zoneEndpoint)
.stateful(stateful)
.build();
return hostSystem.allocateHosts(cluster, Capacity.from(min, max, required, canFail, cloudAccount), logger);
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
index d0a03be2869..4c7bad575d2 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
@@ -11,6 +11,8 @@ import com.yahoo.config.application.api.ApplicationPackage;
import com.yahoo.config.application.api.DeployLogger;
import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
import com.yahoo.config.model.ConfigModelContext;
import com.yahoo.config.model.api.ApplicationClusterEndpoint;
import com.yahoo.config.model.api.ConfigServerSpec;
@@ -29,10 +31,11 @@ import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.bundle.BundleInstantiationSpecification;
import com.yahoo.container.logging.FileConnectionLog;
import com.yahoo.io.IOUtils;
@@ -847,11 +850,14 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
}
}
- private LoadBalancerSettings loadBalancerSettings(Element loadBalancerElement) {
- List<String> allowedUrnElements = XML.getChildren(XML.getChild(loadBalancerElement, "private-access"),
- "allow-urn")
- .stream().map(XML::getValue).toList();
- return new LoadBalancerSettings(allowedUrnElements);
+ private ZoneEndpoint zoneEndpoint(ConfigModelContext context, ClusterSpec.Id cluster) {
+ InstanceName instance = context.properties().applicationId().instance();
+ ZoneId zone = ZoneId.from(context.properties().zone().environment(),
+ context.properties().zone().region());
+ DeploymentSpec spec = context.getApplicationPackage().getDeployment()
+ .map(new DeploymentSpecXmlReader(false)::read)
+ .orElse(DeploymentSpec.empty);
+ return spec.zoneEndpoint(instance, zone, cluster);
}
private static Map<String, String> getEnvironmentVariables(Element environmentVariables) {
@@ -940,11 +946,12 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
private List<ApplicationContainer> createNodesFromNodeCount(ApplicationContainerCluster cluster, Element containerElement, Element nodesElement, ConfigModelContext context) {
NodesSpecification nodesSpecification = NodesSpecification.from(new ModelElement(nodesElement), context);
- LoadBalancerSettings loadBalancerSettings = loadBalancerSettings(XML.getChild(containerElement, "load-balancer"));
+ ClusterSpec.Id clusterId = ClusterSpec.Id.from(cluster.name());
+ ZoneEndpoint zoneEndpoint = zoneEndpoint(context, clusterId);
Map<HostResource, ClusterMembership> hosts = nodesSpecification.provision(cluster.getRoot().hostSystem(),
ClusterSpec.Type.container,
- ClusterSpec.Id.from(cluster.getName()),
- loadBalancerSettings,
+ clusterId,
+ zoneEndpoint,
log,
getZooKeeper(containerElement) != null);
return createNodesFromHosts(hosts, cluster, context.getDeployState());
diff --git a/config-model/src/main/resources/schema/deployment.rnc b/config-model/src/main/resources/schema/deployment.rnc
index 444f66a92ab..d63b8885a57 100644
--- a/config-model/src/main/resources/schema/deployment.rnc
+++ b/config-model/src/main/resources/schema/deployment.rnc
@@ -150,12 +150,21 @@ EndpointInstance = element instance {
text
}
+AllowedUrn = element allow {
+ attribute with { xsd:string } &
+ attribute arn { xsd:string }? &
+ attribute project { xsd:string }?
+}
+
Endpoint = element endpoint {
attribute id { xsd:string }? &
attribute container-id { xsd:string } &
attribute region { xsd:string }? &
+ attribute type { xsd:string }? &
+ attribute enabled { xsd:boolean }? &
EndpointRegion* &
- EndpointInstance*
+ EndpointInstance* &
+ AllowedUrn*
}
Endpoints = element endpoints {
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java
index addf4dffde2..a5b396508ef 100644
--- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java
@@ -22,6 +22,9 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.config.provisioning.FlavorsConfig;
import com.yahoo.container.ComponentsConfig;
import com.yahoo.container.QrConfig;
@@ -57,7 +60,6 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
-import java.util.stream.Collectors;
import static com.yahoo.config.model.test.TestUtil.joinLines;
import static com.yahoo.test.LinePatternMatcher.containsLineWithPattern;
@@ -198,53 +200,96 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase {
@Test
void load_balancers_can_be_set() throws IOException, SAXException {
- // No load-balancer or nodes elements
- verifyAllowedUrns("");
+ // No endpoints
+ verifyAllowedUrns("", Environment.prod, "eu", ZoneEndpoint.defaultEndpoint);
- // No load-balancer element
- verifyAllowedUrns("<nodes count='2' />");
+ // No non-default settings
+ verifyAllowedUrns("""
+ <endpoint type='zone' container-id='default' />
+ """,
+ Environment.prod,
+ "eu",
+ ZoneEndpoint.defaultEndpoint);
- // No nodes element
+ // No allowed urns
verifyAllowedUrns("""
- <load-balancer>
- <private-access>
- <allow-urn>foo</allow-urn>
- <allow-urn>bar</allow-urn>
- </private-access>
- </load-balancer>
- """);
-
- // Both load-balancer and nodes
+ <endpoint type='private' container-id='default' />
+ """,
+ Environment.prod,
+ "eu",
+ new ZoneEndpoint(true, true, List.of()));
+
+ // Various settings
+ verifyAllowedUrns("""
+ <endpoint type='zone' container-id='default' enabled='false' />
+ <endpoint type='private' container-id='default'>
+ <region>eu</region>
+ <allow with='aws-private-link' arn='barn' />
+ <allow with='gcp-service-connect' project='nine' />
+ </endpoint>
+ """,
+ Environment.prod,
+ "eu",
+ new ZoneEndpoint(false, true, List.of(new AllowedUrn(AccessType.awsPrivateLink, "barn"),
+ new AllowedUrn(AccessType.gcpServiceConnect, "nine"))));
+
+ // Various settings, but wrong region
verifyAllowedUrns("""
- <load-balancer>
- <private-access>
- <allow-urn>foo</allow-urn>
- <allow-urn>bar</allow-urn>
- </private-access>
- </load-balancer>
- <nodes count='2' />
+ <endpoint type='zone' container-id='default' enabled='false' />
+ <endpoint type='private' container-id='default'>
+ <region>eu</region>
+ <allow with='aws-private-link' arn='barn' />
+ <allow with='gcp-service-connect' project='nine' />
+ </endpoint>
""",
- "foo", "bar");
+ Environment.prod,
+ "us",
+ ZoneEndpoint.defaultEndpoint);
+
+ // Various settings, but wrong environment
+ verifyAllowedUrns("""
+ <endpoint type='zone' container-id='default' enabled='false' />
+ <endpoint type='private' container-id='default'>
+ <region>eu</region>
+ <allow with='aws-private-link' arn='barn' />
+ <allow with='gcp-service-connect' project='nine' />
+ </endpoint>
+ """,
+ Environment.dev,
+ "eu",
+ ZoneEndpoint.defaultEndpoint);
}
- private void verifyAllowedUrns(String containerXml, String... expectedAllowedUrns) throws IOException, SAXException {
+ private void verifyAllowedUrns(String endpointsTag, Environment environment, String region, ZoneEndpoint expected) throws IOException, SAXException {
String servicesXml = """
<container id='default' version='1.0'>
- %s
+ <nodes count='2' />
</container>
- """.formatted(containerXml);
- ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build();
+ """;
+ String deploymentXml = """
+ <deployment version='1.0'>
+ <prod>
+ <region>eu</region>
+ </prod>
+ <endpoints>
+ %s
+ </endpoints>
+ </deployment>
+ """.formatted(endpointsTag);
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).withDeploymentSpec(deploymentXml).build();
InMemoryProvisioner provisioner = new InMemoryProvisioner(true, false, "host1.yahoo.com", "host2.yahoo.com");
VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder()
.modelHostProvisioner(provisioner)
.provisioned(provisioner.startProvisionedRecording())
.applicationPackage(applicationPackage)
- .properties(new TestProperties().setMultitenant(true).setHostedVespa(true))
+ .properties(new TestProperties().setMultitenant(true)
+ .setHostedVespa(true)
+ .setZone(new Zone(environment, RegionName.from(region))))
.build());
assertEquals(2, model.hostSystem().getHosts().size());
assertEquals(1, provisioner.provisionedClusters().size());
- assertEquals(List.of(expectedAllowedUrns),
- provisioner.provisionedClusters().iterator().next().loadBalancerSettings().allowedUrns());
+ assertEquals(expected,
+ provisioner.provisionedClusters().iterator().next().zoneEndpoint());
}
@Test
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
index 213166447ca..9e8388b6442 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterMembership.java
@@ -20,7 +20,7 @@ public class ClusterMembership {
private final String stringValue;
private ClusterMembership(String stringValue, Version vespaVersion, Optional<DockerImage> dockerImageRepo,
- LoadBalancerSettings loadBalancerSettings) {
+ ZoneEndpoint zoneEndpoint) {
String[] components = stringValue.split("/");
if (components.length < 4)
throw new RuntimeException("Could not parse '" + stringValue + "' to a cluster membership. " +
@@ -49,7 +49,7 @@ public class ClusterMembership {
.exclusive(exclusive)
.combinedId(combinedId.map(ClusterSpec.Id::from))
.dockerImageRepository(dockerImageRepo)
- .loadBalancerSettings(loadBalancerSettings)
+ .loadBalancerSettings(zoneEndpoint)
.stateful(stateful)
.build();
this.index = Integer.parseInt(components[3]);
@@ -125,12 +125,12 @@ public class ClusterMembership {
public String toString() { return stringValue(); }
public static ClusterMembership from(String stringValue, Version vespaVersion, Optional<DockerImage> dockerImageRepo) {
- return from(stringValue, vespaVersion, dockerImageRepo, LoadBalancerSettings.empty);
+ return from(stringValue, vespaVersion, dockerImageRepo, ZoneEndpoint.defaultEndpoint);
}
public static ClusterMembership from(String stringValue, Version vespaVersion, Optional<DockerImage> dockerImageRepo,
- LoadBalancerSettings loadBalancerSettings) {
- return new ClusterMembership(stringValue, vespaVersion, dockerImageRepo, loadBalancerSettings);
+ ZoneEndpoint zoneEndpoint) {
+ return new ClusterMembership(stringValue, vespaVersion, dockerImageRepo, zoneEndpoint);
}
public static ClusterMembership from(ClusterSpec cluster, int index) {
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
index 153b305dc01..196255a8342 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java
@@ -24,12 +24,12 @@ public final class ClusterSpec {
private final boolean exclusive;
private final Optional<Id> combinedId;
private final Optional<DockerImage> dockerImageRepo;
- private final LoadBalancerSettings loadBalancerSettings;
+ private final ZoneEndpoint zoneEndpoint;
private final boolean stateful;
private ClusterSpec(Type type, Id id, Optional<Group> groupId, Version vespaVersion, boolean exclusive,
Optional<Id> combinedId, Optional<DockerImage> dockerImageRepo,
- LoadBalancerSettings loadBalancerSettings, boolean stateful) {
+ ZoneEndpoint zoneEndpoint, boolean stateful) {
this.type = type;
this.id = id;
this.groupId = groupId;
@@ -47,7 +47,7 @@ public final class ClusterSpec {
if (type.isContent() && !stateful) {
throw new IllegalArgumentException("Cluster of type " + type + " must be stateful");
}
- this.loadBalancerSettings = Objects.requireNonNull(loadBalancerSettings);
+ this.zoneEndpoint = Objects.requireNonNull(zoneEndpoint);
this.stateful = stateful;
}
@@ -63,8 +63,8 @@ public final class ClusterSpec {
/** Returns the docker image (repository + vespa version) we want this cluster to run */
public Optional<String> dockerImage() { return dockerImageRepo.map(repo -> repo.withTag(vespaVersion).asString()); }
- /** Returns any additional load balancer settings for application container clusters. */
- public LoadBalancerSettings loadBalancerSettings() { return loadBalancerSettings; }
+ /** Returns any additional zone endpoint settings for application container clusters. */
+ public ZoneEndpoint zoneEndpoint() { return zoneEndpoint; }
/** Returns the version of Vespa that we want this cluster to run */
public Version vespaVersion() { return vespaVersion; }
@@ -87,15 +87,15 @@ public final class ClusterSpec {
public boolean isStateful() { return stateful; }
public ClusterSpec with(Optional<Group> newGroup) {
- return new ClusterSpec(type, id, newGroup, vespaVersion, exclusive, combinedId, dockerImageRepo, loadBalancerSettings, stateful);
+ return new ClusterSpec(type, id, newGroup, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
public ClusterSpec withExclusivity(boolean exclusive) {
- return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, loadBalancerSettings, stateful);
+ return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
public ClusterSpec exclusive(boolean exclusive) {
- return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, loadBalancerSettings, stateful);
+ return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
/** Creates a ClusterSpec when requesting a cluster */
@@ -119,7 +119,7 @@ public final class ClusterSpec {
private Version vespaVersion;
private boolean exclusive = false;
private Optional<Id> combinedId = Optional.empty();
- private LoadBalancerSettings loadBalancerSettings = LoadBalancerSettings.empty;
+ private ZoneEndpoint zoneEndpoint = ZoneEndpoint.defaultEndpoint;
private boolean stateful;
private Builder(Type type, Id id, boolean specification) {
@@ -135,7 +135,7 @@ public final class ClusterSpec {
if (vespaVersion == null) throw new IllegalArgumentException("vespaVersion is required to be set when creating a ClusterSpec with specification()");
} else
if (groupId.isPresent()) throw new IllegalArgumentException("groupId is not allowed to be set when creating a ClusterSpec with request()");
- return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, loadBalancerSettings, stateful);
+ return new ClusterSpec(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
public Builder group(Group groupId) {
@@ -168,8 +168,8 @@ public final class ClusterSpec {
return this;
}
- public Builder loadBalancerSettings(LoadBalancerSettings loadBalancerSettings) {
- this.loadBalancerSettings = loadBalancerSettings;
+ public Builder loadBalancerSettings(ZoneEndpoint zoneEndpoint) {
+ this.zoneEndpoint = zoneEndpoint;
return this;
}
@@ -198,12 +198,12 @@ public final class ClusterSpec {
vespaVersion.equals(that.vespaVersion) &&
combinedId.equals(that.combinedId) &&
dockerImageRepo.equals(that.dockerImageRepo) &&
- loadBalancerSettings.equals(that.loadBalancerSettings);
+ zoneEndpoint.equals(that.zoneEndpoint);
}
@Override
public int hashCode() {
- return Objects.hash(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, loadBalancerSettings, stateful);
+ return Objects.hash(type, id, groupId, vespaVersion, exclusive, combinedId, dockerImageRepo, zoneEndpoint, stateful);
}
/**
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/LoadBalancerSettings.java b/config-provisioning/src/main/java/com/yahoo/config/provision/LoadBalancerSettings.java
deleted file mode 100644
index 723de25fa87..00000000000
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/LoadBalancerSettings.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.yahoo.config.provision;
-
-import java.util.List;
-
-/**
- * Settings for a load balancer provisioned for an application container cluster.
- *
- * @author jonmv
- */
-public record LoadBalancerSettings(List<String> allowedUrns) {
-
- public static final LoadBalancerSettings empty = new LoadBalancerSettings(List.of());
-
- public LoadBalancerSettings(List<String> allowedUrns) {
- this.allowedUrns = List.copyOf(allowedUrns);
- }
-
- public boolean isEmpty() { return allowedUrns.isEmpty(); }
-
-}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ZoneEndpoint.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ZoneEndpoint.java
new file mode 100644
index 00000000000..a14747a0226
--- /dev/null
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ZoneEndpoint.java
@@ -0,0 +1,131 @@
+package com.yahoo.config.provision;
+
+import ai.vespa.validation.Validation;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Settings for a zone endpoint of a deployment.
+ *
+ * TODO: Fix isEmpty
+ * Inline empty and constructor
+ *
+ * @author jonmv
+ */
+public class ZoneEndpoint {
+
+ public static final ZoneEndpoint defaultEndpoint = new ZoneEndpoint(true, false, List.of());
+
+ private final boolean isPublicEndpoint;
+ private final boolean isPrivateEndpoint;
+ private final List<AllowedUrn> allowedUrns;
+
+ public ZoneEndpoint(List<String> allowedUrns) {
+ this(true, true, allowedUrns.stream().map(arn -> new AllowedUrn(AccessType.awsPrivateLink, arn)).toList());
+ }
+
+ public ZoneEndpoint(boolean isPublicEndpoint, boolean isPrivateEndpoint, List<AllowedUrn> allowedUrns) {
+ if ( ! allowedUrns.isEmpty() && ! isPrivateEndpoint)
+ throw new IllegalArgumentException("cannot list allowed urns, without also enabling private visibility");
+ this.isPublicEndpoint = isPublicEndpoint;
+ this.isPrivateEndpoint = isPrivateEndpoint;
+ this.allowedUrns = List.copyOf(allowedUrns);
+ }
+
+ /** Whether this has an endpoint which is visible from the public internet. */
+ public boolean isPublicEndpoint() {
+ return isPublicEndpoint;
+ }
+
+ /** Whether this has an endpoint which is visible through private DNS of the cloud. */
+ public boolean isPrivateEndpoint() {
+ return isPrivateEndpoint;
+ }
+
+ /** List of allowed URNs, for specified private access types. */
+ public List<AllowedUrn> allowedUrns() {
+ return allowedUrns;
+ }
+
+ /** List of URNs for the given access type. */
+ public List<String> allowedUrnsWith(AccessType type) {
+ return allowedUrns.stream().filter(urn -> urn.type == type).map(AllowedUrn::urn).toList();
+ }
+
+ public boolean isDefault() {
+ return equals(defaultEndpoint);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ZoneEndpoint that = (ZoneEndpoint) o;
+ return isPublicEndpoint == that.isPublicEndpoint && isPrivateEndpoint == that.isPrivateEndpoint && allowedUrns.equals(that.allowedUrns);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isPublicEndpoint, isPrivateEndpoint, allowedUrns);
+ }
+
+ @Override
+ public String toString() {
+ return "ZoneEndpoint{" +
+ "isPublicEndpoint=" + isPublicEndpoint +
+ ", isPrivateEndpoint=" + isPrivateEndpoint +
+ ", allowedUrns=" + allowedUrns +
+ '}';
+ }
+
+ public enum AccessType {
+ awsPrivateLink,
+ gcpServiceConnect,
+ }
+
+ /** A URN allowed to access this (private) endpoint, through a {@link AccessType} method. */
+ public static class AllowedUrn {
+
+ private final AccessType type;
+ private final String urn;
+
+ public AllowedUrn(AccessType type, String urn) {
+ this.type = Objects.requireNonNull(type);
+ this.urn = Validation.requireNonBlank(urn, "URN");
+ }
+
+ /** Type of private connection. */
+ public AccessType type() {
+ return type;
+ }
+
+ /** URN allowed to access this private endpoint. */
+ public String urn() {
+ return urn;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AllowedUrn that = (AllowedUrn) o;
+ return type == that.type && urn.equals(that.urn);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, urn);
+ }
+
+ @Override
+ public String toString() {
+ return "AllowedUrn{" +
+ "type=" + type +
+ ", urn='" + urn + '\'' +
+ '}';
+ }
+
+ }
+
+}
diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java b/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java
index 01bb0ca45ff..64e8a7feb94 100644
--- a/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java
+++ b/config-provisioning/src/main/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializer.java
@@ -6,8 +6,10 @@ import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.HostSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -40,8 +42,12 @@ public class AllocatedHostsSerializer {
private static final String hostSpecKey = "hostSpec";
private static final String hostSpecHostNameKey = "hostName";
private static final String hostSpecMembershipKey = "membership";
- private static final String loadBalancerSettingsKey = "loadBalancerSettings";
- private static final String allowedUrnsKey = "allowedUrns";
+ private static final String loadBalancerSettingsKey = "zoneEndpoint";
+ private static final String publicField = "public";
+ private static final String privateField = "private";
+ private static final String allowedUrnsField = "allowedUrns";
+ private static final String accessTypeField = "type";
+ private static final String urnField = "urn";
private static final String realResourcesKey = "realResources";
private static final String advertisedResourcesKey = "advertisedResources";
@@ -85,9 +91,8 @@ public class AllocatedHostsSerializer {
host.membership().ifPresent(membership -> {
object.setString(hostSpecMembershipKey, membership.stringValue());
object.setString(hostSpecVespaVersionKey, membership.cluster().vespaVersion().toFullString());
- if ( ! membership.cluster().loadBalancerSettings().isEmpty())
- membership.cluster().loadBalancerSettings().allowedUrns()
- .forEach(object.setObject(loadBalancerSettingsKey).setArray(allowedUrnsKey)::addString);
+ if ( ! membership.cluster().zoneEndpoint().isDefault())
+ toSlime(object.setObject(loadBalancerSettingsKey), membership.cluster().zoneEndpoint());
membership.cluster().dockerImageRepo().ifPresent(repo -> object.setString(hostSpecDockerImageRepoKey, repo.untagged()));
});
toSlime(host.realResources(), object.setObject(realResourcesKey));
@@ -222,13 +227,41 @@ public class AllocatedHostsSerializer {
object.field(hostSpecDockerImageRepoKey).valid()
? Optional.of(DockerImage.fromString(object.field(hostSpecDockerImageRepoKey).asString()))
: Optional.empty(),
- object.field(loadBalancerSettingsKey).valid()
- ? new LoadBalancerSettings(SlimeUtils.entriesStream(object.field(loadBalancerSettingsKey).field(allowedUrnsKey))
- .map(Inspector::asString)
- .toList())
- : LoadBalancerSettings.empty);
+ zoneEndpoint(object.field(loadBalancerSettingsKey)));
}
+ private static void toSlime(Cursor settingsObject, ZoneEndpoint settings) {
+ settingsObject.setBool(publicField, settings.isPublicEndpoint());
+ settingsObject.setBool(privateField, settings.isPrivateEndpoint());
+ if (settings.isPrivateEndpoint()) {
+ Cursor allowedUrnsArray = settingsObject.setArray(allowedUrnsField);
+ for (AllowedUrn urn : settings.allowedUrns()) {
+ Cursor urnObject = allowedUrnsArray.addObject();
+ urnObject.setString(urnField, urn.urn());
+ urnObject.setString(accessTypeField,
+ switch (urn.type()) {
+ case awsPrivateLink -> "awsPrivateLink";
+ case gcpServiceConnect -> "gcpServiceConnect";
+ });
+ }
+ }
+ }
+
+ private static ZoneEndpoint zoneEndpoint(Inspector settingsObject) {
+ if ( ! settingsObject.valid()) return ZoneEndpoint.defaultEndpoint;
+ return new ZoneEndpoint(settingsObject.field(publicField).asBool(),
+ settingsObject.field(privateField).asBool(),
+ SlimeUtils.entriesStream(settingsObject.field(allowedUrnsField))
+ .map(urnObject -> new AllowedUrn(switch (urnObject.field(accessTypeField).asString()) {
+ case "awsPrivateLink" -> AccessType.awsPrivateLink;
+ case "gcpServiceConnect" -> AccessType.gcpServiceConnect;
+ default -> throw new IllegalArgumentException("unknown service access type in '" + urnObject + "'");
+ },
+ urnObject.field(urnField).asString()))
+ .toList());
+ }
+
+
private static Optional<String> optionalString(Inspector inspector) {
if ( ! inspector.valid()) return Optional.empty();
return Optional.of(inspector.asString());
diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializerTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializerTest.java
index bcb3b8cd4aa..3404d7ed55e 100644
--- a/config-provisioning/src/test/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializerTest.java
+++ b/config-provisioning/src/test/java/com/yahoo/config/provision/serialization/AllocatedHostsSerializerTest.java
@@ -6,9 +6,9 @@ import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.HostSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NetworkPorts;
import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.ZoneEndpoint;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -20,7 +20,6 @@ import java.util.Set;
import static com.yahoo.config.provision.serialization.AllocatedHostsSerializer.fromJson;
import static com.yahoo.config.provision.serialization.AllocatedHostsSerializer.toJson;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
/**
* @author bratseth
@@ -69,7 +68,7 @@ public class AllocatedHostsSerializerTest {
bigSlowDiskSpeedNode,
anyDiskSpeedNode,
ClusterMembership.from("container/test/0/0", Version.fromString("6.73.1"),
- Optional.empty(), new LoadBalancerSettings(List.of("burn"))),
+ Optional.empty(), new ZoneEndpoint(List.of("burn"))),
Optional.empty(),
Optional.empty(),
Optional.empty()));
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java
index a4e26fbe7b3..26330f11d65 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java
@@ -5,6 +5,7 @@ import ai.vespa.http.DomainName;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import java.util.List;
import java.util.Objects;
@@ -84,6 +85,6 @@ public class LoadBalancer {
unknown
}
- public record PrivateServiceInfo(String id, List<String> allowedUrns) { }
+ public record PrivateServiceInfo(String id, List<AllowedUrn> allowedUrns) { }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index 24bab28c520..97eb405b32d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -621,7 +621,7 @@ public class JobController {
submission.applicationPackage().deploymentSpec().majorVersion().ifPresent(explicitMajor -> {
if ( ! controller.readVersionStatus().isOnCurrentMajor(new Version(explicitMajor)))
controller.notificationsDb().setNotification(NotificationSource.from(id), Type.submission, Notification.Level.warning,
- "Vespa " + explicitMajor + " will soon be end of life, upgrade to Vespa " + (explicitMajor + 1) + " now: " +
+ "Vespa " + explicitMajor + " will soon reach end of life, upgrade to Vespa " + (explicitMajor + 1) + " now: " +
"https://cloud.vespa.ai/en/vespa" + (explicitMajor + 1) + "-release-notes.html"); // ∠( ᐛ 」∠)_
});
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 1505d7e2ca8..39cb9bf0d8d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -23,6 +23,7 @@ import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.handler.metrics.JsonResponse;
@@ -1979,7 +1980,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
lbObject.setString("cluster", lb.cluster().value());
lb.service().ifPresent(service -> {
lbObject.setString("serviceId", service.id()); // Really the "serviceName", but this is what the user needs >_<
- service.allowedUrns().forEach(lbObject.setArray("allowedUrns")::addString);
+ Cursor urnsArray = lbObject.setArray("allowedUrns");
+ for (AllowedUrn urn : service.allowedUrns()) {
+ Cursor urnObject = urnsArray.addObject();
+ urnObject.setString("type", switch (urn.type()) {
+ case awsPrivateLink -> "aws-private-link";
+ case gcpServiceConnect -> "gcp-service-connect";
+ });
+ urnObject.setString("urn", urn.urn());
+ }
Cursor endpointsArray = lbObject.setArray("endpoints");
controller.serviceRegistry().vpcEndpointService()
.getConnections(new ClusterId(id, lb.cluster()),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
index 2c1d7315adc..7acd31c2ded 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
@@ -16,6 +16,8 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
@@ -417,7 +419,7 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
LoadBalancer.State.active,
Optional.of("dns-zone-1"),
Optional.empty(),
- Optional.of(new PrivateServiceInfo("service", List.of("arne"))))));
+ Optional.of(new PrivateServiceInfo("service", List.of(new AllowedUrn(AccessType.awsPrivateLink, "arne")))))));
}
Application application = applications.get(id);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index d40485ff5c0..be405c7b876 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -701,7 +701,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/private-service", GET)
.userIdentity(USER_ID),
"""
- {"loadBalancers":[{"cluster":"default","serviceId":"service","allowedUrns":["arne"],"endpoints":[{"endpointId":"endpoint-1","state":"available"}]}]}""");
+ {"loadBalancers":[{"cluster":"default","serviceId":"service","allowedUrns":[{"type":"aws-private-link","urn":"arne"}],"endpoints":[{"endpointId":"endpoint-1","state":"available"}]}]}""");
// GET service/state/v1
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/service/storagenode/host.com/state/v1/?foo=bar", GET)
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java
index 33c9edf694d..2856a38075b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerInstance.java
@@ -4,7 +4,7 @@ package com.yahoo.vespa.hosted.provision.lb;
import ai.vespa.http.DomainName;
import com.google.common.collect.ImmutableSortedSet;
import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.ZoneEndpoint;
import java.util.Objects;
import java.util.Optional;
@@ -24,12 +24,12 @@ public class LoadBalancerInstance {
private final Set<Integer> ports;
private final Set<String> networks;
private final Set<Real> reals;
- private final LoadBalancerSettings settings;
+ private final ZoneEndpoint settings;
private final Optional<PrivateServiceId> serviceId;
private final CloudAccount cloudAccount;
public LoadBalancerInstance(Optional<DomainName> hostname, Optional<String> ipAddress, Optional<DnsZone> dnsZone,
- Set<Integer> ports, Set<String> networks, Set<Real> reals, LoadBalancerSettings settings,
+ Set<Integer> ports, Set<String> networks, Set<Real> reals, ZoneEndpoint settings,
Optional<PrivateServiceId> serviceId, CloudAccount cloudAccount) {
this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null");
this.ipAddress = Objects.requireNonNull(ipAddress, "ip must be non-null");
@@ -78,7 +78,7 @@ public class LoadBalancerInstance {
}
/** Static user-configured settings of this load balancer */
- public LoadBalancerSettings settings() {
+ public ZoneEndpoint settings() {
return settings;
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java
index e0dd41f9008..c19aebcda6e 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerServiceMock.java
@@ -4,8 +4,8 @@ package com.yahoo.vespa.hosted.provision.lb;
import ai.vespa.http.DomainName;
import com.google.common.collect.ImmutableSet;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.ZoneEndpoint;
import java.util.Collections;
import java.util.HashMap;
@@ -62,7 +62,7 @@ public class LoadBalancerServiceMock implements LoadBalancerService {
Collections.singleton(4443),
ImmutableSet.of("10.2.3.0/24", "10.4.5.0/24"),
spec.reals(),
- spec.settings().orElse(LoadBalancerSettings.empty),
+ spec.settings().orElse(ZoneEndpoint.defaultEndpoint),
spec.settings().map(__ -> PrivateServiceId.of("service")),
spec.cloudAccount());
instances.put(id, instance);
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java
index dca6d434330..e0ef6739542 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/LoadBalancerSpec.java
@@ -5,7 +5,7 @@ import com.google.common.collect.ImmutableSortedSet;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.ZoneEndpoint;
import java.util.Objects;
import java.util.Optional;
@@ -21,11 +21,11 @@ public class LoadBalancerSpec {
private final ApplicationId application;
private final ClusterSpec.Id cluster;
private final Set<Real> reals;
- private final Optional<LoadBalancerSettings> settings;
+ private final Optional<ZoneEndpoint> settings;
private final CloudAccount cloudAccount;
public LoadBalancerSpec(ApplicationId application, ClusterSpec.Id cluster, Set<Real> reals,
- LoadBalancerSettings settings, CloudAccount cloudAccount) {
+ ZoneEndpoint settings, CloudAccount cloudAccount) {
this.application = Objects.requireNonNull(application);
this.cluster = Objects.requireNonNull(cluster);
this.reals = ImmutableSortedSet.copyOf(Objects.requireNonNull(reals));
@@ -49,7 +49,7 @@ public class LoadBalancerSpec {
}
/** Static user-configured settings for this load balancer. */
- public Optional<LoadBalancerSettings> settings() {
+ public Optional<ZoneEndpoint> settings() {
return settings;
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java
index c8fb1226b81..5dc099460a4 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerService.java
@@ -3,8 +3,8 @@ package com.yahoo.vespa.hosted.provision.lb;
import ai.vespa.http.DomainName;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.ZoneEndpoint;
import java.util.Objects;
import java.util.Optional;
@@ -29,15 +29,15 @@ public class SharedLoadBalancerService implements LoadBalancerService {
@Override
public LoadBalancerInstance create(LoadBalancerSpec spec, boolean force) {
- if (spec.settings().isPresent() && ! spec.settings().get().isEmpty())
- throw new IllegalArgumentException("custom load balancer settings are not supported with " + getClass());
+ if (spec.settings().isPresent() && ! spec.settings().get().isDefault())
+ throw new IllegalArgumentException("custom zone endpoint settings are not supported with " + getClass());
return new LoadBalancerInstance(Optional.of(DomainName.of(vipHostname)),
Optional.empty(),
Optional.empty(),
Set.of(4443),
Set.of(),
spec.reals(),
- spec.settings().orElse(LoadBalancerSettings.empty),
+ spec.settings().orElse(ZoneEndpoint.defaultEndpoint),
Optional.empty(),
spec.cloudAccount());
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
index 3d352f5596b..6bac1dab3dd 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializer.java
@@ -3,7 +3,9 @@ package com.yahoo.vespa.hosted.provision.persistence;
import ai.vespa.http.DomainName;
import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.ZoneEndpoint;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
+import com.yahoo.config.provision.ZoneEndpoint.AccessType;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -52,7 +54,11 @@ public class LoadBalancerSerializer {
private static final String serviceIdField = "serviceId";
private static final String cloudAccountField = "cloudAccount";
private static final String settingsField = "settings";
+ private static final String publicField = "public";
+ private static final String privateField = "private";
private static final String allowedUrnsField = "allowedUrns";
+ private static final String accessTypeField = "type";
+ private static final String urnField = "urn";
public static byte[] toJson(LoadBalancer loadBalancer) {
Slime slime = new Slime();
@@ -77,15 +83,13 @@ public class LoadBalancerSerializer {
}));
loadBalancer.instance()
.map(LoadBalancerInstance::settings)
- .filter(settings -> ! settings.isEmpty())
- .ifPresent(settings -> settings.allowedUrns().forEach(root.setObject(settingsField)
- .setArray(allowedUrnsField)::addString));
+ .ifPresent(settings -> toSlime(root.setObject(settingsField), settings));
loadBalancer.instance()
.flatMap(LoadBalancerInstance::serviceId)
.ifPresent(serviceId -> root.setString(serviceIdField, serviceId.value()));
loadBalancer.instance()
.map(LoadBalancerInstance::cloudAccount)
- .filter(cloudAccount -> !cloudAccount.isUnspecified())
+ .filter(cloudAccount -> ! cloudAccount.isUnspecified())
.ifPresent(cloudAccount -> root.setString(cloudAccountField, cloudAccount.value()));
try {
return SlimeUtils.toJsonBytes(slime);
@@ -114,7 +118,7 @@ public class LoadBalancerSerializer {
Optional<DomainName> hostname = optionalString(object.field(hostnameField), Function.identity()).filter(s -> !s.isEmpty()).map(DomainName::of);
Optional<String> ipAddress = optionalString(object.field(lbIpAddressField), Function.identity()).filter(s -> !s.isEmpty());
Optional<DnsZone> dnsZone = optionalString(object.field(dnsZoneField), DnsZone::new);
- LoadBalancerSettings settings = loadBalancerSettings(object.field(settingsField));
+ ZoneEndpoint settings = zoneEndpoint(object.field(settingsField));
Optional<PrivateServiceId> serviceId = optionalString(object.field(serviceIdField), PrivateServiceId::of);
CloudAccount cloudAccount = optionalString(object.field(cloudAccountField), CloudAccount::from).orElse(CloudAccount.empty);
Optional<LoadBalancerInstance> instance = hostname.isEmpty() && ipAddress.isEmpty() ? Optional.empty() :
@@ -126,11 +130,35 @@ public class LoadBalancerSerializer {
Instant.ofEpochMilli(object.field(changedAtField).asLong()));
}
- private static LoadBalancerSettings loadBalancerSettings(Inspector settingsObject) {
- if ( ! settingsObject.valid()) return LoadBalancerSettings.empty;
- return new LoadBalancerSettings(SlimeUtils.entriesStream(settingsObject.field(allowedUrnsField))
- .map(Inspector::asString)
- .toList());
+ private static void toSlime(Cursor settingsObject, ZoneEndpoint settings) {
+ settingsObject.setBool(publicField, settings.isPublicEndpoint());
+ settingsObject.setBool(privateField, settings.isPrivateEndpoint());
+ if (settings.isPrivateEndpoint()) {
+ Cursor allowedUrnsArray = settingsObject.setArray(allowedUrnsField);
+ for (AllowedUrn urn : settings.allowedUrns()) {
+ Cursor urnObject = allowedUrnsArray.addObject();
+ urnObject.setString(urnField, urn.urn());
+ urnObject.setString(accessTypeField,
+ switch (urn.type()) {
+ case awsPrivateLink -> "awsPrivateLink";
+ case gcpServiceConnect -> "gcpServiceConnect";
+ });
+ }
+ }
+ }
+
+ private static ZoneEndpoint zoneEndpoint(Inspector settingsObject) {
+ if ( ! settingsObject.valid()) return ZoneEndpoint.defaultEndpoint;
+ return new ZoneEndpoint(settingsObject.field(publicField).asBool(),
+ settingsObject.field(privateField).asBool(),
+ SlimeUtils.entriesStream(settingsObject.field(allowedUrnsField))
+ .map(urnObject -> new AllowedUrn(switch (urnObject.field(accessTypeField).asString()) {
+ case "awsPrivateLink" -> AccessType.awsPrivateLink;
+ case "gcpServiceConnect" -> AccessType.gcpServiceConnect;
+ default -> throw new IllegalArgumentException("unknown service access type in '" + urnObject + "'");
+ },
+ urnObject.field(urnField).asString()))
+ .toList());
}
private static <T> Optional<T> optionalValue(Inspector field, Function<Inspector, T> fieldMapper) {
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
index 3e8124d5309..92fdb1d2e52 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisioner.java
@@ -7,9 +7,9 @@ import com.yahoo.config.provision.ApplicationTransaction;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.config.provision.exception.LoadBalancerServiceException;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.flags.BooleanFlag;
@@ -108,11 +108,13 @@ public class LoadBalancerProvisioner {
* Calling this when no load balancer has been prepared for given cluster is a no-op.
*/
public void activate(Set<ClusterSpec> clusters, NodeList newActive, ApplicationTransaction transaction) {
- Map<ClusterSpec.Id, LoadBalancerSettings> activatingClusters = clusters.stream()
- .collect(groupingBy(LoadBalancerProvisioner::effectiveId,
- reducing(LoadBalancerSettings.empty,
- ClusterSpec::loadBalancerSettings,
- (o, n) -> o.isEmpty() ? n : o)));
+ Map<ClusterSpec.Id, ZoneEndpoint> activatingClusters = clusters.stream()
+ // .collect(Collectors.toMap(ClusterSpec::id, ClusterSpec::zoneEndpoint));
+ // TODO: this dies with combined clusters Ü
+ .collect(groupingBy(LoadBalancerProvisioner::effectiveId,
+ reducing(ZoneEndpoint.defaultEndpoint,
+ ClusterSpec::zoneEndpoint,
+ (o, n) -> o.isDefault() ? n : o)));
for (var cluster : loadBalancedClustersOf(newActive).entrySet()) {
if ( ! activatingClusters.containsKey(cluster.getKey()))
continue;
@@ -209,7 +211,7 @@ public class LoadBalancerProvisioner {
requireInstance(id, instance, cloudAccount);
}
- private void activate(ApplicationTransaction transaction, ClusterSpec.Id cluster, LoadBalancerSettings settings, NodeList nodes) {
+ private void activate(ApplicationTransaction transaction, ClusterSpec.Id cluster, ZoneEndpoint settings, NodeList nodes) {
Instant now = nodeRepository.clock().instant();
LoadBalancerId id = new LoadBalancerId(transaction.application(), cluster);
Optional<LoadBalancer> loadBalancer = db.readLoadBalancer(id);
@@ -226,7 +228,7 @@ public class LoadBalancerProvisioner {
/** Provision or reconfigure a load balancer instance, if necessary */
private Optional<LoadBalancerInstance> provisionInstance(LoadBalancerId id, NodeList nodes,
Optional<LoadBalancer> currentLoadBalancer,
- LoadBalancerSettings loadBalancerSettings,
+ ZoneEndpoint zoneEndpoint,
CloudAccount cloudAccount) {
boolean shouldDeactivateRouting = deactivateRouting.with(FetchVector.Dimension.APPLICATION_ID,
id.application().serializedForm())
@@ -237,13 +239,13 @@ public class LoadBalancerProvisioner {
} else {
reals = realsOf(nodes);
}
- if (isUpToDate(currentLoadBalancer, reals, loadBalancerSettings))
+ if (isUpToDate(currentLoadBalancer, reals, zoneEndpoint))
return currentLoadBalancer.get().instance();
log.log(Level.INFO, () -> "Provisioning instance for " + id + ", targeting: " + reals);
try {
// Override settings at activation, otherwise keep existing ones.
- LoadBalancerSettings settings = loadBalancerSettings != null ? loadBalancerSettings
- : currentLoadBalancer.flatMap(LoadBalancer::instance)
+ ZoneEndpoint settings = zoneEndpoint != null ? zoneEndpoint
+ : currentLoadBalancer.flatMap(LoadBalancer::instance)
.map(LoadBalancerInstance::settings)
.orElse(null);
LoadBalancerInstance created = service.create(new LoadBalancerSpec(id.application(), id.cluster(), reals, settings, cloudAccount),
@@ -306,11 +308,11 @@ public class LoadBalancerProvisioner {
}
/** Returns whether load balancer has given reals, and settings if specified*/
- private static boolean isUpToDate(Optional<LoadBalancer> loadBalancer, Set<Real> reals, LoadBalancerSettings loadBalancerSettings) {
+ private static boolean isUpToDate(Optional<LoadBalancer> loadBalancer, Set<Real> reals, ZoneEndpoint zoneEndpoint) {
if (loadBalancer.isEmpty()) return false;
if (loadBalancer.get().instance().isEmpty()) return false;
return loadBalancer.get().instance().get().reals().equals(reals)
- && (loadBalancerSettings == null || loadBalancer.get().instance().get().settings().equals(loadBalancerSettings));
+ && (zoneEndpoint == null || loadBalancer.get().instance().get().settings().equals(zoneEndpoint));
}
/** Returns whether to allow given load balancer to have no reals */
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java
index fdf69b60690..15a799c06d8 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/LoadBalancersResponse.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.provision.restapi;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
@@ -76,8 +77,17 @@ public class LoadBalancersResponse extends SlimeJsonResponse {
});
});
lb.instance().ifPresent(instance -> {
- if ( ! instance.settings().isEmpty())
- instance.settings().allowedUrns().forEach(lbObject.setObject("settings").setArray("allowedUrns")::addString);
+ if ( ! instance.settings().isDefault()) {
+ Cursor urnsArray = lbObject.setObject("settings").setArray("allowedUrns");
+ for (AllowedUrn urn : instance.settings().allowedUrns()) {
+ Cursor urnObject = urnsArray.addObject();
+ urnObject.setString("type", switch (urn.type()) {
+ case awsPrivateLink -> "aws-private-link";
+ case gcpServiceConnect -> "gcp-service-connect";
+ });
+ urnObject.setString("urn", urn.urn());
+ }
+ }
instance.serviceId().ifPresent(serviceId -> lbObject.setString("serviceId", serviceId.value()));
});
lb.instance()
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
index 91c8f803429..92ffe9828c3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
@@ -14,12 +14,12 @@ import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.HostSpec;
import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeFlavors;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
+import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.transaction.Mutex;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.curator.mock.MockCurator;
@@ -189,7 +189,7 @@ public class MockNodeRepository extends NodeRepository {
activate(provisioner.prepare(zoneApp, zoneCluster, Capacity.fromRequiredNodeType(NodeType.host), null), zoneApp, provisioner);
ApplicationId app1Id = ApplicationId.from(TenantName.from("tenant1"), ApplicationName.from("application1"), InstanceName.from("instance1"));
- ClusterSpec cluster1Id = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("id1")).vespaVersion("6.42").loadBalancerSettings(new LoadBalancerSettings(List.of("arne"))).build();
+ ClusterSpec cluster1Id = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("id1")).vespaVersion("6.42").loadBalancerSettings(new ZoneEndpoint(List.of("arne"))).build();
activate(provisioner.prepare(app1Id,
cluster1Id,
Capacity.from(new ClusterResources(2, 1, new NodeResources(2, 8, 50, 1)),
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java
index 92c7ba7fe27..a646d26ea29 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/lb/SharedLoadBalancerServiceTest.java
@@ -5,7 +5,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.ZoneEndpoint;
import org.junit.Test;
import java.util.Optional;
@@ -29,7 +29,7 @@ public class SharedLoadBalancerServiceTest {
@Test
public void test_create_lb() {
var lb = loadBalancerService.create(new LoadBalancerSpec(applicationId, clusterId, reals,
- LoadBalancerSettings.empty, CloudAccount.empty),
+ ZoneEndpoint.defaultEndpoint, CloudAccount.empty),
false);
assertEquals(Optional.of(HostName.of("vip.example.com")), lb.hostname());
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java
index d5722a59f3e..dee895b02d2 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/LoadBalancerSerializerTest.java
@@ -6,7 +6,7 @@ import com.google.common.collect.ImmutableSet;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
+import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.vespa.hosted.provision.lb.DnsZone;
import com.yahoo.vespa.hosted.provision.lb.LoadBalancer;
import com.yahoo.vespa.hosted.provision.lb.LoadBalancerId;
@@ -46,7 +46,7 @@ public class LoadBalancerSerializerTest {
new Real(DomainName.of("real-2"),
"127.0.0.2",
4080)),
- new LoadBalancerSettings(List.of("123")),
+ new ZoneEndpoint(List.of("123")),
Optional.of(PrivateServiceId.of("foo")),
CloudAccount.from("012345678912"))),
LoadBalancer.State.active,
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
index e32643860f5..3653e20d848 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java
@@ -9,9 +9,9 @@ import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostSpec;
-import com.yahoo.config.provision.LoadBalancerSettings;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
+import com.yahoo.config.provision.ZoneEndpoint;
import com.yahoo.config.provision.exception.LoadBalancerServiceException;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.flags.InMemoryFlagSource;
@@ -215,7 +215,7 @@ public class LoadBalancerProvisionerTest {
public void provision_load_balancer_combined_cluster() {
Supplier<List<LoadBalancer>> lbs = () -> tester.nodeRepository().loadBalancers().list(app1).asList();
var combinedId = ClusterSpec.Id.from("container1");
- var nodes = prepare(app1, clusterRequest(ClusterSpec.Type.combined, ClusterSpec.Id.from("content1"), Optional.of(combinedId), LoadBalancerSettings.empty));
+ var nodes = prepare(app1, clusterRequest(ClusterSpec.Type.combined, ClusterSpec.Id.from("content1"), Optional.of(combinedId), ZoneEndpoint.defaultEndpoint));
assertEquals(1, lbs.get().size());
assertEquals("Prepare provisions load balancer with reserved nodes", 2, lbs.get().get(0).instance().get().reals().size());
tester.activate(app1, nodes);
@@ -320,10 +320,10 @@ public class LoadBalancerProvisionerTest {
tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"))));
LoadBalancerList loadBalancers = tester.nodeRepository().loadBalancers().list();
assertEquals(1, loadBalancers.size());
- assertEquals(LoadBalancerSettings.empty, loadBalancers.first().get().instance().get().settings());
+ assertEquals(ZoneEndpoint.defaultEndpoint, loadBalancers.first().get().instance().get().settings());
// Next deployment contains new settings
- LoadBalancerSettings settings = new LoadBalancerSettings(List.of("alice", "bob"));
+ ZoneEndpoint settings = new ZoneEndpoint(List.of("alice", "bob"));
tester.activate(app1, prepare(app1, capacity, clusterRequest(ClusterSpec.Type.container, ClusterSpec.Id.from("c1"), Optional.empty(), settings)));
loadBalancers = tester.nodeRepository().loadBalancers().list();
assertEquals(1, loadBalancers.size());
@@ -430,10 +430,10 @@ public class LoadBalancerProvisionerTest {
}
private static ClusterSpec clusterRequest(ClusterSpec.Type type, ClusterSpec.Id id) {
- return clusterRequest(type, id, Optional.empty(), LoadBalancerSettings.empty);
+ return clusterRequest(type, id, Optional.empty(), ZoneEndpoint.defaultEndpoint);
}
- private static ClusterSpec clusterRequest(ClusterSpec.Type type, ClusterSpec.Id id, Optional<ClusterSpec.Id> combinedId, LoadBalancerSettings settings) {
+ private static ClusterSpec clusterRequest(ClusterSpec.Type type, ClusterSpec.Id id, Optional<ClusterSpec.Id> combinedId, ZoneEndpoint settings) {
return ClusterSpec.request(type, id).vespaVersion("6.42").combinedId(combinedId).loadBalancerSettings(settings).build();
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/load-balancers.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/load-balancers.json
index bbccc72c7f9..becca98a913 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/load-balancers.json
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/load-balancers.json
@@ -30,7 +30,12 @@
}
],
"settings": {
- "allowedUrns": [ "arne" ]
+ "allowedUrns": [
+ {
+ "type": "aws-private-link",
+ "urn": "arne"
+ }
+ ]
},
"serviceId": "service"
},
diff --git a/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java
index f45f7f9d246..30ed8dcfdd4 100644
--- a/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java
+++ b/vespa-feed-client/src/test/java/ai/vespa/feed/client/impl/ApacheClusterTest.java
@@ -48,9 +48,9 @@ class ApacheClusterTest {
Map.of("name1", () -> "value1",
"name2", () -> "value2"),
"content".getBytes(UTF_8),
- Duration.ofSeconds(5)),
+ Duration.ofSeconds(10)),
vessel);
- HttpResponse response = vessel.get(5, TimeUnit.SECONDS);
+ HttpResponse response = vessel.get(15, TimeUnit.SECONDS);
assertEquals("{}", new String(response.body(), UTF_8));
assertEquals(200, response.code());