diff options
Diffstat (limited to 'config-model-api/src/main/java/com/yahoo')
5 files changed, 238 insertions, 36 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java index fdde4c38fb8..b36c1409459 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -3,17 +3,23 @@ package com.yahoo.config.application.api; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Tags; +import com.yahoo.config.provision.ZoneEndpoint; +import com.yahoo.config.provision.zone.ZoneId; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -26,7 +32,6 @@ import static ai.vespa.validation.Validation.requireInRange; import static com.yahoo.config.application.api.DeploymentSpec.RevisionChange.whenClear; import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.next; import static com.yahoo.config.provision.Environment.prod; -import static java.util.stream.Collectors.toList; /** * The deployment spec for an application instance @@ -55,6 +60,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { private final Optional<CloudAccount> cloudAccount; private final Notifications notifications; private final List<Endpoint> endpoints; + private final Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints; public DeploymentInstanceSpec(InstanceName name, Tags tags, @@ -70,6 +76,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { Optional<CloudAccount> cloudAccount, Notifications notifications, List<Endpoint> endpoints, + Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints, Instant now) { super(steps); this.name = Objects.requireNonNull(name); @@ -91,6 +98,9 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { this.cloudAccount = Objects.requireNonNull(cloudAccount); this.notifications = Objects.requireNonNull(notifications); this.endpoints = List.copyOf(Objects.requireNonNull(endpoints)); + Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpointsCopy = new HashMap<>(); + for (var entry : zoneEndpoints.entrySet()) zoneEndpointsCopy.put(entry.getKey(), Collections.unmodifiableMap(new HashMap<>(entry.getValue()))); + this.zoneEndpoints = Collections.unmodifiableMap(zoneEndpointsCopy); validateZones(new HashSet<>(), new HashSet<>(), this); validateEndpoints(steps(), globalServiceId, this.endpoints); validateChangeBlockers(changeBlockers, now); @@ -251,6 +261,25 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { return zones().stream().anyMatch(zone -> zone.concerns(environment, Optional.of(region))); } + /** Returns the zone endpoint specified for the given region, or empty. */ + Optional<ZoneEndpoint> zoneEndpoint(ZoneId zone, ClusterSpec.Id cluster) { + return Optional.ofNullable(zoneEndpoints.get(cluster)) + .filter(__ -> deploysTo(zone.environment(), zone.region())) + .map(zoneEndpoints -> zoneEndpoints.get(zoneEndpoints.containsKey(zone) ? zone : null)); + } + + /** Returns the zone endpoint data for this instance. */ + Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints() { + return zoneEndpoints; + } + + /** The zone endpoints in the given zone, possibly default values. */ + public Map<ClusterSpec.Id, ZoneEndpoint> zoneEndpoints(ZoneId zone) { + return zoneEndpoints.keySet().stream() + .collect(Collectors.toMap(cluster -> cluster, + cluster -> zoneEndpoint(zone, cluster).orElse(ZoneEndpoint.defaultEndpoint))); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index 6c519a4656e..d731e09d4e4 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -6,16 +6,21 @@ import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.ZoneEndpoint; +import com.yahoo.config.provision.zone.ZoneId; import java.io.Reader; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -47,7 +52,7 @@ public class DeploymentSpec { private final List<Step> steps; - // Attributes which can be set on the root tag and which must be available outside of any particular instance + // Attributes which can be set on the root tag and which must be available outside any particular instance private final Optional<Integer> majorVersion; private final Optional<AthenzDomain> athenzDomain; private final Optional<AthenzService> athenzService; @@ -145,6 +150,10 @@ public class DeploymentSpec { illegal(prefix + "targets undeclared region '" + target.region() + "' in instance '" + target.instance() + "'"); } + if (instance.get().zoneEndpoint(ZoneId.from(Environment.prod, target.region()), ClusterSpec.Id.from(endpoint.containerId())) + .map(zoneEndpoint -> ! zoneEndpoint.isPublicEndpoint()).orElse(false)) + illegal(prefix + "targets '" + target.region().value() + "' in '" + target.instance().value() + + "', but its zone endpoint has 'enabled' set to 'false'"); } } } @@ -175,6 +184,21 @@ public class DeploymentSpec { /** Cloud account set on the deployment root; see discussion for {@link #athenzService}. */ public Optional<CloudAccount> cloudAccount() { return cloudAccount; } + /** + * Returns the most specific zone endpoint, where specificity is given, in decreasing order: + * 1. The given instance has declared a zone endpoint for the cluster, for the given region. + * 2. The given instance has declared a universal zone endpoint for the cluster. + * 3. The application has declared a zone endpoint for the cluster, for the given region. + * 4. The application has declared a universal zone endpoint for the cluster. + * 5. None of the above apply, and the default of a publicly visible endpoint is used. + */ + public ZoneEndpoint zoneEndpoint(InstanceName instance, ZoneId zone, ClusterSpec.Id cluster) { + // TODO: look up endpoints from <dev> tag, or so, if we're to support non-prod settings. + if (zone.environment() != Environment.prod) return ZoneEndpoint.defaultEndpoint; + return instance(instance).flatMap(spec -> spec.zoneEndpoint(zone, cluster)) + .orElse(ZoneEndpoint.defaultEndpoint); + } + /** Returns the XML form of this spec, or null if it was not created by fromXml, nor is empty */ public String xmlForm() { return xmlForm; } diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java index 571cc3c7d5c..b4be99ad20b 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationId.java @@ -24,6 +24,7 @@ public enum ValidationId { skipOldConfigModels("skip-old-config-models"), // Internal use accessControl("access-control"), // Internal use, used in zones where there should be no access-control globalEndpointChange("global-endpoint-change"), // Changing global endpoints + zoneEndpointChange("zone-endpoint-change"), // Changing zone (possibly private) endpoint settings redundancyIncrease("redundancy-increase"), // Increasing redundancy - may easily cause feed blocked redundancyOne("redundancy-one"), // redundancy=1 requires a validation override on first deployment pagedSettingRemoval("paged-setting-removal"), // May cause content nodes to run out of memory diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 8182e697e7e..fb6d834f783 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -15,6 +15,8 @@ import com.yahoo.config.application.api.DeploymentSpec.Steps; import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; import com.yahoo.config.application.api.DeploymentSpec.UpgradeRollout; import com.yahoo.config.application.api.Endpoint; +import com.yahoo.config.application.api.Endpoint.Level; +import com.yahoo.config.application.api.Endpoint.Target; import com.yahoo.config.application.api.Notifications; import com.yahoo.config.application.api.Notifications.Role; import com.yahoo.config.application.api.Notifications.When; @@ -22,10 +24,15 @@ import com.yahoo.config.application.api.TimeWindow; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.Tags; +import com.yahoo.config.provision.ZoneEndpoint; +import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn; +import com.yahoo.config.provision.ZoneEndpoint.AccessType; +import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.io.IOUtils; import com.yahoo.text.XML; import org.w3c.dom.Element; @@ -39,16 +46,20 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Comparator.comparingInt; + /** * @author bratseth */ @@ -123,7 +134,7 @@ public class DeploymentSpecXmlReader { return DeploymentSpec.empty; List<Step> steps = new ArrayList<>(); - List<Endpoint> applicationEndpoints = List.of(); + List<Endpoint> applicationEndpoints = new ArrayList<>(); if ( ! containsTag(instanceTag, root)) { // deployment spec skipping explicit instance -> "default" instance steps.addAll(readInstanceContent("default", root, new HashMap<>(), root)); } @@ -141,7 +152,7 @@ public class DeploymentSpecXmlReader { steps.addAll(readNonInstanceSteps(child, new HashMap<>(), root)); // (No global service id here) } } - applicationEndpoints = readEndpoints(root, Optional.empty(), steps); + readEndpoints(root, Optional.empty(), steps, applicationEndpoints, Map.of()); } return new DeploymentSpec(steps, @@ -190,11 +201,13 @@ public class DeploymentSpecXmlReader { Notifications notifications = readNotifications(instanceElement, parentTag); // Values where there is no default - Tags tags = XML.attribute(tagsTag, instanceElement).map(value -> Tags.fromString(value)).orElse(Tags.empty()); + Tags tags = XML.attribute(tagsTag, instanceElement).map(Tags::fromString).orElse(Tags.empty()); List<Step> steps = new ArrayList<>(); for (Element instanceChild : XML.getChildren(instanceElement)) steps.addAll(readNonInstanceSteps(instanceChild, prodAttributes, instanceChild)); - List<Endpoint> endpoints = readEndpoints(instanceElement, Optional.of(instanceNameString), steps); + List<Endpoint> endpoints = new ArrayList<>(); + Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints = new LinkedHashMap<>(); + readEndpoints(instanceElement, Optional.of(instanceNameString), steps, endpoints, zoneEndpoints); // Build and return instances with these values Instant now = clock.instant(); @@ -214,6 +227,7 @@ public class DeploymentSpecXmlReader { cloudAccount, notifications, endpoints, + zoneEndpoints, now)) .toList(); } @@ -306,19 +320,54 @@ public class DeploymentSpecXmlReader { return Notifications.of(emailAddresses, emailRoles); } - private List<Endpoint> readEndpoints(Element parent, Optional<String> instance, List<Step> steps) { + private void readEndpoints(Element parent, Optional<String> instance, List<Step> steps, List<Endpoint> endpoints, + Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> zoneEndpoints) { var endpointsElement = XML.getChild(parent, endpointsTag); - if (endpointsElement == null) return List.of(); + if (endpointsElement == null) return; Endpoint.Level level = instance.isEmpty() ? Endpoint.Level.application : Endpoint.Level.instance; - Map<String, Endpoint> endpoints = new LinkedHashMap<>(); - for (var endpointElement : XML.getChildren(endpointsElement, endpointTag)) { - String endpointId = stringAttribute("id", endpointElement).orElse(Endpoint.DEFAULT_ID); + Map<String, Endpoint> endpointsById = new LinkedHashMap<>(); + Map<String, Map<RegionName, List<ZoneEndpoint>>> endpointsByZone = new LinkedHashMap<>(); + XML.getChildren(endpointsElement, endpointTag).stream() // Read zone settings first. + .sorted(comparingInt(endpoint -> getZoneEndpointType(endpoint, level).isPresent() ? 0 : 1)) + .forEach(endpointElement -> { String containerId = requireStringAttribute("container-id", endpointElement); + Optional<String> endpointId = stringAttribute("id", endpointElement); + Optional<String> zoneEndpointType = getZoneEndpointType(endpointElement, level); String msgPrefix = (level == Endpoint.Level.application ? "Application-level" : "Instance-level") + - " endpoint '" + endpointId + "': "; + " endpoint '" + endpointId.orElse(Endpoint.DEFAULT_ID) + "': "; + + if (zoneEndpointType.isPresent() && endpointId.isPresent()) + illegal(msgPrefix + "cannot declare 'id' with type 'zone' or 'private'"); + String invalidChild = level == Endpoint.Level.application ? "region" : "instance"; - if (!XML.getChildren(endpointElement, invalidChild).isEmpty()) illegal(msgPrefix + "invalid element '" + invalidChild + "'"); + if ( ! XML.getChildren(endpointElement, invalidChild).isEmpty()) + illegal(msgPrefix + "invalid element '" + invalidChild + "'"); + + boolean enabled = XML.attribute("enabled", endpointElement) + .map(value -> { + if (zoneEndpointType.isEmpty() || ! zoneEndpointType.get().equals("zone")) + illegal(msgPrefix + "only endpoints of type 'zone' can specify 'enabled'"); + + return switch (value) { + case "true" -> true; + case "false" -> false; + default -> throw new IllegalArgumentException(msgPrefix + "invalid 'enabled' value; must be 'true' or 'false'"); + }; + }).orElse(true); + + List<AllowedUrn> allowedUrns = new ArrayList<>(); + for (var allow : XML.getChildren(endpointElement, "allow")) { + if (zoneEndpointType.isEmpty() || ! zoneEndpointType.get().equals("private")) + illegal(msgPrefix + "only endpoints of type 'private' can specify 'allow' children"); + + switch (requireStringAttribute("with", allow)) { + case "aws-private-link" -> allowedUrns.add(new AllowedUrn(AccessType.awsPrivateLink, requireStringAttribute("arn", allow))); + case "gcp-service-connect" -> allowedUrns.add(new AllowedUrn(AccessType.gcpServiceConnect, requireStringAttribute("project", allow))); + default -> illegal("Private endpoint for container-id '" + containerId + "': " + + "invalid attribute 'with': '" + requireStringAttribute("with", allow) + "'"); + } + } List<Endpoint.Target> targets = new ArrayList<>(); if (level == Endpoint.Level.application) { @@ -345,33 +394,132 @@ public class DeploymentSpecXmlReader { if (weightSum == 0) illegal(msgPrefix + "sum of all weights must be positive, got " + weightSum); } else { if (stringAttribute("region", endpointElement).isPresent()) illegal(msgPrefix + "invalid 'region' attribute"); + Set<RegionName> regions = new LinkedHashSet<>(); for (var regionElement : XML.getChildren(endpointElement, "region")) { String region = regionElement.getTextContent(); - if (region == null || region.isBlank()) illegal(msgPrefix + "empty 'region' element"); - targets.add(new Endpoint.Target(RegionName.from(region), - InstanceName.from(instance.get()), - 1)); + if (region == null || region.isBlank()) + illegal(msgPrefix + "empty 'region' element"); + if ( zoneEndpointType.isEmpty() + && Stream.of(RegionName.from(region), null) + .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get) + .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream()) + .anyMatch(endpoint -> ! endpoint.isPublicEndpoint())) + illegal(msgPrefix + "targets zone endpoint in '" + region + "' with 'enabled' set to 'false'"); + if ( ! regions.add(RegionName.from(region))) + illegal(msgPrefix + "duplicate 'region' element: '" + region + "'"); + } + + if (zoneEndpointType.isPresent()) { + if (regions.isEmpty()) regions.add(null); + ZoneEndpoint endpoint = switch (zoneEndpointType.get()) { + case "zone" -> new ZoneEndpoint(enabled, false, List.of()); + case "private" -> new ZoneEndpoint(true, true, allowedUrns); // Doesn't turn off public visibility. + default -> throw new IllegalArgumentException("unsupported zone endpoint type '" + zoneEndpointType.get() + "'"); + }; + for (RegionName region : regions) endpointsByZone.computeIfAbsent(containerId, __ -> new LinkedHashMap<>()) + .computeIfAbsent(region, __ -> new ArrayList<>()) + .add(endpoint); + } + else { + if (regions.isEmpty()) { + // No explicit targets given for instance level endpoint. Include all declared, enabled regions by default + List<RegionName> declared = + steps.stream() + .filter(step -> step.concerns(Environment.prod)) + .flatMap(step -> step.zones().stream()) + .flatMap(zone -> zone.region().stream()) + .toList(); + if (declared.isEmpty()) illegal(msgPrefix + "no declared regions to target"); + + declared.stream().filter(region -> Stream.of(region, null) + .map(endpointsByZone.getOrDefault(containerId, new HashMap<>())::get) + .flatMap(maybeEndpoints -> maybeEndpoints == null ? Stream.empty() : maybeEndpoints.stream()) + .allMatch(ZoneEndpoint::isPublicEndpoint)) + .forEach(regions::add); + } + if (regions.isEmpty()) illegal(msgPrefix + "all eligible zone endpoints have 'enabled' set to 'false'"); + InstanceName instanceName = instance.map(InstanceName::from).get(); + for (RegionName region : regions) targets.add(new Target(region, instanceName, 1)); } - } - if (targets.isEmpty() && level == Endpoint.Level.instance) { - // No explicit targets given for instance level endpoint. Include all declared regions by default - InstanceName instanceName = instance.map(InstanceName::from).get(); - steps.stream() - .filter(step -> step.concerns(Environment.prod)) - .flatMap(step -> step.zones().stream()) - .flatMap(zone -> zone.region().stream()) - .distinct() - .map(region -> new Endpoint.Target(region, instanceName, 1)) - .forEach(targets::add); } - Endpoint endpoint = new Endpoint(endpointId, containerId, level, targets); - if (endpoints.containsKey(endpoint.endpointId())) { - illegal("Endpoint ID '" + endpoint.endpointId() + "' is specified multiple times"); + if (zoneEndpointType.isEmpty()) { + Endpoint endpoint = new Endpoint(endpointId.orElse(Endpoint.DEFAULT_ID), containerId, level, targets); + if (endpointsById.containsKey(endpoint.endpointId())) { + illegal("Endpoint ID '" + endpoint.endpointId() + "' is specified multiple times"); + } + endpointsById.put(endpoint.endpointId(), endpoint); } - endpoints.put(endpoint.endpointId(), endpoint); - } - return List.copyOf(endpoints.values()); + }); + endpoints.addAll(endpointsById.values()); + validateAndConsolidate(endpointsByZone, zoneEndpoints); + } + + static void validateAndConsolidate(Map<String, Map<RegionName, List<ZoneEndpoint>>> in, Map<ClusterSpec.Id, Map<ZoneId, ZoneEndpoint>> out) { + in.forEach((cluster, regions) -> { + List<ZoneEndpoint> wildcards = regions.remove(null); + ZoneEndpoint wildcardZoneEndpoint = null; + ZoneEndpoint wildcardPrivateEndpoint = null; + if (wildcards != null) { + for (ZoneEndpoint endpoint : wildcards) { + if (endpoint.isPrivateEndpoint()) { + if (wildcardPrivateEndpoint != null) illegal("Multiple private endpoints (for all regions) declared for " + + "container id '" + cluster + "'"); + wildcardPrivateEndpoint = endpoint; + } + else { + if (wildcardZoneEndpoint != null) illegal("Multiple zone endpoints (for all regions) declared " + + "for container id '" + cluster + "'"); + wildcardZoneEndpoint = endpoint; + } + } + } + for (RegionName region : regions.keySet()) { + ZoneEndpoint zoneEndpoint = null; + ZoneEndpoint privateEndpoint = null; + for (ZoneEndpoint endpoint : regions.getOrDefault(region, List.of())) { + if (endpoint.isPrivateEndpoint()) { + if (privateEndpoint != null) illegal("Multiple private endpoints declared for " + + "container id '" + cluster + "' in region '" + region + "'"); + privateEndpoint = endpoint; + } + else { + if (zoneEndpoint != null) illegal("Multiple zone endpoints (without regions) declared " + + "for container id '" + cluster + "' in region '" + region + "'"); + zoneEndpoint = endpoint; + } + } + if (wildcardZoneEndpoint != null && zoneEndpoint != null) illegal("Zone endpoint for container id '" + cluster + "' declared " + + "both with region '" + region + "', and for all regions."); + if (wildcardPrivateEndpoint != null && privateEndpoint != null) illegal("Private endpoint for container id '" + cluster + "' declared " + + "both with region '" + region + "', and for all regions."); + + if (zoneEndpoint == null) zoneEndpoint = wildcardZoneEndpoint; + if (privateEndpoint == null) privateEndpoint = wildcardPrivateEndpoint; + + // Gosh, we made it here! Now we'll combine the settings for zone and private types into one ZoneEndpoint to rule them all. + out.computeIfAbsent(ClusterSpec.Id.from(cluster), __ -> new LinkedHashMap<>()) + .put(ZoneId.from(Environment.prod, region), new ZoneEndpoint(zoneEndpoint == null || zoneEndpoint.isPublicEndpoint(), + privateEndpoint != null, + privateEndpoint != null ? privateEndpoint.allowedUrns() : List.of())); + } + out.computeIfAbsent(ClusterSpec.Id.from(cluster), __ -> new LinkedHashMap<>()) + .put(null, new ZoneEndpoint(wildcardZoneEndpoint == null || wildcardZoneEndpoint.isPublicEndpoint(), + wildcardPrivateEndpoint != null, + wildcardPrivateEndpoint != null ? wildcardPrivateEndpoint.allowedUrns() : List.of())); + }); + } + + /** Returns endpoint type if a private or zone type endpoint, throws if invalid, or otherwise returns empty (global, application). */ + static Optional<String> getZoneEndpointType(Element endpoint, Level level) { + Optional<String> type = XML.attribute("type", endpoint); + if (type.isPresent() && ! List.of("zone", "private", "global", "application").contains(type.get())) + illegal("Illegal endpoint type '" + type.get() + "'"); + + String implied = switch (level) { case instance -> "global"; case application -> "application"; }; + if (type.isEmpty() || type.get().equals(implied)) return Optional.empty(); + if (level == Level.instance && (type.get().equals("zone") || type.get().equals("private"))) return type; + throw new IllegalArgumentException("Endpoints at " + level + " level cannot be of type '" + type.get() + "'"); } /** diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java index 99bf768f67e..b7969267328 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ApplicationClusterEndpoint.java @@ -26,7 +26,7 @@ public class ApplicationClusterEndpoint { ", routingMethod=" + routingMethod + ", weight=" + weight + ", hostNames=" + hostNames + - ", clusterId='" + clusterId + '\'' + + ", clusterId='" + clusterId + "'" + '}'; } |