diff options
author | jonmv <venstad@gmail.com> | 2023-01-16 15:41:26 +0100 |
---|---|---|
committer | jonmv <venstad@gmail.com> | 2023-01-17 15:49:42 +0100 |
commit | 12ef34b0204e5acee25655139e7f6e79cefe983b (patch) | |
tree | 96c497e68be57d41f62ba9ae00b60042cf2ce32d | |
parent | 69d0f324263f0075a283b66bca6fab2a12b2b66e (diff) |
Parse, validate and use new zone endpoint syntax
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()); |