diff options
author | Håkon Hallingstad <hakon@yahooinc.com> | 2023-07-28 14:33:32 +0200 |
---|---|---|
committer | Håkon Hallingstad <hakon@yahooinc.com> | 2023-07-28 14:33:32 +0200 |
commit | 2ee24c4628c75abb8f8495eac978b3eb75c66162 (patch) | |
tree | f7d2d7a182ecee2f2387f97a930d666f81e78bcb /controller-api | |
parent | e9613c844167b4e8a55096043f42953182cd3482 (diff) |
Add cloud flag dimension
Diffstat (limited to 'controller-api')
5 files changed, 233 insertions, 130 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; diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java index d010893f1d4..b514ab8d3d7 100644 --- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java @@ -54,15 +54,20 @@ public class SystemFlagsDataArchiveTest { @TempDir public File temporaryFolder; - private static final FlagsTarget mainControllerTarget = FlagsTarget.forController(SYSTEM); - private static final FlagsTarget cdControllerTarget = FlagsTarget.forController(SystemName.cd); + private static final FlagsTarget mainControllerTarget = createControllerTarget(SYSTEM); + private static final FlagsTarget cdControllerTarget = createControllerTarget(SystemName.cd); private static final FlagsTarget prodUsWestCfgTarget = createConfigserverTarget(Environment.prod, "us-west-1"); private static final FlagsTarget prodUsEast3CfgTarget = createConfigserverTarget(Environment.prod, "us-east-3"); private static final FlagsTarget devUsEast1CfgTarget = createConfigserverTarget(Environment.dev, "us-east-1"); + private static FlagsTarget createControllerTarget(SystemName system) { + return new ControllerFlagsTarget(system, CloudName.YAHOO, ZoneId.from(Environment.prod, RegionName.from("us-east-1"))); + } + private static FlagsTarget createConfigserverTarget(Environment environment, String region) { return new ConfigServerFlagsTarget( SYSTEM, + CloudName.YAHOO, ZoneId.from(environment, RegionName.from(region)), URI.create("https://cfg-" + region), new AthenzService("vespa.cfg-" + region)); @@ -177,102 +182,81 @@ public class SystemFlagsDataArchiveTest { " \"comment\": \"comment d\"\n" + " }\n" + " ]\n" + - "}"))); + "}", Set.of()))); } @Test - void normalize_json_fail_on_invalid_application() { - try { - SystemFlagsDataArchive.normalizeJson("{\n" + - " \"id\": \"foo\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"conditions\": [\n" + - " {\n" + - " \"type\": \"whitelist\",\n" + - " \"dimension\": \"application\",\n" + - " \"values\": [ \"a.b.c\" ]\n" + - " }\n" + - " ],\n" + - " \"value\": true\n" + - " }\n" + - " ]\n" + - "}\n"); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("Application ids must be on the form tenant:application:instance, but was a.b.c", e.getMessage()); - } + void normalize_json_succeed_on_valid_values() { + normalizeJson("application", "\"a:b:c\""); + normalizeJson("cloud", "\"yahoo\""); + normalizeJson("cloud", "\"aws\""); + normalizeJson("cloud", "\"gcp\""); + normalizeJson("cluster-id", "\"some-id\""); + normalizeJson("cluster-type", "\"admin\""); + normalizeJson("cluster-type", "\"container\""); + normalizeJson("cluster-type", "\"content\""); + normalizeJson("console-user-email", "\"name@domain.com\""); + normalizeJson("hostname", "\"2080046-v6-11.ostk.bm2.prod.gq1.yahoo.com\""); + normalizeJson("node-type", "\"tenant\""); + normalizeJson("node-type", "\"host\""); + normalizeJson("node-type", "\"config\""); + normalizeJson("node-type", "\"host\""); + normalizeJson("system", "\"main\""); + normalizeJson("system", "\"public\""); + normalizeJson("tenant", "\"vespa\""); + normalizeJson("vespa-version", "\"8.201.13\""); + normalizeJson("zone", "\"prod.us-west-1\"", Set.of(ZoneId.from("prod.us-west-1"))); } - @Test - void normalize_json_fail_on_invalid_node_type() { - try { - SystemFlagsDataArchive.normalizeJson("{\n" + - " \"id\": \"foo\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"conditions\": [\n" + - " {\n" + - " \"type\": \"whitelist\",\n" + - " \"dimension\": \"node-type\",\n" + - " \"values\": [ \"footype\" ]\n" + - " }\n" + - " ],\n" + - " \"value\": true\n" + - " }\n" + - " ]\n" + - "}\n"); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("No enum constant com.yahoo.config.provision.NodeType.footype", e.getMessage()); - } + private void normalizeJson(String dimension, String jsonValue) { + normalizeJson(dimension, jsonValue, Set.of()); } - @Test - void normalize_json_fail_on_invalid_email() { - try { - SystemFlagsDataArchive.normalizeJson("{\n" + - " \"id\": \"foo\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"conditions\": [\n" + - " {\n" + - " \"type\": \"whitelist\",\n" + - " \"dimension\": \"console-user-email\",\n" + - " \"values\": [ 123 ]\n" + - " }\n" + - " ],\n" + - " \"value\": true\n" + - " }\n" + - " ]\n" + - "}\n"); - fail(); - } catch (IllegalArgumentException e) { - assertEquals("Non-string email address: 123", e.getMessage()); - } + private void normalizeJson(String dimension, String jsonValue, Set<ZoneId> zones) { + SystemFlagsDataArchive.normalizeJson(""" + { + "id": "foo", + "rules": [ + { + "conditions": [ + { + "type": "whitelist", + "dimension": "%s", + "values": [ %s ] + } + ], + "value": true + } + ] + } + """.formatted(dimension, jsonValue), zones); } @Test - void normalize_json_fail_on_invalid_tenant_id() { + void normalize_json_fail_on_invalid_values() { + failNormalizeJson("application", "\"a.b.c\"", "Application ids must be on the form tenant:application:instance, but was a.b.c"); + failNormalizeJson("cloud", "\"foo\"", "Unknown cloud: foo"); + // failNormalizeJson("cluster-id", ... any String is valid + failNormalizeJson("cluster-type", "\"foo\"", "Illegal cluster type 'foo'"); + failNormalizeJson("console-user-email", "123", "Non-string value in console-user-email whitelist condition: 123"); + failNormalizeJson("hostname", "\"not:a:hostname\"", "hostname must match '(([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])\\.?', but got: 'not:a:hostname'"); + failNormalizeJson("node-type", "\"footype\"", "No enum constant com.yahoo.config.provision.NodeType.footype"); + failNormalizeJson("system", "\"bar\"", "'bar' is not a valid system"); + failNormalizeJson("tenant", "123", "Non-string value in tenant whitelist condition: 123"); + failNormalizeJson("vespa-version", "\"not-a-version\"", "For input string: \"not-a-version\""); + failNormalizeJson("zone", "\"dev.non-existing-zone\"", Set.of(ZoneId.from("prod.example-region")), "Unknown zone: dev.non-existing-zone"); + } + + private void failNormalizeJson(String dimension, String jsonValue, String expectedExceptionMessage) { + failNormalizeJson(dimension, jsonValue, Set.of(), expectedExceptionMessage); + } + + private void failNormalizeJson(String dimension, String jsonValue, Set<ZoneId> zones, String expectedExceptionMessage) { try { - SystemFlagsDataArchive.normalizeJson("{\n" + - " \"id\": \"foo\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"conditions\": [\n" + - " {\n" + - " \"type\": \"whitelist\",\n" + - " \"dimension\": \"tenant\",\n" + - " \"values\": [ 123 ]\n" + - " }\n" + - " ],\n" + - " \"value\": true\n" + - " }\n" + - " ]\n" + - "}\n"); + normalizeJson(dimension, jsonValue, zones); fail(); - } catch (IllegalArgumentException e) { - assertEquals("Non-string tenant ID: 123", e.getMessage()); + } catch (RuntimeException e) { + assertEquals(expectedExceptionMessage, e.getMessage()); } } @@ -291,6 +275,11 @@ public class SystemFlagsDataArchiveTest { // Cannot use the standard registry mock as it's located in controller-server module ZoneRegistry registryMock = mock(ZoneRegistry.class); when(registryMock.system()).thenReturn(SystemName.main); + ZoneApi zoneApi = mock(ZoneApi.class); + when(zoneApi.getSystemName()).thenReturn(SystemName.main); + when(zoneApi.getCloudName()).thenReturn(CloudName.YAHOO); + when(zoneApi.getVirtualId()).thenReturn(ZoneId.ofVirtualControllerZone()); + when(registryMock.systemZone()).thenReturn(zoneApi); when(registryMock.getConfigServerVipUri(any())).thenReturn(URI.create("http://localhost:8080/")); when(registryMock.getConfigServerHttpsIdentity(any())).thenReturn(new AthenzService("domain", "servicename")); ZoneList zoneListMock = mock(ZoneList.class); @@ -333,7 +322,7 @@ public class SystemFlagsDataArchiveTest { @Override public SystemName getSystemName() { return SystemName.main; } @Override public ZoneId getId() { return zoneId; } - @Override public CloudName getCloudName() { throw new UnsupportedOperationException(); } + @Override public CloudName getCloudName() { return CloudName.YAHOO; } @Override public String getCloudNativeRegionName() { throw new UnsupportedOperationException(); } } |