aboutsummaryrefslogtreecommitdiffstats
path: root/controller-api/src/main/java/com
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@yahooinc.com>2023-07-28 14:33:32 +0200
committerHåkon Hallingstad <hakon@yahooinc.com>2023-07-28 14:33:32 +0200
commit2ee24c4628c75abb8f8495eac978b3eb75c66162 (patch)
treef7d2d7a182ecee2f2387f97a930d666f81e78bcb /controller-api/src/main/java/com
parente9613c844167b4e8a55096043f42953182cd3482 (diff)
Add cloud flag dimension
Diffstat (limited to 'controller-api/src/main/java/com')
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java36
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java35
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java49
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java80
4 files changed, 157 insertions, 43 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java
index 5842ee3c3c0..585000cf22c 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java
@@ -1,9 +1,11 @@
// 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.systemflags.v1;
+import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.flags.json.FlagData;
import java.net.URI;
import java.util.List;
@@ -20,12 +22,14 @@ import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.z
*/
class ConfigServerFlagsTarget implements FlagsTarget {
private final SystemName system;
+ private final CloudName cloud;
private final ZoneId zone;
private final URI endpoint;
private final AthenzIdentity identity;
- ConfigServerFlagsTarget(SystemName system, ZoneId zone, URI endpoint, AthenzIdentity identity) {
+ ConfigServerFlagsTarget(SystemName system, CloudName cloud, ZoneId zone, URI endpoint, AthenzIdentity identity) {
this.system = Objects.requireNonNull(system);
+ this.cloud = Objects.requireNonNull(cloud);
this.zone = Objects.requireNonNull(zone);
this.endpoint = Objects.requireNonNull(endpoint);
this.identity = Objects.requireNonNull(identity);
@@ -36,16 +40,32 @@ class ConfigServerFlagsTarget implements FlagsTarget {
@Override public Optional<AthenzIdentity> athenzHttpsIdentity() { return Optional.of(identity); }
@Override public String asString() { return String.format("%s.%s", system.value(), zone.value()); }
- @Override public boolean equals(Object o) {
+ @Override
+ public FlagData partiallyResolveFlagData(FlagData data) {
+ return FlagsTarget.partialResolve(data, system, cloud, zone);
+ }
+
+ @Override
+ public String toString() {
+ return "ConfigServerFlagsTarget{" +
+ "system=" + system +
+ ", cloud=" + cloud +
+ ", zone=" + zone +
+ ", endpoint=" + endpoint +
+ ", identity=" + identity +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConfigServerFlagsTarget that = (ConfigServerFlagsTarget) o;
- return system == that.system &&
- Objects.equals(zone, that.zone) &&
- Objects.equals(endpoint, that.endpoint) &&
- Objects.equals(identity, that.identity);
+ return system == that.system && cloud.equals(that.cloud) && zone.equals(that.zone) && endpoint.equals(that.endpoint) && identity.equals(that.identity);
}
- @Override public int hashCode() { return Objects.hash(system, zone, endpoint, identity); }
+ @Override
+ public int hashCode() {
+ return Objects.hash(system, cloud, zone, endpoint, identity);
+ }
}
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java
index efeaf12de1c..043c6ea5963 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java
@@ -1,8 +1,11 @@
// 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.systemflags.v1;
+import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.flags.json.FlagData;
import java.net.URI;
import java.util.List;
@@ -18,20 +21,44 @@ import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.s
*/
class ControllerFlagsTarget implements FlagsTarget {
private final SystemName system;
+ private final CloudName cloud;
+ private final ZoneId zone;
- ControllerFlagsTarget(SystemName system) { this.system = Objects.requireNonNull(system); }
+ ControllerFlagsTarget(SystemName system, CloudName cloud, ZoneId zone) {
+ this.system = Objects.requireNonNull(system);
+ this.cloud = Objects.requireNonNull(cloud);
+ this.zone = Objects.requireNonNull(zone);
+ }
@Override public List<String> flagDataFilesPrioritized() { return List.of(controllerFile(system), systemFile(system), defaultFile()); }
@Override public URI endpoint() { return URI.create("https://localhost:4443/"); } // Note: Cannot use VIPs for controllers due to network configuration on AWS
@Override public Optional<AthenzIdentity> athenzHttpsIdentity() { return Optional.empty(); }
@Override public String asString() { return String.format("%s.controller", system.value()); }
- @Override public boolean equals(Object o) {
+ @Override
+ public FlagData partiallyResolveFlagData(FlagData data) {
+ return FlagsTarget.partialResolve(data, system, cloud, zone);
+ }
+
+ @Override
+ public String toString() {
+ return "ControllerFlagsTarget{" +
+ "system=" + system +
+ ", cloud=" + cloud +
+ ", zone=" + zone +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ControllerFlagsTarget that = (ControllerFlagsTarget) o;
- return system == that.system;
+ return system == that.system && cloud.equals(that.cloud) && zone.equals(that.zone);
}
- @Override public int hashCode() { return Objects.hash(system); }
+ @Override
+ public int hashCode() {
+ return Objects.hash(system, cloud, zone);
+ }
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java
index 1c8e68ff378..b42b22d3eff 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java
@@ -1,20 +1,31 @@
// 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.systemflags.v1;
+import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.config.provision.zone.ZoneList;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagDefinition;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import java.net.URI;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import static com.yahoo.vespa.flags.FetchVector.Dimension.CLOUD;
+import static com.yahoo.vespa.flags.FetchVector.Dimension.SYSTEM;
+import static com.yahoo.vespa.flags.FetchVector.Dimension.ZONE_ID;
+
/**
* Represents either configservers in a zone or controllers in a system.
*
@@ -38,24 +49,28 @@ public interface FlagsTarget {
Optional<AthenzIdentity> athenzHttpsIdentity();
String asString();
+ FlagData partiallyResolveFlagData(FlagData data);
+
static Set<FlagsTarget> getAllTargetsInSystem(ZoneRegistry registry, boolean reachableOnly) {
- SystemName system = registry.system();
Set<FlagsTarget> targets = new HashSet<>();
ZoneList filteredZones = reachableOnly ? registry.zones().reachable() : registry.zones().all();
for (ZoneApi zone : filteredZones.zones()) {
- targets.add(forConfigServer(registry, zone.getId()));
+ targets.add(forConfigServer(registry, zone));
}
- targets.add(forController(system));
+ targets.add(forController(registry.systemZone()));
return targets;
}
- static FlagsTarget forController(SystemName systemName) {
- return new ControllerFlagsTarget(systemName);
+ static FlagsTarget forController(ZoneApi controllerZone) {
+ return new ControllerFlagsTarget(controllerZone.getSystemName(), controllerZone.getCloudName(), controllerZone.getVirtualId());
}
- static FlagsTarget forConfigServer(ZoneRegistry registry, ZoneId zoneId) {
- return new ConfigServerFlagsTarget(
- registry.system(), zoneId, registry.getConfigServerVipUri(zoneId), registry.getConfigServerHttpsIdentity(zoneId));
+ static FlagsTarget forConfigServer(ZoneRegistry registry, ZoneApi zone) {
+ return new ConfigServerFlagsTarget(registry.system(),
+ zone.getCloudName(),
+ zone.getVirtualId(),
+ registry.getConfigServerVipUri(zone.getVirtualId()),
+ registry.getConfigServerHttpsIdentity(zone.getVirtualId()));
}
static String defaultFile() { return jsonFile("default"); }
@@ -64,6 +79,24 @@ public interface FlagsTarget {
static String zoneFile(SystemName system, ZoneId zone) { return jsonFile(system.value() + "." + zone.environment().value() + "." + zone.region().value()); }
static String controllerFile(SystemName system) { return jsonFile(system.value() + ".controller"); }
+ /** Partially resolve the system, cloud, and zone dimensions, except those dimensions defined by the flag for a controller zone. */
+ static FlagData partialResolve(FlagData data, SystemName system, CloudName cloud, ZoneId virtualZoneId) {
+ Set<FetchVector.Dimension> flagDimensions =
+ virtualZoneId.equals(ZoneId.ofVirtualControllerZone()) ?
+ Flags.getFlag(data.id())
+ .map(FlagDefinition::getDimensions)
+ .map(Set::copyOf)
+ // E.g. testing: Assume unknown flag should resolve any and all dimensions below
+ .orElse(EnumSet.noneOf(FetchVector.Dimension.class)) :
+ EnumSet.noneOf(FetchVector.Dimension.class);
+
+ var fetchVector = new FetchVector();
+ if (!flagDimensions.contains(SYSTEM)) fetchVector = fetchVector.with(SYSTEM, system.value());
+ if (!flagDimensions.contains(CLOUD)) fetchVector = fetchVector.with(CLOUD, cloud.value());
+ if (!flagDimensions.contains(ZONE_ID)) fetchVector = fetchVector.with(ZONE_ID, virtualZoneId.value());
+ return fetchVector.isEmpty() ? data : data.partialResolve(fetchVector);
+ }
+
private static String jsonFile(String nameWithoutExtension) { return nameWithoutExtension + ".json"; }
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java
index 60950341a42..169387fd2ab 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java
@@ -4,9 +4,16 @@ package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.CloudName;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.zone.ZoneApi;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.text.JSON;
import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FlagId;
@@ -38,6 +45,9 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
+import static com.yahoo.config.provision.CloudName.AWS;
+import static com.yahoo.config.provision.CloudName.GCP;
+import static com.yahoo.config.provision.CloudName.YAHOO;
import static com.yahoo.yolean.Exceptions.uncheck;
/**
@@ -189,7 +199,10 @@ public class SystemFlagsDataArchive {
if (rawData.isBlank()) {
flagData = new FlagData(directoryDeducedFlagId);
} else {
- String normalizedRawData = normalizeJson(rawData);
+ Set<ZoneId> zones = systemDefinition == null ?
+ Set.of() :
+ systemDefinition.zones().all().zones().stream().map(ZoneApi::getVirtualId).collect(Collectors.toSet());
+ String normalizedRawData = normalizeJson(rawData, zones);
flagData = FlagData.deserialize(normalizedRawData);
if (!directoryDeducedFlagId.equals(flagData.id())) {
throw new IllegalArgumentException(
@@ -217,41 +230,62 @@ public class SystemFlagsDataArchive {
builder.addFile(filename, flagData);
}
- static String normalizeJson(String json) {
+ static String normalizeJson(String json, Set<ZoneId> zones) {
JsonNode root = uncheck(() -> mapper.readTree(json));
removeCommentsRecursively(root);
- verifyValues(root);
+ verifyValues(root, zones);
return root.toString();
}
- private static void verifyValues(JsonNode root) {
+ private static void verifyValues(JsonNode root, Set<ZoneId> zones) {
var cursor = new JsonAccessor(root);
cursor.get("rules").forEachArrayElement(rule -> rule.get("conditions").forEachArrayElement(condition -> {
- var dimension = condition.get("dimension");
- if (dimension.isEqualTo(DimensionHelper.toWire(FetchVector.Dimension.APPLICATION_ID))) {
- condition.get("values").forEachArrayElement(conditionValue -> {
- String applicationIdString = conditionValue.asString()
- .orElseThrow(() -> new IllegalArgumentException("Non-string application ID: " + conditionValue));
- // Throws exception if not recognized
- ApplicationId.fromSerializedForm(applicationIdString);
+ FetchVector.Dimension dimension = DimensionHelper
+ .fromWire(condition.get("dimension")
+ .asString()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid dimension in condition: " + condition)));
+ switch (dimension) {
+ case APPLICATION_ID -> validateStringValues(condition, ApplicationId::fromSerializedForm);
+ case CONSOLE_USER_EMAIL -> validateStringValues(condition, email -> {});
+ case CLOUD -> validateStringValues(condition, cloud -> {
+ if (!Set.of(YAHOO, AWS, GCP).contains(CloudName.from(cloud)))
+ throw new IllegalArgumentException("Unknown cloud: " + cloud);
});
- } else if (dimension.isEqualTo(DimensionHelper.toWire(FetchVector.Dimension.NODE_TYPE))) {
- condition.get("values").forEachArrayElement(conditionValue -> {
- String nodeTypeString = conditionValue.asString()
- .orElseThrow(() -> new IllegalArgumentException("Non-string node type: " + conditionValue));
- // Throws exception if not recognized
- NodeType.valueOf(nodeTypeString);
+ case CLUSTER_ID -> validateStringValues(condition, ClusterSpec.Id::from);
+ case CLUSTER_TYPE -> validateStringValues(condition, ClusterSpec.Type::from);
+ case HOSTNAME -> validateStringValues(condition, HostName::of);
+ case NODE_TYPE -> validateStringValues(condition, NodeType::valueOf);
+ case SYSTEM -> validateStringValues(condition, system -> {
+ if (!Set.of(SystemName.cd, SystemName.main, SystemName.PublicCd, SystemName.Public).contains(SystemName.from(system)))
+ throw new IllegalArgumentException("Unknown system: " + system);
+ });
+ case TENANT_ID -> validateStringValues(condition, TenantName::from);
+ case VESPA_VERSION -> validateStringValues(condition, versionString -> {
+ Version vespaVersion = Version.fromString(versionString);
+ if (vespaVersion.getMajor() < 8)
+ throw new IllegalArgumentException("Major Vespa version must be at least 8: " + versionString);
+ });
+ case ZONE_ID -> validateStringValues(condition, zoneId -> {
+ if (!zones.contains(ZoneId.from(zoneId)))
+ throw new IllegalArgumentException("Unknown zone: " + zoneId);
});
- } else if (dimension.isEqualTo(DimensionHelper.toWire(FetchVector.Dimension.CONSOLE_USER_EMAIL))) {
- condition.get("values").forEachArrayElement(conditionValue -> conditionValue.asString()
- .orElseThrow(() -> new IllegalArgumentException("Non-string email address: " + conditionValue)));
- } else if (dimension.isEqualTo(DimensionHelper.toWire(FetchVector.Dimension.TENANT_ID))) {
- condition.get("values").forEachArrayElement(conditionValue -> conditionValue.asString()
- .orElseThrow(() -> new IllegalArgumentException("Non-string tenant ID: " + conditionValue)));
}
}));
}
+ private static void validateStringValues(JsonAccessor condition, Consumer<String> valueValidator) {
+ condition.get("values").forEachArrayElement(conditionValue -> {
+ String value = conditionValue.asString()
+ .orElseThrow(() -> {
+ String dimension = condition.get("dimension").asString().orElseThrow();
+ String type = condition.get("type").asString().orElseThrow();
+ return new IllegalArgumentException("Non-string value in %s %s condition: %s".formatted(
+ dimension, type, conditionValue));
+ });
+ valueValidator.accept(value);
+ });
+ }
+
private static void removeCommentsRecursively(JsonNode node) {
if (node instanceof ObjectNode) {
ObjectNode objectNode = (ObjectNode) node;