diff options
author | Morten Tokle <mortent@verizonmedia.com> | 2021-11-17 09:22:37 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-17 09:22:37 +0100 |
commit | 19339fff868707dbcd08efa910d67ba20f7154d1 (patch) | |
tree | d98abb474b85f9a6267156b1ce621cb7e352be67 /controller-server | |
parent | ac75c95519127d3bcc56e8ae30c8b429e0ef51b5 (diff) | |
parent | 803d44ee818d1ebeaf8e7aa16a0630c0a040a60c (diff) |
Merge pull request #20038 from vespa-engine/mpolden/add-names-to-cert
Include declared application endpoints in certificate
Diffstat (limited to 'controller-server')
7 files changed, 174 insertions, 72 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 2d7f9b27334..01b12a5136d 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -376,7 +376,7 @@ public class ApplicationController { && run.testerCertificate().isPresent()) applicationPackage = applicationPackage.withTrustedCertificate(run.testerCertificate().get()); - endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone, applicationPackage.deploymentSpec().instance(instance.name())); + endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone, applicationPackage.deploymentSpec()); containerEndpoints = controller.routing().containerEndpointsOf(application.get(), job.application().instance(), zone); 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 16f12b3ac07..37266dd6dac 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 @@ -173,27 +173,41 @@ public class RoutingController { } /** Returns certificate DNS names (CN and SAN values) for given deployment */ - public List<String> certificateDnsNames(DeploymentId deployment) { + public List<String> certificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec) { List<String> endpointDnsNames = new ArrayList<>(); // We add first an endpoint name based on a hash of the application ID, // as the certificate provider requires the first CN to be < 64 characters long. endpointDnsNames.add(commonNameHashOf(deployment.applicationId(), controller.system())); - // Add wildcard names for global endpoints when deploying to production List<Endpoint.EndpointBuilder> builders = new ArrayList<>(); if (deployment.zoneId().environment().isProduction()) { + // Add default and wildcard names for global endpoints builders.add(Endpoint.of(deployment.applicationId()).target(EndpointId.defaultId())); builders.add(Endpoint.of(deployment.applicationId()).wildcard()); + + // Add default and wildcard names for each region targeted by application endpoints + List<DeploymentId> deploymentTargets = deploymentSpec.endpoints().stream() + .map(com.yahoo.config.application.api.Endpoint::targets) + .flatMap(Collection::stream) + .map(com.yahoo.config.application.api.Endpoint.Target::region) + .distinct() + .map(region -> new DeploymentId(deployment.applicationId(), ZoneId.from(Environment.prod, region))) + .collect(Collectors.toUnmodifiableList()); + for (var targetDeployment : deploymentTargets) { + builders.add(Endpoint.of(targetDeployment.applicationId()).targetApplication(EndpointId.defaultId(), targetDeployment)); + builders.add(Endpoint.of(targetDeployment.applicationId()).wildcardApplication(targetDeployment)); + } } - // Add wildcard names for zone endpoints + // Add default and wildcard names for zone endpoints builders.add(Endpoint.of(deployment.applicationId()).target(ClusterSpec.Id.from("default"), deployment)); builders.add(Endpoint.of(deployment.applicationId()).wildcard(deployment)); - // Build all endpoints + // Build all certificate names for (var builder : builders) { - Endpoint endpoint = builder.routingMethod(RoutingMethod.exclusive) + Endpoint endpoint = builder.certificateName() + .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(controller.system()); endpointDnsNames.add(endpoint.dnsName()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java index 35601dd94dd..1e917dd1376 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java @@ -46,7 +46,7 @@ public class Endpoint { private Endpoint(TenantAndApplicationId application, Optional<InstanceName> instanceName, EndpointId id, ClusterSpec.Id cluster, URI url, List<Target> targets, Scope scope, Port port, boolean legacy, - RoutingMethod routingMethod) { + RoutingMethod routingMethod, boolean certificateName) { Objects.requireNonNull(application, "application must be non-null"); Objects.requireNonNull(instanceName, "instanceName must be non-null"); Objects.requireNonNull(cluster, "cluster must be non-null"); @@ -56,6 +56,9 @@ public class Endpoint { Objects.requireNonNull(routingMethod, "routingMethod must be non-null"); if (scope.multiDeployment()) { if (id == null) throw new IllegalArgumentException("Endpoint ID must be set for multi-deployment endpoints"); + if (!certificateName && targets.isEmpty()) { + throw new IllegalArgumentException("At least one target must be given for " + scope + " endpoints"); + } } else { if (scope == Scope.zone && id != null) throw new IllegalArgumentException("Endpoint ID cannot be set for " + scope + " endpoints"); if (targets.size() != 1) throw new IllegalArgumentException("A single target must be given for " + scope + " endpoints"); @@ -89,11 +92,14 @@ public class Endpoint { this.legacy = legacy; this.routingMethod = routingMethod; this.tls = port.tls; + if (!certificateName && "*".equals(name())) { + throw new IllegalArgumentException("Wildcard found in endpoint that is not intended as a certificate name"); + } } private Endpoint(EndpointId id, ClusterSpec.Id cluster, TenantAndApplicationId application, Optional<InstanceName> instance, List<Target> targets, Scope scope, SystemName system, Port port, - boolean legacy, RoutingMethod routingMethod) { + boolean legacy, RoutingMethod routingMethod, boolean certificateName) { this(application, instance, id, @@ -107,7 +113,7 @@ public class Endpoint { Objects.requireNonNull(port, "port must be non-null"), legacy, routingMethod), - targets, scope, port, legacy, routingMethod); + targets, scope, port, legacy, routingMethod, certificateName); } /** @@ -446,7 +452,7 @@ public class Endpoint { ClusterSpec.Id.from("admin"), url, List.of(new Target(new DeploymentId(systemApplication.id(), zone))), - Scope.zone, port, false, routingMethod); + Scope.zone, port, false, routingMethod, false); } /** A target of an endpoint */ @@ -491,6 +497,7 @@ public class Endpoint { private Port port; private RoutingMethod routingMethod = RoutingMethod.shared; private boolean legacy = false; + private boolean certificateName = false; private EndpointBuilder(TenantAndApplicationId application, Optional<InstanceName> instance) { this.application = Objects.requireNonNull(application); @@ -499,33 +506,41 @@ public class Endpoint { /** Sets the deployment target for this */ public EndpointBuilder target(ClusterSpec.Id cluster, DeploymentId deployment) { - checkScope(); this.cluster = cluster; - this.scope = Scope.zone; + this.scope = requireUnset(Scope.zone); this.targets = List.of(new Target(deployment)); return this; } - /** Sets the global target with given ID and pointing to the default cluster */ - public EndpointBuilder target(EndpointId endpointId) { - return target(endpointId, ClusterSpec.Id.from("default"), List.of()); - } - /** Sets the global target with given ID, deployments and cluster (as defined in deployments.xml) */ public EndpointBuilder target(EndpointId endpointId, ClusterSpec.Id cluster, List<DeploymentId> deployments) { - checkScope(); this.endpointId = endpointId; this.cluster = cluster; this.targets = deployments.stream().map(Target::new).collect(Collectors.toUnmodifiableList()); - this.scope = Scope.global; + this.scope = requireUnset(Scope.global); return this; } + /** Sets the global target with given ID and pointing to the default cluster */ + public EndpointBuilder target(EndpointId endpointId) { + return target(endpointId, ClusterSpec.Id.from("default"), List.of()); + } + + /** Sets the application target with given ID and pointing to the default cluster */ + public EndpointBuilder targetApplication(EndpointId endpointId, DeploymentId deployment) { + return targetApplication(endpointId, ClusterSpec.Id.from("default"), Map.of(deployment, 1)); + } + /** Sets the global wildcard target for this */ public EndpointBuilder wildcard() { return target(EndpointId.of("*"), ClusterSpec.Id.from("*"), List.of()); } + /** Sets the application wildcard target for this */ + public EndpointBuilder wildcardApplication(DeploymentId deployment) { + return targetApplication(EndpointId.of("*"), ClusterSpec.Id.from("*"), Map.of(deployment, 1)); + } + /** Sets the zone wildcard target for this */ public EndpointBuilder wildcard(DeploymentId deployment) { return target(ClusterSpec.Id.from("*"), deployment); @@ -533,7 +548,6 @@ public class Endpoint { /** Sets the application target with given ID, cluster, deployments and their weights */ public EndpointBuilder targetApplication(EndpointId endpointId, ClusterSpec.Id cluster, Map<DeploymentId, Integer> deployments) { - checkScope(); this.endpointId = endpointId; this.cluster = cluster; this.targets = deployments.entrySet().stream() @@ -545,9 +559,8 @@ public class Endpoint { /** Sets the region target for this, deduced from given zone */ public EndpointBuilder targetRegion(ClusterSpec.Id cluster, ZoneId zone) { - checkScope(); this.cluster = cluster; - this.scope = Scope.weighted; + this.scope = requireUnset(Scope.weighted); this.targets = List.of(new Target(new DeploymentId(application.instance(instance.get()), effectiveZone(zone)))); return this; } @@ -570,6 +583,12 @@ public class Endpoint { return this; } + /** Sets whether we're building a name for inclusion in a certificate */ + public EndpointBuilder certificateName() { + this.certificateName = true; + return this; + } + /** Sets the system that owns this */ public Endpoint in(SystemName system) { if (system.isPublic() && routingMethod != RoutingMethod.exclusive) { @@ -578,13 +597,14 @@ public class Endpoint { if (routingMethod.isDirect() && !port.isDefault()) { throw new IllegalArgumentException("Routing method " + routingMethod + " can only use default port"); } - return new Endpoint(endpointId, cluster, application, instance, targets, scope, system, port, legacy, routingMethod); + return new Endpoint(endpointId, cluster, application, instance, targets, scope, system, port, legacy, routingMethod, certificateName); } - private void checkScope() { - if (scope != null) { + private Scope requireUnset(Scope scope) { + if (this.scope != null) { throw new IllegalArgumentException("Cannot change endpoint scope. Already set to " + scope); } + return scope; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java index 684648ed70a..e3091b704e4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.certificate; import com.yahoo.config.application.api.DeploymentInstanceSpec; +import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.text.Text; import com.yahoo.vespa.hosted.controller.Controller; @@ -52,9 +53,9 @@ public class EndpointCertificates { } /** Returns certificate metadata for endpoints of given instance and zone */ - public Optional<EndpointCertificateMetadata> getMetadata(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) { + public Optional<EndpointCertificateMetadata> getMetadata(Instance instance, ZoneId zone, DeploymentSpec deploymentSpec) { Instant start = clock.instant(); - Optional<EndpointCertificateMetadata> metadata = getOrProvision(instance, zone, instanceSpec); + Optional<EndpointCertificateMetadata> metadata = getOrProvision(instance, zone, deploymentSpec); metadata.ifPresent(m -> curator.writeEndpointCertificateMetadata(instance.id(), m.withLastRequested(clock.instant().getEpochSecond()))); Duration duration = Duration.between(start, clock.instant()); if (duration.toSeconds() > 30) @@ -62,13 +63,12 @@ public class EndpointCertificates { return metadata; } - private Optional<EndpointCertificateMetadata> getOrProvision(Instance instance, ZoneId zone, Optional<DeploymentInstanceSpec> instanceSpec) { - final var currentCertificateMetadata = curator.readEndpointCertificateMetadata(instance.id()); - + private Optional<EndpointCertificateMetadata> getOrProvision(Instance instance, ZoneId zone, DeploymentSpec deploymentSpec) { + Optional<EndpointCertificateMetadata> currentCertificateMetadata = curator.readEndpointCertificateMetadata(instance.id()); DeploymentId deployment = new DeploymentId(instance.id(), zone); if (currentCertificateMetadata.isEmpty()) { - var provisionedCertificateMetadata = provisionEndpointCertificate(deployment, Optional.empty(), instanceSpec); + var provisionedCertificateMetadata = provisionEndpointCertificate(deployment, Optional.empty(), deploymentSpec); // We do not verify the certificate if one has never existed before - because we do not want to // wait for it to be available before we deploy. This allows the config server to start // provisioning nodes ASAP, and the risk is small for a new deployment. @@ -77,10 +77,10 @@ public class EndpointCertificates { } // Re-provision certificate if it is missing SANs for the zone we are deploying to - var requiredSansForZone = controller.routing().certificateDnsNames(deployment); + var requiredSansForZone = controller.routing().certificateDnsNames(deployment, deploymentSpec); if (!currentCertificateMetadata.get().requestedDnsSans().containsAll(requiredSansForZone)) { var reprovisionedCertificateMetadata = - provisionEndpointCertificate(deployment, currentCertificateMetadata, instanceSpec) + provisionEndpointCertificate(deployment, currentCertificateMetadata, deploymentSpec) .withRequestId(currentCertificateMetadata.get().requestId()); // We're required to keep the original request ID curator.writeEndpointCertificateMetadata(instance.id(), reprovisionedCertificateMetadata); // Verification is unlikely to succeed in this case, as certificate must be available first - controller will retry @@ -94,12 +94,13 @@ public class EndpointCertificates { private EndpointCertificateMetadata provisionEndpointCertificate(DeploymentId deployment, Optional<EndpointCertificateMetadata> currentMetadata, - Optional<DeploymentInstanceSpec> instanceSpec) { + DeploymentSpec deploymentSpec) { List<ZoneId> zonesInSystem = controller.zoneRegistry().zones().controllerUpgraded().ids(); Set<ZoneId> requiredZones = new LinkedHashSet<>(); requiredZones.add(deployment.zoneId()); if (!deployment.zoneId().environment().isManuallyDeployed()) { // If not deploying to a dev or perf zone, require all prod zones in deployment spec + test and staging + Optional<DeploymentInstanceSpec> instanceSpec = deploymentSpec.instance(deployment.applicationId().instance()); zonesInSystem.stream() .filter(zone -> zone.environment().isTest() || (instanceSpec.isPresent() && @@ -107,14 +108,16 @@ public class EndpointCertificates { .forEach(requiredZones::add); } Set<String> requiredNames = requiredZones.stream() - .flatMap(zone -> controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone)).stream()) + .flatMap(zone -> controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone), + deploymentSpec) + .stream()) .collect(Collectors.toCollection(LinkedHashSet::new)); // Preserve any currently present names that are still valid List<String> currentNames = currentMetadata.map(EndpointCertificateMetadata::requestedDnsSans) .orElseGet(List::of); zonesInSystem.stream() - .map(zone -> controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone))) + .map(zone -> controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone), deploymentSpec)) .filter(currentNames::containsAll) .forEach(requiredNames::addAll); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java index c1f9137ecfa..fa4a871d423 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java @@ -27,122 +27,128 @@ public class EndpointTest { @Test public void global_endpoints() { + DeploymentId deployment1 = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1")); + DeploymentId deployment2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1")); + ClusterSpec.Id cluster = ClusterSpec.Id.from("default"); EndpointId endpointId = EndpointId.defaultId(); Map<String, Endpoint> tests = Map.of( // Legacy endpoint "http://a1.t1.global.vespa.yahooapis.com:4080/", - Endpoint.of(instance1).target(endpointId).on(Port.plain(4080)).legacy().in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.plain(4080)).legacy().in(SystemName.main), // Legacy endpoint with TLS "https://a1--t1.global.vespa.yahooapis.com:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).legacy().in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).legacy().in(SystemName.main), // Main endpoint "https://a1--t1.global.vespa.oath.cloud:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).in(SystemName.main), // Main endpoint in CD "https://cd--a1--t1.global.vespa.oath.cloud:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).in(SystemName.cd), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).in(SystemName.cd), // Main endpoint in CD "https://cd--i2--a2--t2.global.vespa.oath.cloud:4443/", - Endpoint.of(instance2).target(endpointId).on(Port.tls(4443)).in(SystemName.cd), + Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls(4443)).in(SystemName.cd), // Main endpoint with direct routing and default TLS port "https://a1.t1.global.vespa.oath.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint with custom rotation name "https://r1.a1.t1.global.vespa.oath.cloud/", - Endpoint.of(instance1).target(EndpointId.of("r1")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance1).target(EndpointId.of("r1"), cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint for custom instance in default rotation "https://i2.a2.t2.global.vespa.oath.cloud/", - Endpoint.of(instance2).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint for custom instance with custom rotation name "https://r2.i2.a2.t2.global.vespa.oath.cloud/", - Endpoint.of(instance2).target(EndpointId.of("r2")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance2).target(EndpointId.of("r2"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint in public system "https://a1.t1.g.vespa-app.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) ); tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString())); Map<String, Endpoint> tests2 = Map.of( // Default endpoint in public system "https://a1.t1.g.vespa-app.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public), // Default endpoint in public CD system "https://a1.t1.g.cd.vespa-app.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd), // Custom instance in public system "https://i2.a2.t2.g.vespa-app.cloud/", - Endpoint.of(instance2).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) + Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) ); tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString())); } @Test public void global_endpoints_with_endpoint_id() { - var endpointId = EndpointId.defaultId(); + DeploymentId deployment1 = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1")); + DeploymentId deployment2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1")); + ClusterSpec.Id cluster = ClusterSpec.Id.from("default"); + EndpointId endpointId = EndpointId.defaultId(); Map<String, Endpoint> tests = Map.of( // Legacy endpoint "http://a1.t1.global.vespa.yahooapis.com:4080/", - Endpoint.of(instance1).target(endpointId).on(Port.plain(4080)).legacy().in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.plain(4080)).legacy().in(SystemName.main), // Legacy endpoint with TLS "https://a1--t1.global.vespa.yahooapis.com:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).legacy().in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).legacy().in(SystemName.main), // Main endpoint "https://a1--t1.global.vespa.oath.cloud:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).in(SystemName.main), // Main endpoint in CD "https://cd--i2--a2--t2.global.vespa.oath.cloud:4443/", - Endpoint.of(instance2).target(endpointId).on(Port.tls(4443)).in(SystemName.cd), + Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls(4443)).in(SystemName.cd), // Main endpoint in CD "https://cd--a1--t1.global.vespa.oath.cloud:4443/", - Endpoint.of(instance1).target(endpointId).on(Port.tls(4443)).in(SystemName.cd), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls(4443)).in(SystemName.cd), // Main endpoint with direct routing and default TLS port "https://a1.t1.global.vespa.oath.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint with custom rotation name "https://r1.a1.t1.global.vespa.oath.cloud/", - Endpoint.of(instance1).target(EndpointId.of("r1")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance1).target(EndpointId.of("r1"), cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint for custom instance in default rotation "https://i2.a2.t2.global.vespa.oath.cloud/", - Endpoint.of(instance2).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint for custom instance with custom rotation name "https://r2.i2.a2.t2.global.vespa.oath.cloud/", - Endpoint.of(instance2).target(EndpointId.of("r2")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), + Endpoint.of(instance2).target(EndpointId.of("r2"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main), // Main endpoint in public system "https://a1.t1.g.vespa-app.cloud/", - Endpoint.of(instance1).target(endpointId).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) + Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) ); tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString())); Map<String, Endpoint> tests2 = Map.of( // Custom endpoint and instance in public CD system) "https://foo.i2.a2.t2.g.cd.vespa-app.cloud/", - Endpoint.of(instance2).target(EndpointId.of("foo")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd), + Endpoint.of(instance2).target(EndpointId.of("foo"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd), // Custom endpoint and instance in public system "https://foo.i2.a2.t2.g.vespa-app.cloud/", - Endpoint.of(instance2).target(EndpointId.of("foo")).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) + Endpoint.of(instance2).target(EndpointId.of("foo"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public) ); tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString())); } @@ -228,6 +234,7 @@ public class EndpointTest { "https://a1.t1.g.vespa-app.cloud/", Endpoint.of(instance1) .target(EndpointId.defaultId()) + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -236,6 +243,7 @@ public class EndpointTest { "https://*.a1.t1.g.vespa-app.cloud/", Endpoint.of(instance1) .wildcard() + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -244,6 +252,7 @@ public class EndpointTest { "https://a1.t1.us-north-1.z.vespa-app.cloud/", Endpoint.of(instance1) .target(defaultCluster, prodZone) + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -252,6 +261,7 @@ public class EndpointTest { "https://a1.t1.us-north-2.test.z.vespa-app.cloud/", Endpoint.of(instance1) .target(defaultCluster, testZone) + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -260,6 +270,7 @@ public class EndpointTest { "https://*.a1.t1.us-north-2.test.z.vespa-app.cloud/", Endpoint.of(instance1) .wildcard(testZone) + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), @@ -268,6 +279,7 @@ public class EndpointTest { "https://*.a1.t1.us-north-1.z.vespa-app.cloud/", Endpoint.of(instance1) .wildcard(prodZone) + .certificateName() .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public) @@ -353,7 +365,7 @@ public class EndpointTest { var tests1 = Map.of( // With default cluster "a1.t1.us-north-1.prod", - Endpoint.of(instance1).target(EndpointId.defaultId()).on(Port.tls(4443)).in(SystemName.main), + Endpoint.of(instance1).target(EndpointId.defaultId(), ClusterSpec.Id.from("default"), List.of(zone)).on(Port.tls(4443)).in(SystemName.main), // With non-default cluster "c1.a1.t1.us-north-1.prod", diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java index c6a9c12027f..0a93c936b41 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java @@ -5,6 +5,8 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; @@ -19,6 +21,8 @@ import com.yahoo.vespa.hosted.controller.Instance; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMock; import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorImpl; +import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import org.junit.Before; @@ -31,6 +35,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -111,7 +116,7 @@ public class EndpointCertificatesTest { @Test public void provisions_new_certificate_in_dev() { ZoneId testZone = tester.zoneRegistry().zones().routingMethod(RoutingMethod.exclusive).in(Environment.dev).zones().stream().findFirst().orElseThrow().getId(); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key")); assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert")); @@ -121,7 +126,7 @@ public class EndpointCertificatesTest { @Test public void provisions_new_certificate_in_prod() { - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key")); assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert")); @@ -145,7 +150,7 @@ public class EndpointCertificatesTest { "default.default.aws-us-east-1c.staging.z.vespa-app.cloud", "*.default.default.aws-us-east-1c.staging.z.vespa-app.cloud" ); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key")); assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert")); @@ -164,7 +169,7 @@ public class EndpointCertificatesTest { "", Optional.empty(), Optional.empty())); secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 7); secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 7); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertEquals(testKeyName, endpointCertificateMetadata.get().keyName()); assertEquals(testCertName, endpointCertificateMetadata.get().certName()); @@ -176,7 +181,7 @@ public class EndpointCertificatesTest { mockCuratorDb.writeEndpointCertificateMetadata(testInstance.id(), new EndpointCertificateMetadata(testKeyName, testCertName, -1, 0, "uuid", List.of(), "issuer", Optional.empty(), Optional.empty())); secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0); secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 0); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertEquals(0, endpointCertificateMetadata.get().version()); assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id())); @@ -193,7 +198,7 @@ public class EndpointCertificatesTest { secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0); secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate2) + X509CertificateUtils.toPem(testCertificate2), 0); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.empty()); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, DeploymentSpec.empty); assertTrue(endpointCertificateMetadata.isPresent()); assertEquals(0, endpointCertificateMetadata.get().version()); assertEquals(endpointCertificateMetadata, mockCuratorDb.readEndpointCertificateMetadata(testInstance.id())); @@ -203,7 +208,6 @@ public class EndpointCertificatesTest { @Test public void includes_zones_in_deployment_spec_when_deploying_to_staging() { - DeploymentSpec deploymentSpec = new DeploymentSpecXmlReader(true).read( "<deployment version=\"1.0\">\n" + " <instance id=\"default\">\n" + @@ -215,7 +219,7 @@ public class EndpointCertificatesTest { "</deployment>\n"); ZoneId testZone = tester.zoneRegistry().zones().all().in(Environment.staging).zones().stream().findFirst().orElseThrow().getId(); - Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, Optional.of(deploymentSpec.requireInstance("default"))); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(testInstance, testZone, deploymentSpec); assertTrue(endpointCertificateMetadata.isPresent()); assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.default.default.*-key")); assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.default.default.*-cert")); @@ -223,4 +227,49 @@ public class EndpointCertificatesTest { assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(endpointCertificateMetadata.get().requestedDnsSans())); } + @Test + public void includes_application_endpoint_when_declared() { + Instance instance = new Instance(ApplicationId.from("t1", "a1", "default")); + ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c")); + ZoneId zone2 = ZoneId.from(Environment.prod, RegionName.from("aws-us-west-2a")); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("beta,main") + .region(zone1.region()) + .region(zone2.region()) + .applicationEndpoint("a", "qrs", zone2.region().value(), + Map.of(InstanceName.from("beta"), 2, + InstanceName.from("main"), 8)) + .applicationEndpoint("b", "qrs", zone2.region().value(), + Map.of(InstanceName.from("beta"), 1, + InstanceName.from("main"), 1)) + .applicationEndpoint("c", "qrs", zone1.region().value(), + Map.of(InstanceName.from("beta"), 4, + InstanceName.from("main"), 6)) + .build(); + ControllerTester tester = new ControllerTester(SystemName.Public); + EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock); + EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator); + List<String> expectedSans = List.of( + "vlfms2wpoa4nyrka2s5lktucypjtxkqhv.internal.vespa-app.cloud", + "a1.t1.g.vespa-app.cloud", + "*.a1.t1.g.vespa-app.cloud", + "a1.t1.aws-us-west-2a.r.vespa-app.cloud", + "*.a1.t1.aws-us-west-2a.r.vespa-app.cloud", + "a1.t1.aws-us-east-1c.r.vespa-app.cloud", + "*.a1.t1.aws-us-east-1c.r.vespa-app.cloud", + "a1.t1.aws-us-east-1c.z.vespa-app.cloud", + "*.a1.t1.aws-us-east-1c.z.vespa-app.cloud", + "a1.t1.aws-us-east-1c.test.z.vespa-app.cloud", + "*.a1.t1.aws-us-east-1c.test.z.vespa-app.cloud", + "a1.t1.aws-us-east-1c.staging.z.vespa-app.cloud", + "*.a1.t1.aws-us-east-1c.staging.z.vespa-app.cloud" + ); + Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone1, applicationPackage.deploymentSpec()); + assertTrue(endpointCertificateMetadata.isPresent()); + assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.t1.a1.*-key")); + assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.t1.a1.*-cert")); + assertEquals(0, endpointCertificateMetadata.get().version()); + assertEquals(expectedSans, endpointCertificateMetadata.get().requestedDnsSans()); + } + } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java index 4ad70c4e9dd..61eca05cc67 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java @@ -2,9 +2,11 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.SlimeUtils; +import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; @@ -31,7 +33,9 @@ public class TestConfigSerializerTest { JobType.systemTest, true, Map.of(zone, List.of(Endpoint.of(ApplicationId.defaultId()) - .target(EndpointId.of("ai")) + .target(EndpointId.of("ai"), ClusterSpec.Id.from("qrs"), + List.of(new DeploymentId(ApplicationId.defaultId(), + ZoneId.defaultId()))) .on(Endpoint.Port.tls()) .in(SystemName.main))), Map.of(zone, List.of("facts"))); |