aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java160
1 files changed, 101 insertions, 59 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
index b1ffce65852..f11d67762ad 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
@@ -1,4 +1,4 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
import com.google.common.hash.HashCode;
@@ -14,9 +14,9 @@ import com.yahoo.config.provision.zone.AuthMethod;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.BooleanFlag;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.StringFlag;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
@@ -32,6 +32,7 @@ import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
+import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
import com.yahoo.vespa.hosted.controller.routing.GeneratedEndpointList;
import com.yahoo.vespa.hosted.controller.routing.PreparedEndpoints;
import com.yahoo.vespa.hosted.controller.routing.RoutingId;
@@ -51,7 +52,6 @@ import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -64,6 +64,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -79,11 +81,12 @@ import static java.util.stream.Collectors.toMap;
*/
public class RoutingController {
+ private static final Logger LOG = Logger.getLogger(RoutingController.class.getName());
+
private final Controller controller;
private final RoutingPolicies routingPolicies;
private final RotationRepository rotationRepository;
- private final BooleanFlag generatedEndpoints;
- private final BooleanFlag legacyEndpoints;
+ private final StringFlag endpointConfig;
public RoutingController(Controller controller, RotationsConfig rotationsConfig) {
this.controller = Objects.requireNonNull(controller, "controller must be non-null");
@@ -91,8 +94,7 @@ public class RoutingController {
this.rotationRepository = new RotationRepository(Objects.requireNonNull(rotationsConfig, "rotationsConfig must be non-null"),
controller.applications(),
controller.curator());
- this.generatedEndpoints = Flags.RANDOMIZED_ENDPOINT_NAMES.bindTo(controller.flagSource());
- this.legacyEndpoints = Flags.LEGACY_ENDPOINTS.bindTo(controller.flagSource());
+ this.endpointConfig = Flags.ENDPOINT_CONFIG.bindTo(controller.flagSource());
}
/** Create a routing context for given deployment */
@@ -122,8 +124,23 @@ public class RoutingController {
return rotationRepository;
}
+ /** Returns the endpoint config to use for given instance */
+ public EndpointConfig endpointConfig(ApplicationId instance) {
+ String flagValue = endpointConfig.with(FetchVector.Dimension.TENANT_ID, instance.tenant().value())
+ .with(FetchVector.Dimension.APPLICATION_ID, TenantAndApplicationId.from(instance).serialized())
+ .with(FetchVector.Dimension.INSTANCE_ID, instance.serializedForm())
+ .value();
+ return switch (flagValue) {
+ case "legacy" -> EndpointConfig.legacy;
+ case "combined" -> EndpointConfig.combined;
+ case "generated" -> EndpointConfig.generated;
+ default -> throw new IllegalArgumentException("Invalid endpoint-config flag value: '" + flagValue + "', must be " +
+ "'legacy', 'combined' or 'generated'");
+ };
+ }
+
/** Prepares and returns the endpoints relevant for given deployment */
- public PreparedEndpoints prepare(DeploymentId deployment, BasicServicesXml services, Optional<EndpointCertificate> certificate, LockedApplication application) {
+ public PreparedEndpoints prepare(DeploymentId deployment, BasicServicesXml services, EndpointCertificate certificate, LockedApplication application) {
EndpointList endpoints = EndpointList.EMPTY;
DeploymentSpec spec = application.get().deploymentSpec();
@@ -135,9 +152,9 @@ public class RoutingController {
}
// Add zone-scoped endpoints
- Map<EndpointId, GeneratedEndpointList> generatedForDeclaredEndpoints = new HashMap<>();
+ Map<EndpointId, List<GeneratedEndpoint>> generatedForDeclaredEndpoints = new HashMap<>();
Set<ClusterSpec.Id> clustersWithToken = new HashSet<>();
- boolean generatedEndpointsEnabled = generatedEndpointsEnabled(deployment.applicationId());
+ EndpointConfig config = endpointConfig(deployment.applicationId());
RoutingPolicyList applicationPolicies = policies().read(TenantAndApplicationId.from(deployment.applicationId()));
RoutingPolicyList deploymentPolicies = applicationPolicies.deployment(deployment);
for (var container : services.containers()) {
@@ -149,11 +166,12 @@ public class RoutingController {
Optional<RoutingPolicy> clusterPolicy = deploymentPolicies.cluster(clusterId).first();
List<GeneratedEndpoint> generatedForCluster = clusterPolicy.map(policy -> policy.generatedEndpoints().cluster().asList())
.orElseGet(List::of);
- // Generate endpoints if cluster does not have any
- if (generatedForCluster.isEmpty()) {
- generatedForCluster = generateEndpoints(tokenSupported, certificate, Optional.empty());
+ // Generate endpoint for each auth method, if not present
+ generatedForCluster = generateEndpoints(AuthMethod.mtls, certificate, Optional.empty(), generatedForCluster);
+ if (tokenSupported) {
+ generatedForCluster = generateEndpoints(AuthMethod.token, certificate, Optional.empty(), generatedForCluster);
}
- GeneratedEndpointList generatedEndpoints = generatedEndpointsEnabled ? GeneratedEndpointList.copyOf(generatedForCluster) : GeneratedEndpointList.EMPTY;
+ GeneratedEndpointList generatedEndpoints = config.supportsGenerated() ? GeneratedEndpointList.copyOf(generatedForCluster) : GeneratedEndpointList.EMPTY;
endpoints = endpoints.and(endpointsOf(deployment, clusterId, generatedEndpoints).scope(Scope.zone));
}
@@ -162,18 +180,34 @@ public class RoutingController {
ClusterSpec.Id clusterId = ClusterSpec.Id.from(container.id());
applicationPolicies.cluster(clusterId).asList().stream()
.flatMap(policy -> policy.generatedEndpoints().declared().asList().stream())
- .forEach(ge -> generatedForDeclaredEndpoints.computeIfAbsent(ge.endpoint().get(), (k) -> GeneratedEndpointList.of(ge)));
+ .forEach(ge -> {
+ List<GeneratedEndpoint> generated = generatedForDeclaredEndpoints.computeIfAbsent(ge.endpoint().get(), (k) -> new ArrayList<>());
+ if (!generated.contains(ge)) {
+ generated.add(ge);
+ }
+ });
}
// Generate endpoints if declared endpoint does not have any
Stream.concat(spec.endpoints().stream(), spec.instances().stream().flatMap(i -> i.endpoints().stream()))
.forEach(endpoint -> {
EndpointId endpointId = EndpointId.of(endpoint.endpointId());
- generatedForDeclaredEndpoints.computeIfAbsent(endpointId, (k) -> {
+ generatedForDeclaredEndpoints.compute(endpointId, (k, old) -> {
+ if (old == null) {
+ old = List.of();
+ }
+ List<GeneratedEndpoint> generatedEndpoints = generateEndpoints(AuthMethod.mtls, certificate, Optional.of(endpointId), old);
boolean tokenSupported = clustersWithToken.contains(ClusterSpec.Id.from(endpoint.containerId()));
- return generatedEndpointsEnabled ? GeneratedEndpointList.copyOf(generateEndpoints(tokenSupported, certificate, Optional.of(endpointId))) : null;
+ if (tokenSupported){
+ generatedEndpoints = generateEndpoints(AuthMethod.token, certificate, Optional.of(endpointId), generatedEndpoints);
+ }
+ return generatedEndpoints;
});
});
- Map<EndpointId, GeneratedEndpointList> generatedEndpoints = generatedEndpointsEnabled ? generatedForDeclaredEndpoints : Map.of();
+ Map<EndpointId, GeneratedEndpointList> generatedEndpoints = config.supportsGenerated()
+ ? generatedForDeclaredEndpoints.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, kv -> GeneratedEndpointList.copyOf(kv.getValue())))
+ : Map.of();
endpoints = endpoints.and(declaredEndpointsOf(application.get().id(), spec, generatedEndpoints).targets(deployment));
PreparedEndpoints prepared = new PreparedEndpoints(deployment,
endpoints,
@@ -183,13 +217,9 @@ public class RoutingController {
// Register rotation-backed endpoints in DNS
registerRotationEndpointsInDns(prepared);
- return prepared;
- }
+ LOG.log(Level.FINE, () -> "Prepared endpoints: " + prepared);
- private List<GeneratedEndpoint> generateEndpoints(boolean tokenSupported, Optional<EndpointCertificate> certificate, Optional<EndpointId> endpoint) {
- return certificate.flatMap(EndpointCertificate::randomizedId)
- .map(id -> generateEndpoints(id, tokenSupported, endpoint))
- .orElseGet(List::of);
+ return prepared;
}
// -------------- Implicit endpoints (scopes 'zone' and 'weighted') --------------
@@ -197,19 +227,22 @@ public class RoutingController {
/** Returns the zone- and region-scoped endpoints of given deployment */
public EndpointList endpointsOf(DeploymentId deployment, ClusterSpec.Id cluster, GeneratedEndpointList generatedEndpoints) {
requireGeneratedEndpoints(generatedEndpoints, false);
+ boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
boolean tokenSupported = !generatedEndpoints.authMethod(AuthMethod.token).isEmpty();
- RoutingMethod routingMethod = controller.zoneRegistry().routingMethod(deployment.zoneId());
boolean isProduction = deployment.zoneId().environment().isProduction();
+ RoutingMethod routingMethod = controller.zoneRegistry().routingMethod(deployment.zoneId());
List<Endpoint> endpoints = new ArrayList<>();
Endpoint.EndpointBuilder zoneEndpoint = Endpoint.of(deployment.applicationId())
.routingMethod(routingMethod)
.on(Port.fromRoutingMethod(routingMethod))
+ .legacy(generatedEndpointsAvailable)
.target(cluster, deployment);
endpoints.add(zoneEndpoint.in(controller.system()));
ZoneApi zone = controller.zoneRegistry().zones().all().get(deployment.zoneId()).get();
Endpoint.EndpointBuilder regionEndpoint = Endpoint.of(deployment.applicationId())
.routingMethod(routingMethod)
.on(Port.fromRoutingMethod(routingMethod))
+ .legacy(generatedEndpointsAvailable)
.targetRegion(cluster,
zone.getCloudNativeRegionName(),
zone.getCloudName());
@@ -226,12 +259,14 @@ public class RoutingController {
};
if (include) {
endpoints.add(zoneEndpoint.generatedFrom(generatedEndpoint)
+ .legacy(false)
.authMethod(generatedEndpoint.authMethod())
.in(controller.system()));
// Only a single region endpoint is needed, not one per auth method
if (isProduction && generatedEndpoint.authMethod() == AuthMethod.mtls) {
GeneratedEndpoint weightedGeneratedEndpoint = generatedEndpoint.withClusterPart(weightedClusterPart(cluster, deployment));
endpoints.add(regionEndpoint.generatedFrom(weightedGeneratedEndpoint)
+ .legacy(false)
.authMethod(AuthMethod.none)
.in(controller.system()));
}
@@ -257,6 +292,7 @@ public class RoutingController {
var endpoints = new ArrayList<Endpoint>();
var directMethods = 0;
var availableRoutingMethods = routingMethodsOfAll(deployments);
+ boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
for (var method : availableRoutingMethods) {
if (method.isDirect() && ++directMethods > 1) {
throw new IllegalArgumentException("Invalid routing methods for " + routingId + ": Exceeded maximum " +
@@ -265,10 +301,11 @@ public class RoutingController {
Endpoint.EndpointBuilder builder = Endpoint.of(routingId.instance())
.target(routingId.endpointId(), cluster, deployments)
.on(Port.fromRoutingMethod(method))
+ .legacy(generatedEndpointsAvailable)
.routingMethod(method);
endpoints.add(builder.in(controller.system()));
for (var ge : generatedEndpoints) {
- endpoints.add(builder.generatedFrom(ge).authMethod(ge.authMethod()).in(controller.system()));
+ endpoints.add(builder.generatedFrom(ge).legacy(false).authMethod(ge.authMethod()).in(controller.system()));
}
}
return filterEndpoints(routingId.instance(), EndpointList.copyOf(endpoints));
@@ -280,16 +317,18 @@ public class RoutingController {
requireGeneratedEndpoints(generatedEndpoints, true);
ZoneId zone = deployments.keySet().iterator().next().zoneId(); // Where multiple zones are possible, they all have the same routing method.
RoutingMethod routingMethod = usesSharedRouting(zone) ? RoutingMethod.sharedLayer4 : RoutingMethod.exclusive;
+ boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
Endpoint.EndpointBuilder builder = Endpoint.of(application)
.targetApplication(endpoint,
cluster,
deployments)
.routingMethod(routingMethod)
+ .legacy(generatedEndpointsAvailable)
.on(Port.fromRoutingMethod(routingMethod));
List<Endpoint> endpoints = new ArrayList<>();
endpoints.add(builder.in(controller.system()));
for (var ge : generatedEndpoints) {
- endpoints.add(builder.generatedFrom(ge).authMethod(ge.authMethod()).in(controller.system()));
+ endpoints.add(builder.generatedFrom(ge).legacy(false).authMethod(ge.authMethod()).in(controller.system()));
}
return EndpointList.copyOf(endpoints);
}
@@ -361,7 +400,24 @@ public class RoutingController {
}
/** Returns certificate DNS names (CN and SAN values) for given deployment */
- public List<String> certificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec) {
+ public List<String> certificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec, String generatedId, boolean legacy) {
+ List<String> endpointDnsNames = new ArrayList<>();
+ if (legacy) {
+ endpointDnsNames.addAll(legacyCertificateDnsNames(deployment, deploymentSpec));
+ }
+ for (Scope scope : List.of(Scope.zone, Scope.global, Scope.application)) {
+ endpointDnsNames.add(Endpoint.of(deployment.applicationId())
+ .wildcardGenerated(generatedId, scope)
+ .routingMethod(RoutingMethod.exclusive)
+ .on(Port.tls())
+ .certificateName()
+ .in(controller.system())
+ .dnsName());
+ }
+ return Collections.unmodifiableList(endpointDnsNames);
+ }
+
+ private List<String> legacyCertificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec) {
List<String> endpointDnsNames = new ArrayList<>();
// We add first an endpoint name based on a hash of the application ID,
@@ -428,10 +484,7 @@ public class RoutingController {
}
private EndpointList filterEndpoints(ApplicationId instance, EndpointList endpoints) {
- if (generatedEndpointsEnabled(instance) && !legacyEndpointsEnabled(instance)) {
- return endpoints.generated();
- }
- return endpoints;
+ return endpointConfig(instance) == EndpointConfig.generated ? endpoints.generated() : endpoints;
}
private void registerRotationEndpointsInDns(PreparedEndpoints prepared) {
@@ -471,19 +524,22 @@ public class RoutingController {
}
}
- /** Generate endpoints for all authentication methods, using given application part */
- private List<GeneratedEndpoint> generateEndpoints(String applicationPart, boolean token, Optional<EndpointId> endpoint) {
- return Arrays.stream(AuthMethod.values())
- .filter(method -> switch (method) {
- case token -> token;
- case mtls -> true;
- case none -> false;
- })
- .map(method -> new GeneratedEndpoint(GeneratedEndpoint.createPart(controller.random(true)),
- applicationPart,
- method,
- endpoint))
- .toList();
+ /** Returns generated endpoints. A new endpoint is generated if no matching endpoint already exists */
+ private List<GeneratedEndpoint> generateEndpoints(AuthMethod authMethod, EndpointCertificate certificate,
+ Optional<EndpointId> declaredEndpoint,
+ List<GeneratedEndpoint> current) {
+ if (current.stream().anyMatch(e -> e.authMethod() == authMethod && e.endpoint().equals(declaredEndpoint))) {
+ return current;
+ }
+ Optional<String> applicationPart = certificate.generatedId();
+ if (applicationPart.isPresent()) {
+ current = new ArrayList<>(current);
+ current.add(new GeneratedEndpoint(GeneratedEndpoint.createPart(controller.random(true)),
+ applicationPart.get(),
+ authMethod,
+ declaredEndpoint));
+ }
+ return current;
}
/** Generate the cluster part of a {@link GeneratedEndpoint} for use in a {@link Endpoint.Scope#weighted} endpoint */
@@ -550,20 +606,6 @@ public class RoutingController {
return Collections.unmodifiableList(routingMethods);
}
- public boolean generatedEndpointsEnabled(ApplicationId instance) {
- return generatedEndpoints.with(FetchVector.Dimension.INSTANCE_ID, instance.serializedForm())
- .with(FetchVector.Dimension.TENANT_ID, instance.tenant().value())
- .with(FetchVector.Dimension.APPLICATION_ID, TenantAndApplicationId.from(instance).serialized())
- .value();
- }
-
- public boolean legacyEndpointsEnabled(ApplicationId instance) {
- return legacyEndpoints.with(FetchVector.Dimension.INSTANCE_ID, instance.serializedForm())
- .with(FetchVector.Dimension.TENANT_ID, instance.tenant().value())
- .with(FetchVector.Dimension.APPLICATION_ID, TenantAndApplicationId.from(instance).serialized())
- .value();
- }
-
private static void requireGeneratedEndpoints(GeneratedEndpointList generatedEndpoints, boolean declared) {
if (generatedEndpoints.asList().stream().anyMatch(ge -> ge.declared() != declared)) {
throw new IllegalStateException("All generated endpoints require declared=" + declared +