summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorValerij Fredriksen <freva@users.noreply.github.com>2022-10-10 15:19:10 +0200
committerGitHub <noreply@github.com>2022-10-10 15:19:10 +0200
commitbfc6c2d96a5a30f5134b35a703a62f2fd8767cd3 (patch)
tree8395471db264645243a30d70bf3a6baee82a1475
parent8be08877e45a714263e7e1a48d56a41ebc5bfb9b (diff)
parent63e2e795b44a8487b9bc48b92cc30f3384c4bb1b (diff)
Merge pull request #24374 from vespa-engine/freva/gcp-routing
Global endpoints in GCP
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java57
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java6
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java66
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java6
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java70
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java36
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java107
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java32
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java5
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