diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2022-10-10 15:19:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-10 15:19:10 +0200 |
commit | bfc6c2d96a5a30f5134b35a703a62f2fd8767cd3 (patch) | |
tree | 8395471db264645243a30d70bf3a6baee82a1475 | |
parent | 8be08877e45a714263e7e1a48d56a41ebc5bfb9b (diff) | |
parent | 63e2e795b44a8487b9bc48b92cc30f3384c4bb1b (diff) |
Merge pull request #24374 from vespa-engine/freva/gcp-routing
Global endpoints in GCP
18 files changed, 385 insertions, 90 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java index 1659a87acb3..7ccbcf2a954 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java @@ -59,8 +59,8 @@ public sealed abstract class AliasTarget permits LatencyAliasTarget, WeightedAli public static AliasTarget unpack(RecordData data) { String[] parts = data.asString().split("/"); switch (parts[0]) { - case "latency": return LatencyAliasTarget.unpack(data); - case "weighted": return WeightedAliasTarget.unpack(data); + case LatencyAliasTarget.TARGET_TYPE: return LatencyAliasTarget.unpack(data); + case WeightedAliasTarget.TARGET_TYPE: return WeightedAliasTarget.unpack(data); } throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'"); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java new file mode 100644 index 00000000000..c3cedf93841 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java @@ -0,0 +1,57 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import java.util.Objects; + +/** + * Same as {@link AliasTarget}, except for targets outside AWS (cannot be targeted with ALIAS record). + * + * @author freva + */ +public sealed abstract class DirectTarget permits LatencyDirectTarget, WeightedDirectTarget { + + private final RecordData recordData; + private final String id; + + protected DirectTarget(RecordData recordData, String id) { + this.recordData = Objects.requireNonNull(recordData, "recordData must be non-null"); + this.id = Objects.requireNonNull(id, "id must be non-null"); + } + + /** A unique identifier of this record within the record group */ + public String id() { + return id; + } + + /** Data in this, e.g. IP address for records of type A */ + public RecordData recordData() { + return recordData; + } + + /** Returns the fields in this encoded as record data */ + public abstract RecordData pack(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DirectTarget that = (DirectTarget) o; + return recordData.equals(that.recordData) && id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(recordData, id); + } + + /** Unpack target from given record data */ + public static DirectTarget unpack(RecordData data) { + String[] parts = data.asString().split("/"); + return switch (parts[0]) { + case LatencyDirectTarget.TARGET_TYPE -> LatencyDirectTarget.unpack(data); + case WeightedDirectTarget.TARGET_TYPE -> WeightedDirectTarget.unpack(data); + default -> throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'"); + }; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java index 70c89b05f09..00e5218dead 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java @@ -13,6 +13,8 @@ import java.util.Objects; */ public final class LatencyAliasTarget extends AliasTarget { + static final String TARGET_TYPE = "latency"; + private final ZoneId zone; public LatencyAliasTarget(DomainName name, String dnsZone, ZoneId zone) { @@ -27,7 +29,7 @@ public final class LatencyAliasTarget extends AliasTarget { @Override public RecordData pack() { - return RecordData.from("latency/" + name().value() + "/" + dnsZone() + "/" + id()); + return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id())); } @Override @@ -56,7 +58,7 @@ public final class LatencyAliasTarget extends AliasTarget { throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id, but got " + data.asString()); } - if (!"latency".equals(parts[0])) { + if (!TARGET_TYPE.equals(parts[0])) { throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); } return new LatencyAliasTarget(DomainName.of(parts[1]), parts[2], ZoneId.from(parts[3])); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java new file mode 100644 index 00000000000..09795ae08a7 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java @@ -0,0 +1,66 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; + +import java.util.Objects; + +/** + * An implementation of {@link DirectTarget} that uses latency-based routing. + * + * @author freva + */ +public final class LatencyDirectTarget extends DirectTarget { + + static final String TARGET_TYPE = "latency"; + + private final ZoneId zone; + + public LatencyDirectTarget(RecordData recordData, ZoneId zone) { + super(recordData, zone.value()); + this.zone = Objects.requireNonNull(zone); + } + + /** The zone this record points to */ + public ZoneId zone() { + return zone; + } + + @Override + public RecordData pack() { + return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + LatencyDirectTarget that = (LatencyDirectTarget) o; + return zone.equals(that.zone); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), zone); + } + + @Override + public String toString() { + return "latency target for " + recordData() + " [id=" + id() + "]"; + } + + /** Unpack latency alias from given record data */ + public static LatencyDirectTarget unpack(RecordData data) { + var parts = data.asString().split("/"); + if (parts.length != 3) { + throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id, but got " + + data.asString()); + } + if (!TARGET_TYPE.equals(parts[0])) { + throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); + } + return new LatencyDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2])); + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java index 18d7bc53035..9a9270bdf7f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java @@ -54,6 +54,20 @@ public class MemoryNameService implements NameService { } @Override + public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) { + var records = targets.stream() + .sorted((a, b) -> Comparator.comparing((DirectTarget target) -> target.recordData().asString()).compare(a, b)) + .map(d -> new Record(Record.Type.DIRECT, name, d.pack())) + .collect(Collectors.toList()); + // Satisfy idempotency contract of interface + for (var r1 : records) { + this.records.removeIf(r2 -> conflicts(r1, r2)); + } + this.records.addAll(records); + return records; + } + + @Override public List<Record> createTxtRecords(RecordName name, List<RecordData> txtData) { var records = txtData.stream() .map(data -> new Record(Record.Type.TXT, name, data)) @@ -122,6 +136,11 @@ public class MemoryNameService implements NameService { AliasTarget t2 = AliasTarget.unpack(r2.data()); return t1.name().equals(t2.name()); // ALIAS records require distinct targets } + if (r1.type() == Record.Type.DIRECT && r1.type() == r2.type()) { + DirectTarget t1 = DirectTarget.unpack(r1.data()); + DirectTarget t2 = DirectTarget.unpack(r2.data()); + return t1.id().equals(t2.id()); // DIRECT records require distinct IDs + } return true; // Anything else is considered a conflict } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java index 505ff3850ab..72e983680d9 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java @@ -31,6 +31,15 @@ public interface NameService { List<Record> createAlias(RecordName name, Set<AliasTarget> targets); /** + * Create a non-standard record pointing to given targets. Implementations of this are expected to be + * idempotent + * + * @param targets Targets that should be resolved by this name. + * @return The created records. One per target. + */ + List<Record> createDirect(RecordName name, Set<DirectTarget> targets); + + /** * Create a new TXT record containing the provided data. * @param name Name of the created record * @param txtRecords TXT data values for the record, each consisting of one or more space-separated double-quoted diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java index 2f9312b2f89..e76445faa60 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java @@ -55,6 +55,7 @@ public record Record(Type type, AAAA, ALIAS, CNAME, + DIRECT, MX, NS, PTR, diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java index 6a61b62f3a4..ca01c713e93 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java @@ -16,6 +16,8 @@ import java.util.Objects; */ public final class WeightedAliasTarget extends AliasTarget { + static final String TARGET_TYPE = "weighted"; + private final long weight; public WeightedAliasTarget(DomainName name, String dnsZone, ZoneId zone, long weight) { @@ -31,7 +33,7 @@ public final class WeightedAliasTarget extends AliasTarget { @Override public RecordData pack() { - return RecordData.from("weighted/" + name().value() + "/" + dnsZone() + "/" + id() + "/" + weight); + return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id(), Long.toString(weight))); } @Override @@ -60,7 +62,7 @@ public final class WeightedAliasTarget extends AliasTarget { throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id/weight, " + "but got " + data.asString()); } - if (!"weighted".equals(parts[0])) { + if (!TARGET_TYPE.equals(parts[0])) { throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); } return new WeightedAliasTarget(DomainName.of(parts[1]), parts[2], ZoneId.from(parts[3]), diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java new file mode 100644 index 00000000000..b899cb57b60 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java @@ -0,0 +1,70 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; + +import java.util.Objects; + +/** + * An implementation of {@link DirectTarget} where is requests are answered based on the weight assigned to the + * record, as a proportion of the total weight for all records having the same DNS name. + * + * The portion of received traffic is calculated as follows: (record weight / sum of the weights of all records). + * + * @author freva + */ +public final class WeightedDirectTarget extends DirectTarget { + + static final String TARGET_TYPE = "weighted"; + + private final long weight; + + public WeightedDirectTarget(RecordData recordData, ZoneId zone, long weight) { + super(recordData, zone.value()); + this.weight = weight; + if (weight < 0) throw new IllegalArgumentException("Weight cannot be negative"); + } + + /** The weight of this target */ + public long weight() { + return weight; + } + + @Override + public RecordData pack() { + return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id(), Long.toString(weight))); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + WeightedDirectTarget that = (WeightedDirectTarget) o; + return weight == that.weight; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), weight); + } + + @Override + public String toString() { + return "weighted target for " + recordData() + "[id=" + id() + ",weight=" + weight + "]"; + } + + /** Unpack weighted alias from given record data */ + public static WeightedDirectTarget unpack(RecordData data) { + var parts = data.asString().split("/"); + if (parts.length != 4) { + throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id/weight, " + + "but got " + data.asString()); + } + if (!TARGET_TYPE.equals(parts[0])) { + throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); + } + return new WeightedDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2]), Long.parseLong(parts[3])); + } + +} diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java new file mode 100644 index 00000000000..f262821a638 --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java @@ -0,0 +1,36 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author freva + */ +class DirectTargetTest { + + @Test + void packing() { + List<DirectTarget> tests = List.of( + new LatencyDirectTarget(RecordData.from("foo.example.com"), ZoneId.from("prod.us-north-1")), + new WeightedDirectTarget(RecordData.from("bar.example.com"), ZoneId.from("prod.us-north-2"), 50)); + for (var target : tests) { + DirectTarget unpacked = DirectTarget.unpack(target.pack()); + assertEquals(target, unpacked); + } + + List<RecordData> invalidData = List.of(RecordData.from(""), RecordData.from("foobar")); + for (var data : invalidData) { + try { + DirectTarget.unpack(data); + fail("Expected exception"); + } catch (IllegalArgumentException ignored) { } + } + } + +}
\ No newline at end of file diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java index 8e8a4e24970..f72b6f2e9f0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java @@ -10,7 +10,6 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; -import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; @@ -26,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** @@ -97,15 +97,10 @@ public class ApplicationPackageValidator { var clouds = new HashSet<CloudName>(); for (var region : endpoint.regions()) { for (ZoneApi zone : controller.zoneRegistry().zones().all().in(Environment.prod).in(region).zones()) { - if (zone.getCloudName().equals(CloudName.GCP)) { - throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' in " + instance + - " contains a Google Cloud region (" + region + - "), which is not yet supported"); - } clouds.add(zone.getCloudName()); } } - if (clouds.size() != 1) { + if (clouds.size() != 1 && !clouds.equals(Set.of(CloudName.GCP, CloudName.AWS))) { throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' in " + instance + " cannot contain regions in different clouds: " + endpoint.regions().stream().sorted().toList()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java index d0c901ccb36..b97fdde560e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; @@ -28,8 +29,8 @@ public class CreateRecords implements NameServiceRequest { this.name = requireOneOf(Record::name, records); this.type = requireOneOf(Record::type, records); this.records = List.copyOf(Objects.requireNonNull(records, "records must be non-null")); - if (type != Record.Type.ALIAS && type != Record.Type.TXT) { - throw new IllegalArgumentException("Records of type " + type + "are not supported: " + records); + if (type != Record.Type.ALIAS && type != Record.Type.TXT && type != Record.Type.DIRECT) { + throw new IllegalArgumentException("Records of type " + type + " are not supported: " + records); } } @@ -40,14 +41,18 @@ public class CreateRecords implements NameServiceRequest { @Override public void dispatchTo(NameService nameService) { switch (type) { - case ALIAS: + case ALIAS -> { var targets = records.stream().map(Record::data).map(AliasTarget::unpack).collect(Collectors.toSet()); nameService.createAlias(name, targets); - break; - case TXT: + } + case DIRECT -> { + var targets = records.stream().map(Record::data).map(DirectTarget::unpack).collect(Collectors.toSet()); + nameService.createDirect(name, targets); + } + case TXT -> { var dataFields = records.stream().map(Record::data).collect(Collectors.toList()); nameService.createTxtRecords(name, dataFields); - break; + } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java index 9d2c7918252..57c83280b8b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; @@ -55,6 +56,14 @@ public class NameServiceForwarder { forward(new CreateRecords(records), priority); } + /** Create or update a DIRECT record with given name and targets */ + public void createDirect(RecordName name, Set<DirectTarget> targets, NameServiceQueue.Priority priority) { + var records = targets.stream() + .map(target -> new Record(Record.Type.DIRECT, name, target.pack())) + .collect(Collectors.toList()); + forward(new CreateRecords(records), priority); + } + /** Create or update a TXT record with given name and data */ public void createTxt(RecordName name, List<RecordData> txtData, NameServiceQueue.Priority priority) { var records = txtData.stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java index f940d53fab3..6fa7473ad1e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; @@ -69,11 +70,11 @@ public class RemoveRecords implements NameServiceRequest { .stream() .filter(record -> { // Records to remove must match both name and data fields - String dataValue = record.data().asString(); - // If we're comparing an ALIAS record we have to unpack it to access the target name - if (record.type() == Record.Type.ALIAS) { - dataValue = AliasTarget.unpack(record.data()).name().value(); - } + String dataValue = switch (record.type()) { + case ALIAS -> AliasTarget.unpack(record.data()).name().value(); + case DIRECT -> DirectTarget.unpack(record.data()).recordData().asString(); + default -> record.data().asString(); + }; return fqdn(dataValue).equals(fqdn(data.get().asString())); }) .forEach(records::add); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java index f76d04c9e1d..fc0badae9ea 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java @@ -12,11 +12,13 @@ import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; @@ -34,6 +36,7 @@ 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.stream.Collectors; @@ -169,9 +172,16 @@ public class RoutingPolicies { // Create a weighted ALIAS per region, pointing to all zones within the same region Collection<RegionEndpoint> regionEndpoints = computeRegionEndpoints(policies, inactiveZones); regionEndpoints.forEach(regionEndpoint -> { - controller.nameServiceForwarder().createAlias(RecordName.from(regionEndpoint.target().name().value()), - Collections.unmodifiableSet(regionEndpoint.zoneTargets()), - Priority.normal); + if ( ! regionEndpoint.zoneAliasTargets().isEmpty()) { + controller.nameServiceForwarder().createAlias(RecordName.from(regionEndpoint.target().name().value()), + regionEndpoint.zoneAliasTargets(), + Priority.normal); + } + if ( ! regionEndpoint.zoneDirectTargets().isEmpty()) { + controller.nameServiceForwarder().createDirect(RecordName.from(regionEndpoint.target().name().value()), + regionEndpoint.zoneDirectTargets(), + Priority.normal); + } }); // Create global latency-based ALIAS pointing to each per-region weighted ALIAS @@ -203,7 +213,7 @@ public class RoutingPolicies { private Collection<RegionEndpoint> computeRegionEndpoints(List<RoutingPolicy> policies, Set<ZoneId> inactiveZones) { Map<Endpoint, RegionEndpoint> endpoints = new LinkedHashMap<>(); for (var policy : policies) { - if (policy.dnsZone().isEmpty()) continue; + if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue; if (controller.zoneRegistry().routingMethod(policy.id().zone()) != RoutingMethod.exclusive) continue; Endpoint endpoint = policy.regionEndpointIn(controller.system(), RoutingMethod.exclusive); var zonePolicy = db.readZoneRoutingPolicy(policy.id().zone()); @@ -219,9 +229,11 @@ public class RoutingPolicies { if (policy.canonicalName().isPresent()) { var weightedTarget = new WeightedAliasTarget( policy.canonicalName().get(), policy.dnsZone().get(), policy.id().zone(), weight); - regionEndpoint.zoneTargets().add(weightedTarget); + regionEndpoint.add(weightedTarget); } else { - // TODO (freva): Add direct weighted record + var weightedTarget = new WeightedDirectTarget( + RecordData.from(policy.ipAddress().get()), policy.id().zone(), weight); + regionEndpoint.add(weightedTarget); } } return endpoints.values(); @@ -237,8 +249,8 @@ public class RoutingPolicies { if (routingTable.isEmpty()) return; Application application = controller.applications().requireApplication(routingTable.keySet().iterator().next().application()); - Map<Endpoint, Set<AliasTarget>> targetsByEndpoint = new LinkedHashMap<>(); - Map<Endpoint, Set<AliasTarget>> inactiveTargetsByEndpoint = new LinkedHashMap<>(); + Map<Endpoint, Set<Target>> targetsByEndpoint = new LinkedHashMap<>(); + Map<Endpoint, Set<Target>> inactiveTargetsByEndpoint = new LinkedHashMap<>(); for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) { RoutingId routingId = routeEntry.getKey(); EndpointList endpoints = controller.routing().declaredEndpointsOf(application) @@ -253,17 +265,15 @@ public class RoutingPolicies { for (var policy : routeEntry.getValue()) { for (var target : endpoint.targets()) { if (!policy.appliesTo(target.deployment())) continue; - if (policy.dnsZone().isEmpty()) continue; // Does not support ALIAS records - if (policy.canonicalName().isEmpty()) continue; // TODO (freva): Handle DIRECT records + if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue; // Does not support ALIAS records ZoneRoutingPolicy zonePolicy = db.readZoneRoutingPolicy(policy.id().zone()); - WeightedAliasTarget weightedAliasTarget = new WeightedAliasTarget(policy.canonicalName().get(), policy.dnsZone().get(), - target.deployment().zoneId(), target.weight()); - Set<AliasTarget> activeTargets = targetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); - Set<AliasTarget> inactiveTargets = inactiveTargetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); + + Set<Target> activeTargets = targetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); + Set<Target> inactiveTargets = inactiveTargetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); if (isConfiguredOut(zonePolicy, policy, inactiveZones)) { - inactiveTargets.add(weightedAliasTarget); + inactiveTargets.add(Target.weighted(policy, target)); } else { - activeTargets.add(weightedAliasTarget); + activeTargets.add(Target.weighted(policy, target)); } } } @@ -273,11 +283,11 @@ public class RoutingPolicies { // the ALIAS records would cause the application endpoint to stop resolving entirely (NXDOMAIN). for (var kv : targetsByEndpoint.entrySet()) { Endpoint endpoint = kv.getKey(); - Set<AliasTarget> activeTargets = kv.getValue(); + Set<Target> activeTargets = kv.getValue(); if (!activeTargets.isEmpty()) { continue; } - Set<AliasTarget> inactiveTargets = inactiveTargetsByEndpoint.get(endpoint); + Set<Target> inactiveTargets = inactiveTargetsByEndpoint.get(endpoint); activeTargets.addAll(inactiveTargets); inactiveTargets.clear(); } @@ -287,9 +297,21 @@ public class RoutingPolicies { .map(DeploymentId::zoneId) .findFirst() .get(); - nameServiceForwarderIn(targetZone).createAlias(RecordName.from(applicationEndpoint.dnsName()), - targets, - Priority.normal); + Set<AliasTarget> aliasTargets = new LinkedHashSet<>(); + Set<DirectTarget> directTargets = new LinkedHashSet<>(); + for (Target target : targets) { + if (target.aliasOrDirectTarget() instanceof AliasTarget at) aliasTargets.add(at); + else directTargets.add((DirectTarget) target.aliasOrDirectTarget()); + } + + if ( ! aliasTargets.isEmpty()) { + nameServiceForwarderIn(targetZone).createAlias( + RecordName.from(applicationEndpoint.dnsName()), aliasTargets, Priority.normal); + } + if ( ! directTargets.isEmpty()) { + nameServiceForwarderIn(targetZone).createDirect( + RecordName.from(applicationEndpoint.dnsName()), directTargets, Priority.normal); + } }); inactiveTargetsByEndpoint.forEach((applicationEndpoint, targets) -> { ZoneId targetZone = applicationEndpoint.targets().stream() @@ -298,9 +320,9 @@ public class RoutingPolicies { .findFirst() .get(); targets.forEach(target -> { - nameServiceForwarderIn(targetZone).removeRecords(Record.Type.ALIAS, + nameServiceForwarderIn(targetZone).removeRecords(target.type(), RecordName.from(applicationEndpoint.dnsName()), - RecordData.fqdn(target.name().value()), + target.data(), Priority.normal); }); }); @@ -317,7 +339,8 @@ public class RoutingPolicies { if (loadBalancer.hostname().isEmpty() && loadBalancer.ipAddress().isEmpty()) continue; var policyId = new RoutingPolicyId(loadBalancer.application(), loadBalancer.cluster(), allocation.deployment.zoneId()); var existingPolicy = policies.get(policyId); - var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname(), loadBalancer.ipAddress(), loadBalancer.dnsZone(), + var dnsZone = loadBalancer.ipAddress().isPresent() ? Optional.of("ignored") : loadBalancer.dnsZone(); + var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname(), loadBalancer.ipAddress(), dnsZone, allocation.instanceEndpointsOf(loadBalancer), allocation.applicationEndpointsOf(loadBalancer), new RoutingPolicy.Status(isActive(loadBalancer), RoutingStatus.DEFAULT)); @@ -407,7 +430,10 @@ public class RoutingPolicies { RecordData.fqdn(policy.canonicalName().get().value()), Priority.normal); } else { - // TODO (freva): Remove DIRECT records + forwarder.removeRecords(Record.Type.DIRECT, + RecordName.from(endpoint.dnsName()), + RecordData.from(policy.ipAddress().get()), + Priority.normal); } } } @@ -460,22 +486,23 @@ public class RoutingPolicies { private static class RegionEndpoint { private final LatencyAliasTarget target; - private final Set<WeightedAliasTarget> zoneTargets = new LinkedHashSet<>(); + private final Set<WeightedAliasTarget> zoneAliasTargets = new LinkedHashSet<>(); + private final Set<WeightedDirectTarget> zoneDirectTargets = new LinkedHashSet<>(); public RegionEndpoint(LatencyAliasTarget target) { this.target = Objects.requireNonNull(target); } - public LatencyAliasTarget target() { - return target; - } + public LatencyAliasTarget target() { return target; } + public Set<AliasTarget> zoneAliasTargets() { return Collections.unmodifiableSet(zoneAliasTargets); } + public Set<DirectTarget> zoneDirectTargets() { return Collections.unmodifiableSet(zoneDirectTargets); } - public Set<WeightedAliasTarget> zoneTargets() { - return zoneTargets; - } + public void add(WeightedAliasTarget target) { zoneAliasTargets.add(target); } + public void add(WeightedDirectTarget target) { zoneDirectTargets.add(target); } public boolean active() { - return zoneTargets.stream().anyMatch(target -> target.weight() > 0); + return zoneAliasTargets.stream().anyMatch(target -> target.weight() > 0) || + zoneDirectTargets.stream().anyMatch(target -> target.weight() > 0); } @Override @@ -573,6 +600,20 @@ public class RoutingPolicies { }; } + /** Denotes record data (record rhs) of either an ALIAS or a DIRECT target */ + private record Target(Record.Type type, RecordData data, Object aliasOrDirectTarget) { + static Target weighted(RoutingPolicy policy, Endpoint.Target endpointTarget) { + if (policy.ipAddress().isPresent()) { + var wt = new WeightedDirectTarget(RecordData.from(policy.ipAddress().get()), + endpointTarget.deployment().zoneId(), endpointTarget.weight()); + return new Target(Record.Type.DIRECT, wt.recordData(), wt); + } + var wt = new WeightedAliasTarget(policy.canonicalName().get(), policy.dnsZone().get(), + endpointTarget.deployment().zoneId(), endpointTarget.weight()); + return new Target(Record.Type.ALIAS, RecordData.fqdn(wt.name().value()), wt); + } + } + /** A {@link NameServiceForwarder} that does nothing. Used in zones where no explicit DNS updates are needed */ private static class NameServiceDiscarder extends NameServiceForwarder { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index c4e138c4d18..cd3d6ca7531 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -912,38 +912,6 @@ public class ControllerTest { } @Test - void testDeployWithGlobalEndpointsInGcp() { - tester.controllerTester().zoneRegistry().setZones( - ZoneApiMock.fromId("test.us-west-1"), - ZoneApiMock.fromId("staging.us-west-1"), - ZoneApiMock.newBuilder().with(CloudName.GCP).withId("prod.gcp-us-east1-b").build() - ); - var context = tester.newDeploymentContext(); - var applicationPackage = new ApplicationPackageBuilder() - .region("gcp-us-east1-b") - .endpoint("default", "default") // Contains all regions by default - .build(); - - try { - context.submit(applicationPackage); - fail("Expected exception"); - } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'default' in instance 'default' contains a Google Cloud region (gcp-us-east1-b), which is not yet supported", e.getMessage()); - } - - var applicationPackage2 = new ApplicationPackageBuilder() - .region("gcp-us-east1-b") - .endpoint("gcp", "default", "gcp-us-east1-b") - .build(); - try { - context.submit(applicationPackage2); - fail("Expected exception"); - } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'gcp' in instance 'default' contains a Google Cloud region (gcp-us-east1-b), which is not yet supported", e.getMessage()); - } - } - - @Test void testDeployWithoutSourceRevision() { var context = tester.newDeploymentContext(); var applicationPackage = new ApplicationPackageBuilder() diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java index 6f0a36690ed..c42d5621a46 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java @@ -7,6 +7,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; +import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget; import com.yahoo.vespa.hosted.controller.dns.CreateRecord; import com.yahoo.vespa.hosted.controller.dns.CreateRecords; import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue; @@ -38,8 +39,16 @@ public class NameServiceQueueSerializerTest { new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"), new LatencyAliasTarget(HostName.of("alias2"), "dns-zone-02", - ZoneId.from("prod", "us-north-2")).pack())) + ZoneId.from("prod", "us-north-2")).pack()), + new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"), + new LatencyAliasTarget(HostName.of("alias2"), + "ignored", + ZoneId.from("prod", "us-south-1")).pack())) ), + new CreateRecords(List.of(new Record(Record.Type.DIRECT, RecordName.from("direct.example.com"), + new WeightedDirectTarget(RecordData.from("10.1.2.3"), + ZoneId.from("prod", "us-north-1"), + 100).pack()))), new RemoveRecords(record1.type(), record1.name()), new RemoveRecords(record2.type(), record2.data()) ); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java index 2761c736e11..29834863976 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java @@ -370,11 +370,16 @@ public class RoutingPoliciesTest { List<String> expectedRecords = List.of("c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud", "c0.app1.tenant1.gcp-us-south1-b.z.vespa-app.cloud", "c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud", + "c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud", "r0.app1.tenant1.g.vespa-app.cloud"); assertEquals(Set.copyOf(expectedRecords), tester.recordNames()); assertEquals(List.of("lb-0--tenant1.app1.default--prod.aws-us-east-1c."), tester.recordDataOf(Record.Type.CNAME, expectedRecords.get(0))); assertEquals(List.of("10.0.0.0"), tester.recordDataOf(Record.Type.A, expectedRecords.get(1))); + assertEquals(List.of("weighted/10.0.0.0/prod.gcp-us-south1-b/1"), tester.recordDataOf(Record.Type.DIRECT, expectedRecords.get(3))); + assertEquals(List.of("latency/c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud/dns-zone-1/prod.aws-us-east-1c", + "latency/c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud/ignored/prod.gcp-us-south1-b"), + tester.recordDataOf(Record.Type.ALIAS, expectedRecords.get(4))); } @Test |