summaryrefslogtreecommitdiffstats
path: root/routing-generator/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'routing-generator/src/test')
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingGeneratorTest.java77
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingTableTest.java64
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/TestUtil.java34
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HealthStatusMock.java26
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HttpClientMock.java79
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/RoutingStatusMock.java29
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxHealthClientTest.java73
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxMetricsReporterTest.java162
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxTest.java216
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/restapi/AkamaiHandlerTest.java110
-rw-r--r--routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/status/RoutingStatusClientTest.java72
-rw-r--r--routing-generator/src/test/resources/lbservices-config52
-rw-r--r--routing-generator/src/test/resources/nginx-health-multiple-tenants-application-metrics.json18
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-all-down.json11
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-all-up-but-other-down.json15
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-all-up.json9
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-policy-down.json12
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-policy-up.json12
-rw-r--r--routing-generator/src/test/resources/nginx-health-output-stream.json12
-rw-r--r--routing-generator/src/test/resources/nginx-health-output.json11
-rw-r--r--routing-generator/src/test/resources/nginx-updated.conf56
-rw-r--r--routing-generator/src/test/resources/nginx.conf48
22 files changed, 1198 insertions, 0 deletions
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingGeneratorTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingGeneratorTest.java
new file mode 100644
index 00000000000..3e8b9be572f
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingGeneratorTest.java
@@ -0,0 +1,77 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing;
+
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.subscription.ConfigSet;
+import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author mpolden
+ */
+public class RoutingGeneratorTest {
+
+ @Test(timeout = 2000)
+ public void config_subscription() {
+ RouterMock router = new RouterMock();
+ RoutingGenerator generator = new RoutingGenerator(new ConfigSetMock(), router, new ManualClock());
+ try {
+ router.awaitLoad();
+ assertNotNull("Router loads table", router.currentTable);
+ assertEquals("Routing generator and router has same table",
+ generator.routingTable().get(),
+ router.currentTable);
+ } finally {
+ generator.deconstruct();
+ }
+ }
+
+ private static class RouterMock implements Router {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ private volatile RoutingTable currentTable = null;
+
+ @Override
+ public void load(RoutingTable table) {
+ currentTable = table;
+ latch.countDown();
+ }
+
+ public void awaitLoad() {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ @SuppressWarnings("removal") // TODO Vespa 8: remove
+ private static class ConfigSetMock extends ConfigSet {
+
+ private int attempt = 0;
+
+ public ConfigSetMock() {
+ addBuilder("*", new LbServicesConfig.Builder());
+ }
+
+ @Override
+ public ConfigInstance.Builder get(ConfigKey<?> key) {
+ if (++attempt <= 5) {
+ throw new RuntimeException("Failed to get config on attempt " + attempt);
+ }
+ return super.get(key);
+ }
+
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingTableTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingTableTest.java
new file mode 100644
index 00000000000..288eabe16f7
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/RoutingTableTest.java
@@ -0,0 +1,64 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Endpoint;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Real;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Target;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author mpolden
+ */
+public class RoutingTableTest {
+
+ @Test
+ public void translate_from_lb_services_config() {
+ RoutingTable expected = new RoutingTable(Map.of(
+ new Endpoint("beta.music.vespa.us-north-1.vespa.oath.cloud"),
+ Target.create(ApplicationId.from("vespa", "music", "beta"),
+ ClusterSpec.Id.from("default"), ZoneId.from("prod.us-north-1"),
+ List.of(new Real("host3-beta", 4443, 1, true),
+ new Real("host4-beta", 4443, 1, true))),
+
+ new Endpoint("music.vespa.global.vespa.oath.cloud"),
+ Target.create(ApplicationId.from("vespa", "music", "default"),
+ ClusterSpec.Id.from("default"), ZoneId.from("prod.us-north-1"),
+ List.of(new Real("host1-default", 4443, 1, true),
+ new Real("host2-default", 4443, 1, true))),
+
+ new Endpoint("music.vespa.us-north-1.vespa.oath.cloud"),
+ Target.create(ApplicationId.from("vespa", "music", "default"),
+ ClusterSpec.Id.from("default"), ZoneId.from("prod.us-north-1"),
+ List.of(new Real("host1-default", 4443, 1, true),
+ new Real("host2-default", 4443, 1, true))),
+
+ new Endpoint("rotation-02.vespa.global.routing"),
+ Target.create(ApplicationId.from("vespa", "music", "default"),
+ ClusterSpec.Id.from("default"), ZoneId.from("prod.us-north-1"),
+ List.of(new Real("host1-default", 4443, 1, true),
+ new Real("host2-default", 4443, 1, true))),
+
+ new Endpoint("use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud"),
+ Target.create("use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud", TenantName.from("vespa"), ApplicationName.from("music"),
+ ClusterSpec.Id.from("default"), ZoneId.from("prod.us-north-1"),
+ List.of(new Real("host3-beta", 4443, 1, true),
+ new Real("host4-beta", 4443, 1, true),
+ new Real("host1-default", 4443, 0, true),
+ new Real("host2-default", 4443, 0, true)))
+ ), 42);
+
+ RoutingTable actual = TestUtil.readRoutingTable("lbservices-config");
+ assertEquals(expected, actual);
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/TestUtil.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/TestUtil.java
new file mode 100644
index 00000000000..09440cfaac7
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/TestUtil.java
@@ -0,0 +1,34 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing;
+
+import com.yahoo.cloud.config.LbServicesConfig;
+import com.yahoo.config.subscription.CfgConfigPayloadBuilder;
+import com.yahoo.yolean.Exceptions;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * @author mpolden
+ */
+public class TestUtil {
+
+ private static final Path testData = Paths.get("src/test/resources/");
+
+ @SuppressWarnings("removal") // TODO Vespa 8: remove
+ public static RoutingTable readRoutingTable(String filename) {
+ List<String> lines = Exceptions.uncheck(() -> Files.readAllLines(testFile(filename),
+ StandardCharsets.UTF_8));
+ LbServicesConfig lbServicesConfig = new CfgConfigPayloadBuilder().deserialize(lines)
+ .toInstance(LbServicesConfig.class, "*");
+ return RoutingTable.from(lbServicesConfig, 42);
+ }
+
+ public static Path testFile(String filename) {
+ return testData.resolve(filename);
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HealthStatusMock.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HealthStatusMock.java
new file mode 100644
index 00000000000..66aff350b8b
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HealthStatusMock.java
@@ -0,0 +1,26 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.mock;
+
+import com.yahoo.vespa.hosted.routing.status.HealthStatus;
+import com.yahoo.vespa.hosted.routing.status.ServerGroup;
+
+import java.util.List;
+
+/**
+ * @author mpolden
+ */
+public class HealthStatusMock implements HealthStatus {
+
+ private ServerGroup status = new ServerGroup(List.of());
+
+ public HealthStatusMock setStatus(ServerGroup newStatus) {
+ this.status = newStatus;
+ return this;
+ }
+
+ @Override
+ public ServerGroup servers() {
+ return status;
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HttpClientMock.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HttpClientMock.java
new file mode 100644
index 00000000000..025eac90b8d
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/HttpClientMock.java
@@ -0,0 +1,79 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.mock;
+
+import com.yahoo.yolean.Exceptions;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HttpContext;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * @author mpolden
+ */
+@SuppressWarnings("deprecation") // Deprecations in third-party interface
+public class HttpClientMock extends CloseableHttpClient {
+
+ private final Map<String, CloseableHttpResponse> responses = new HashMap<>();
+
+ public HttpClientMock setResponse(String method, String url, CloseableHttpResponse response) {
+ responses.put(requestKey(method, url), response);
+ return this;
+ }
+
+ @Override
+ protected CloseableHttpResponse doExecute(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext) {
+ String key = requestKey(httpRequest.getRequestLine().getMethod(), httpRequest.getRequestLine().getUri());
+ CloseableHttpResponse response = responses.get(key);
+ if (response == null) {
+ throw new IllegalArgumentException("No response defined for " + key);
+ }
+ return response;
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public HttpParams getParams() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ClientConnectionManager getConnectionManager() {
+ throw new UnsupportedOperationException();
+ }
+
+ private static String requestKey(String method, String url) {
+ return method.toUpperCase(Locale.ENGLISH) + " " + url;
+ }
+
+ public static class JsonResponse extends BasicHttpResponse implements CloseableHttpResponse {
+
+ public JsonResponse(Path jsonFile, int code) {
+ this(Exceptions.uncheck(() -> Files.readString(jsonFile)), code);
+ }
+
+ public JsonResponse(String json, int code) {
+ super(HttpVersion.HTTP_1_1, code, null);
+ setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
+ }
+
+ @Override
+ public void close() {}
+
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/RoutingStatusMock.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/RoutingStatusMock.java
new file mode 100644
index 00000000000..931627cd7c4
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/mock/RoutingStatusMock.java
@@ -0,0 +1,29 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.mock;
+
+import com.yahoo.vespa.hosted.routing.status.RoutingStatus;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author mortent
+ */
+public class RoutingStatusMock implements RoutingStatus {
+
+ private final Set<String> outOfRotation = new HashSet<>();
+
+ @Override
+ public boolean isActive(String upstreamName) {
+ return !outOfRotation.contains(upstreamName);
+ }
+
+ public RoutingStatusMock setStatus(String upstreamName, boolean active) {
+ if (active) {
+ outOfRotation.remove(upstreamName);
+ } else {
+ outOfRotation.add(upstreamName);
+ }
+ return this;
+ }
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxHealthClientTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxHealthClientTest.java
new file mode 100644
index 00000000000..722bc55437f
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxHealthClientTest.java
@@ -0,0 +1,73 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.nginx;
+
+import com.yahoo.vespa.hosted.routing.mock.HttpClientMock;
+import org.junit.Test;
+
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author oyving
+ * @author mpolden
+ */
+public class NginxHealthClientTest {
+
+ @Test
+ public void unknown_endpoint_is_down() {
+ NginxHealthClient client = createClient("nginx-health-output.json");
+ assertFalse(client.servers().isHealthy("no.such.endpoint"));
+ }
+
+ @Test
+ public void all_down_endpoint_is_down() {
+ NginxHealthClient service = createClient("nginx-health-output-all-down.json");
+ assertFalse(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ }
+
+ @Test
+ public void all_up_endpoint_is_up() {
+ NginxHealthClient service = createClient("nginx-health-output-all-up.json");
+ assertTrue(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ }
+
+ @Test
+ public void two_down_endpoint_is_down() {
+ NginxHealthClient service = createClient("nginx-health-output-policy-down.json");
+ assertFalse(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ }
+
+ @Test
+ public void one_down_endpoint_is_up() {
+ NginxHealthClient service = createClient("nginx-health-output-policy-up.json");
+ assertTrue(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ }
+
+ @Test
+ public void all_up_but_other_endpoint_down() {
+ NginxHealthClient service = createClient("nginx-health-output-all-up-but-other-down.json");
+ assertTrue(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ assertFalse(service.servers().isHealthy("frog.prod.music.vespa.us-east-2.prod"));
+ }
+
+ @Test
+ public void all_up_but_other_endpoint_down_stream() {
+ NginxHealthClient service = createClient("nginx-health-output-stream.json");
+ assertTrue(service.servers().isHealthy("gateway.prod.music.vespa.us-east-2.prod"));
+ assertFalse(service.servers().isHealthy("frog.prod.music.vespa.us-east-2.prod"));
+ }
+
+ private static NginxHealthClient createClient(String file) {
+ HttpClientMock httpClient = new HttpClientMock().setResponse("GET",
+ "http://localhost:4080/health-status/?format=json",
+ response(file));
+ return new NginxHealthClient(httpClient);
+ }
+
+ private static HttpClientMock.JsonResponse response(String file) {
+ return new HttpClientMock.JsonResponse(Paths.get("src/test/resources/", file), 200);
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxMetricsReporterTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxMetricsReporterTest.java
new file mode 100644
index 00000000000..72014047db7
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxMetricsReporterTest.java
@@ -0,0 +1,162 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.nginx;
+
+import com.google.common.jimfs.Jimfs;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.jdisc.test.MockMetric;
+import com.yahoo.vespa.hosted.routing.RoutingTable;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Endpoint;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Target;
+import com.yahoo.vespa.hosted.routing.mock.HealthStatusMock;
+import com.yahoo.vespa.hosted.routing.status.ServerGroup;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author mortent
+ * @author mpolden
+ */
+public class NginxMetricsReporterTest {
+
+ private static final ApplicationId routingApp = ApplicationId.from("hosted-vespa", "routing", "default");
+
+ private static final Target target0 = createTarget("vespa", "music", "prod", "gateway");
+ private static final Target target1 = createTarget("vespa", "music", "prod", "qrs");
+ private static final Target target2 = createTarget("vespa", "donbot", "default", "default");
+ private static final Target target3 = createTarget("notchecked", "notchecked", "default", "default");
+ private static final Target target4 = createTarget("not", "appearing-in-routing", "default", "default");
+ private static final Target target5 = createTarget(routingApp.tenant().value(), routingApp.application().value(), routingApp.instance().value(), "routing");
+
+ private final MockMetric metrics = new MockMetric();
+ private final RoutingTable routingTable = createRoutingTable();
+ private final HealthStatusMock healthService = new HealthStatusMock();
+ private final FileSystem fileSystem = Jimfs.newFileSystem();
+ private final NginxMetricsReporter reporter = new NginxMetricsReporter(routingApp, metrics, healthService,
+ fileSystem, Duration.ofDays(1),
+ () -> Optional.of(routingTable));
+
+ @Test
+ public void upstream_metrics() {
+ List<ServerGroup.Server> servers = List.of(
+ new ServerGroup.Server("gateway.prod.music.vespa.us-east-2.prod", "10.78.114.166:4080", true),
+ new ServerGroup.Server("gateway.prod.music.vespa.us-east-2.prod", "10.78.115.68:4080", true),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.114.166:4080", true),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.115.68:4080", true),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.114.166:4080", false),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.115.68:4080", false),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.114.166:4080", false),
+ new ServerGroup.Server("qrs.prod.music.vespa.us-east-2.prod", "10.78.115.68:4080", false),
+ new ServerGroup.Server("donbot.vespa.us-east-2.prod", "10.201.8.47:4080", true),
+ new ServerGroup.Server("donbot.vespa.us-east-2.prod", "10.201.14.46:4080", false),
+ new ServerGroup.Server("appearing-in-routing.not.us-east-2.prod", "10.201.14.50:4080", false)
+ );
+ healthService.setStatus(new ServerGroup(servers));
+ reporter.run();
+
+ assertEquals(2D, getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target0)), Double.MIN_VALUE);
+ assertEquals(0D, getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target0)), Double.MIN_VALUE);
+ assertEquals(0D, getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target0)), Double.MIN_VALUE);
+
+ assertEquals(2L, getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target1)), Double.MIN_VALUE);
+ assertEquals(4L, getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target1)), Double.MIN_VALUE);
+ assertEquals(0L, getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target1)), Double.MIN_VALUE);
+
+ assertEquals(1D, getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target2)), Double.MIN_VALUE);
+ assertEquals(1D, getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target2)), Double.MIN_VALUE);
+ assertEquals(0D, getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target2)), Double.MIN_VALUE);
+
+ // If the application appears in routing table - but not in health check cache yet
+ assertEquals(0D, getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target3)), Double.MIN_VALUE);
+ assertEquals(0D, getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target3)), Double.MIN_VALUE);
+ assertEquals(1D, getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target3)), Double.MIN_VALUE);
+
+ // If the application does not appear in routing table - but still appears in cache
+ assertNull(getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target4)));
+ assertNull(getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target4)));
+ assertNull(getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target4)));
+
+ assertNull(getMetric(NginxMetricsReporter.UPSTREAM_UP_METRIC, dimensionsOf(target5)));
+ assertNull(getMetric(NginxMetricsReporter.UPSTREAM_DOWN_METRIC, dimensionsOf(target5)));
+ assertEquals(1D, getMetric(NginxMetricsReporter.UPSTREAM_UNKNOWN_METRIC, dimensionsOf(target5)), Double.MIN_VALUE);
+ }
+
+ @Test
+ public void config_age_metric() throws Exception {
+ reporter.run();
+ // No files exist
+ assertEquals(0D, getMetric(NginxMetricsReporter.CONFIG_AGE_METRIC), Double.MIN_VALUE);
+
+ // Only temporary file exists
+ Path configRoot = fileSystem.getPath("/opt/vespa/var/vespa-hosted/routing/");
+ Path tempFile = configRoot.resolve("nginxl4.conf.tmp");
+ createFile(tempFile, Instant.ofEpochSecond(123));
+ reporter.run();
+ assertEquals(123D, getMetric(NginxMetricsReporter.CONFIG_AGE_METRIC), Double.MIN_VALUE);
+
+ // Only main file exists
+ Files.delete(tempFile);
+ createFile(configRoot.resolve("nginxl4.conf"), Instant.ofEpochSecond(456));
+ reporter.run();
+ assertEquals(0D, getMetric(NginxMetricsReporter.CONFIG_AGE_METRIC), Double.MIN_VALUE);
+
+ // Both files exist
+ createFile(tempFile, Instant.ofEpochSecond(123));
+ reporter.run();
+ assertEquals(333D, getMetric(NginxMetricsReporter.CONFIG_AGE_METRIC), Double.MIN_VALUE);
+ }
+
+ private double getMetric(String name) {
+ return getMetric(name, Map.of());
+ }
+
+ private Double getMetric(String name, Map<String, ?> dimensions) {
+ Map<Map<String, ?>, Double> metric = metrics.metrics().get(name);
+ if (metric == null) throw new IllegalArgumentException("Metric '" + name + "' not found");
+ return metric.get(dimensions);
+ }
+
+ private void createFile(Path path, Instant lastModified) throws IOException {
+ Files.createDirectories(path.getParent());
+ Files.createFile(path);
+ Files.setLastModifiedTime(path, FileTime.from(lastModified));
+ }
+
+ private Map<String, ?> dimensionsOf(Target target) {
+ return Map.of(
+ "tenantName", target.tenant().value(),
+ "app", String.format("%s.%s", target.application().value(), target.instance().get().value()),
+ "applicationId", String.format("%s.%s.%s", target.tenant().value(), target.application().value(), target.instance().get().value()),
+ "clusterid", target.cluster().value()
+ );
+ }
+
+ private static Target createTarget(String tenantName, String applicationName, String instanceName, String clusterName) {
+ ZoneId zone = ZoneId.from("prod", "us-east-2");
+ ClusterSpec.Id cluster = ClusterSpec.Id.from(clusterName);
+ return Target.create(ApplicationId.from(tenantName, applicationName, instanceName), cluster, zone, List.of());
+ }
+
+ private static RoutingTable createRoutingTable() {
+ return new RoutingTable(Map.of(new Endpoint("endpoint0"), target0,
+ new Endpoint("endpoint1"), target1,
+ new Endpoint("endpoint2"), target2,
+ new Endpoint("endpoint3"), target3),
+ 42);
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxTest.java
new file mode 100644
index 00000000000..bea4d2a822c
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/nginx/NginxTest.java
@@ -0,0 +1,216 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.nginx;
+
+import com.google.common.jimfs.Jimfs;
+import com.yahoo.collections.Pair;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.jdisc.test.MockMetric;
+import com.yahoo.system.ProcessExecuter;
+import com.yahoo.test.ManualClock;
+import com.yahoo.vespa.hosted.routing.RoutingTable;
+import com.yahoo.vespa.hosted.routing.TestUtil;
+import com.yahoo.vespa.hosted.routing.mock.RoutingStatusMock;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.yolean.concurrent.Sleeper;
+import org.junit.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author mpolden
+ */
+public class NginxTest {
+
+ @Test
+ public void load_routing_table() {
+ NginxTester tester = new NginxTester();
+ tester.clock.setInstant(Instant.parse("2022-01-01T15:00:00Z"));
+
+ // Load routing table
+ RoutingTable table0 = TestUtil.readRoutingTable("lbservices-config");
+ tester.load(table0)
+ .assertVerifiedConfig(1)
+ .assertLoadedConfig(true)
+ .assertConfigContents("nginx.conf")
+ .assertTemporaryConfigRemoved(true)
+ .assertMetric(Nginx.CONFIG_RELOADS_METRIC, 1)
+ .assertMetric(Nginx.OK_CONFIG_RELOADS_METRIC, 1)
+ .assertMetric(Nginx.GENERATED_UPSTREAMS_METRIC, 5);
+
+ // Loading the same table again does nothing
+ tester.load(table0);
+ tester.assertVerifiedConfig(1)
+ .assertLoadedConfig(false)
+ .assertConfigContents("nginx.conf")
+ .assertTemporaryConfigRemoved(true)
+ .assertMetric(Nginx.CONFIG_RELOADS_METRIC, 1)
+ .assertMetric(Nginx.OK_CONFIG_RELOADS_METRIC, 1)
+ .assertMetric(Nginx.GENERATED_UPSTREAMS_METRIC, 5);
+
+ // A new table is loaded
+ Map<RoutingTable.Endpoint, RoutingTable.Target> newEntries = new HashMap<>(table0.asMap());
+ newEntries.put(new RoutingTable.Endpoint("endpoint1"),
+ RoutingTable.Target.create(ApplicationId.from("t1", "a1", "i1"),
+ ClusterSpec.Id.from("default"),
+ ZoneId.from("prod", "us-north-1"),
+ List.of(new RoutingTable.Real("host42", 4443, 1, true))));
+ RoutingTable table1 = new RoutingTable(newEntries, 43);
+
+ // Verification of new table fails enough times to exhaust retries
+ tester.processExecuter.withFailCount(10);
+ try {
+ tester.load(table1);
+ fail("Expected exception");
+ } catch (Exception ignored) {}
+ tester.assertVerifiedConfig(5)
+ .assertLoadedConfig(false)
+ .assertConfigContents("nginx.conf")
+ .assertTemporaryConfigRemoved(false)
+ .assertMetric(Nginx.CONFIG_RELOADS_METRIC, 1)
+ .assertMetric(Nginx.OK_CONFIG_RELOADS_METRIC, 1);
+
+ // Verification succeeds, with few enough failures
+ tester.processExecuter.withFailCount(3);
+ tester.load(table1)
+ .assertVerifiedConfig(3)
+ .assertLoadedConfig(true)
+ .assertConfigContents("nginx-updated.conf")
+ .assertTemporaryConfigRemoved(true)
+ .assertRotatedFiles("nginxl4.conf-2022-01-01-15:00:00.000")
+ .assertMetric(Nginx.CONFIG_RELOADS_METRIC, 2)
+ .assertMetric(Nginx.OK_CONFIG_RELOADS_METRIC, 2);
+
+ // Some time passes and new tables are loaded. Old rotated files are removed
+ tester.clock.advance(Duration.ofDays(3));
+ // Simulate old rotated layer 7 config file, which should be removed
+ Exceptions.uncheck(() -> Files.createFile(NginxPath.root.in(tester.fileSystem).resolve("nginx.conf-2021-12-15-15:00:00.000")));
+ tester.load(table0);
+ tester.clock.advance(Duration.ofDays(4).plusSeconds(1));
+ tester.load(table1)
+ .assertRotatedFiles("nginxl4.conf-2022-01-04-15:00:00.000",
+ "nginxl4.conf-2022-01-08-15:00:01.000");
+ tester.clock.advance(Duration.ofDays(4));
+ tester.load(table1) // Same table is loaded again, which is a no-op, but old rotated files are still removed
+ .assertRotatedFiles("nginxl4.conf-2022-01-08-15:00:01.000");
+ }
+
+ private static class NginxTester {
+
+ private final FileSystem fileSystem = Jimfs.newFileSystem();
+ private final ManualClock clock = new ManualClock();
+ private final RoutingStatusMock routingStatus = new RoutingStatusMock();
+ private final ProcessExecuterMock processExecuter = new ProcessExecuterMock();
+ private final MockMetric metric = new MockMetric();
+ private final Nginx nginx = new Nginx(fileSystem, processExecuter, Sleeper.NOOP, clock, routingStatus, metric);
+
+ public NginxTester load(RoutingTable table) {
+ processExecuter.clearHistory();
+ nginx.load(table);
+ return this;
+ }
+
+ public NginxTester assertMetric(String name, double expected) {
+ assertEquals("Metric " + name + " has expected value", expected, metric.metrics().get(name).get(Map.of()), Double.MIN_VALUE);
+ return this;
+ }
+
+ public NginxTester assertConfigContents(String expectedConfig) {
+ String expected = Exceptions.uncheck(() -> Files.readString(TestUtil.testFile(expectedConfig)));
+ String actual = Exceptions.uncheck(() -> Files.readString(NginxPath.config.in(fileSystem)));
+ assertEquals(expected, actual);
+ return this;
+ }
+
+ public NginxTester assertTemporaryConfigRemoved(boolean removed) {
+ Path path = NginxPath.temporaryConfig.in(fileSystem);
+ assertEquals(path + (removed ? " does not exist" : " exists"), removed, !Files.exists(path));
+ return this;
+ }
+
+ public NginxTester assertRotatedFiles(String... expectedRotatedFiles) {
+ List<String> rotatedFiles = Exceptions.uncheck(() -> Files.list(NginxPath.root.in(fileSystem))
+ .map(path -> path.getFileName().toString())
+ .filter(filename -> filename.contains(".conf-"))
+ .collect(Collectors.toList()));
+ assertEquals(List.of(expectedRotatedFiles), rotatedFiles);
+ return this;
+ }
+
+ public NginxTester assertVerifiedConfig(int times) {
+ for (int i = 0; i < times; i++) {
+ assertEquals("/usr/bin/sudo /opt/vespa/bin/vespa-verify-nginx", processExecuter.history().get(i));
+ }
+ return this;
+ }
+
+ public NginxTester assertLoadedConfig(boolean loaded) {
+ String reloadCommand = "/usr/bin/sudo /opt/vespa/bin/vespa-reload-nginx";
+ if (loaded) {
+ assertEquals(reloadCommand, processExecuter.history().get(processExecuter.history().size() - 1));
+ } else {
+ assertTrue("Config is not loaded",
+ processExecuter.history.stream().noneMatch(command -> command.equals(reloadCommand)));
+ }
+ return this;
+ }
+
+ }
+
+ private static class ProcessExecuterMock extends ProcessExecuter {
+
+ private final List<String> history = new ArrayList<>();
+
+ private int wantedFailCount = 0;
+ private int currentFailCount = 0;
+
+ public List<String> history() {
+ return Collections.unmodifiableList(history);
+ }
+
+ public ProcessExecuterMock clearHistory() {
+ history.clear();
+ return this;
+ }
+
+ public ProcessExecuterMock withFailCount(int count) {
+ this.wantedFailCount = count;
+ this.currentFailCount = 0;
+ return this;
+ }
+
+ @Override
+ public Pair<Integer, String> exec(String command) {
+ history.add(command);
+ int exitCode = 0;
+ String out = "";
+ if (++currentFailCount <= wantedFailCount) {
+ exitCode = 1;
+ out = "failing to unit test";
+ }
+ return new Pair<>(exitCode, out);
+ }
+
+ @Override
+ public Pair<Integer, String> exec(String[] command) {
+ return exec(String.join(" ", command));
+ }
+
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/restapi/AkamaiHandlerTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/restapi/AkamaiHandlerTest.java
new file mode 100644
index 00000000000..e38d5a654f7
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/restapi/AkamaiHandlerTest.java
@@ -0,0 +1,110 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.restapi;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpRequestBuilder;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.vespa.hosted.routing.RoutingTable;
+import com.yahoo.vespa.hosted.routing.RoutingTable.Endpoint;
+import com.yahoo.vespa.hosted.routing.mock.HealthStatusMock;
+import com.yahoo.vespa.hosted.routing.mock.RoutingStatusMock;
+import com.yahoo.vespa.hosted.routing.status.HealthStatus;
+import com.yahoo.vespa.hosted.routing.status.ServerGroup;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author oyving
+ * @author mpolden
+ */
+public class AkamaiHandlerTest {
+
+ private static final String
+ ENDPOINT_OK = "ok.vespa.yahooapis.com",
+ ENDPOINT_UNKNOWN = "unknown.vespa.yahooapis.com",
+ ENDPOINT_UNAVAILABLE = "out.vespa.yahooapis.com",
+ ENDPOINT_UNHEALTHY = "unhealthy.vespa.yahooapis.com",
+ ENDPOINT_INACTIVE = "inactive.vespa.yahooapis.com";
+
+ private static final String ENDPOINT_WITH_PORT_OK = ENDPOINT_OK + ":4080";
+
+ private final RoutingStatusMock statusService = new RoutingStatusMock().setStatus("i3.a3.t3.us-north-1.prod", false);
+
+ private final HealthStatus healthStatus = new HealthStatusMock().setStatus(new ServerGroup(List.of(
+ new ServerGroup.Server("i1.a1.t1.us-north-1.prod", "hostport", true),
+ new ServerGroup.Server("i2.a2.t2.us-north-1.prod", "hostport", false))));
+
+ private final AkamaiHandler handler = new AkamaiHandler(ThreadedHttpRequestHandler.testContext(),
+ () -> Optional.of(makeRoutingTable()),
+ statusService,
+ healthStatus);
+
+ @Test
+ public void ok_endpoint() {
+ assertResponse(ENDPOINT_OK, 200, AkamaiHandler.ROTATION_OK_MESSAGE);
+ assertResponse(ENDPOINT_WITH_PORT_OK, 200, AkamaiHandler.ROTATION_OK_MESSAGE);
+ }
+
+ @Test
+ public void unknown_endpoint() {
+ assertResponse(ENDPOINT_UNKNOWN, 404, AkamaiHandler.ROTATION_UNKNOWN_MESSAGE);
+ }
+
+ @Test
+ public void out_of_rotation_endpoint() {
+ assertResponse(ENDPOINT_UNAVAILABLE, 404, AkamaiHandler.ROTATION_UNAVAILABLE_MESSAGE);
+ }
+
+ @Test
+ public void unhealthy_endpoint() {
+ assertResponse(ENDPOINT_UNHEALTHY, 502, AkamaiHandler.ROTATION_UNHEALTHY_MESSAGE);
+ }
+
+ @Test
+ public void inactive_endpoint() {
+ assertResponse(ENDPOINT_INACTIVE, 404, AkamaiHandler.ROTATION_INACTIVE_MESSAGE);
+ }
+
+ private void assertResponse(String rotation, int status, String message) {
+ HttpRequest req = HttpRequestBuilder.create(com.yahoo.jdisc.http.HttpRequest.Method.GET, "/akamai/v1/status")
+ .withHeader("Host", rotation)
+ .build();
+ HttpResponse response = handler.handle(req);
+ assertEquals(status, response.getStatus());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Exceptions.uncheck(() -> response.render(out));
+ String responseBody = out.toString();
+
+ String expected = "\"message\":\"" + message + "\"";
+ assertTrue("Contains expected message", responseBody.contains(expected));
+ }
+
+ private static RoutingTable makeRoutingTable() {
+ return new RoutingTable(Map.of(
+ new Endpoint(ENDPOINT_OK), createTarget("t1", "a1", "i1", "default", true),
+ new Endpoint(ENDPOINT_UNAVAILABLE), createTarget("t3", "a3", "i3", "default", true),
+ new Endpoint(ENDPOINT_UNHEALTHY), createTarget("t2", "a2", "i2", "default", true),
+ new Endpoint(ENDPOINT_INACTIVE), createTarget("t1", "a1", "i1", "default", false)
+ ), 42);
+ }
+
+ private static RoutingTable.Target createTarget(String tenantName, String applicationName, String instanceName, String clusterName, boolean routingActive) {
+ ZoneId zone = ZoneId.from("prod", "us-north-1");
+ ClusterSpec.Id cluster = ClusterSpec.Id.from(clusterName);
+ return RoutingTable.Target.create(ApplicationId.from(tenantName, applicationName, instanceName), cluster, zone,
+ List.of(new RoutingTable.Real("host", 8080, 1, routingActive)));
+ }
+
+}
diff --git a/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/status/RoutingStatusClientTest.java b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/status/RoutingStatusClientTest.java
new file mode 100644
index 00000000000..d30774a686a
--- /dev/null
+++ b/routing-generator/src/test/java/com/yahoo/vespa/hosted/routing/status/RoutingStatusClientTest.java
@@ -0,0 +1,72 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.routing.status;
+
+import com.yahoo.vespa.hosted.routing.mock.HttpClientMock;
+import com.yahoo.vespa.hosted.routing.mock.HttpClientMock.JsonResponse;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author mpolden
+ */
+public class RoutingStatusClientTest {
+
+ @Test
+ public void client() {
+ String deploymentUrl = "http://host/routing/v1/status";
+ String zoneUrl = "http://host/routing/v1/status/zone";
+ HttpClientMock httpClient = new HttpClientMock();
+ RoutingStatusClient client = new RoutingStatusClient(httpClient, URI.create("http://host"));
+
+ // Nothing is inactive
+ httpClient.setResponse("GET", deploymentUrl, inactiveDeployments())
+ .setResponse("GET", zoneUrl, zoneActive(true));
+ assertTrue(client.isActive("foo"));
+
+ // Two upstreams are set inactive
+ httpClient.setResponse("GET", deploymentUrl, inactiveDeployments("bar", "foo"));
+ client.invalidateCache();
+ assertFalse(client.isActive("foo"));
+ assertFalse(client.isActive("bar"));
+ assertTrue(client.isActive("baz"));
+
+ // Bad response results in active status
+ client.invalidateCache();
+ httpClient.setResponse("GET", deploymentUrl, badRequest("something went wrong"));
+ assertTrue(client.isActive("foo"));
+
+ // Inactive zone overrides deployment status
+ client.invalidateCache();
+ httpClient.setResponse("GET", deploymentUrl, inactiveDeployments("bar"));
+ httpClient.setResponse("GET", zoneUrl, zoneActive(false));
+ assertFalse(client.isActive("foo"));
+ assertFalse(client.isActive("bar"));
+
+ // Zone is active again. Fall back to reading deployment status
+ httpClient.setResponse("GET", zoneUrl, zoneActive(true));
+ client.invalidateCache();
+ assertTrue(client.isActive("foo"));
+ assertFalse(client.isActive("bar"));
+ }
+
+ private static JsonResponse badRequest(String message) {
+ return new JsonResponse("{\"message\":\"" + message + "\"}", 400);
+ }
+
+ private static JsonResponse zoneActive(boolean active) {
+ return new JsonResponse("{\"status\":\"" + (active ? "IN" : "OUT") + "\"}", 200);
+ }
+
+ private static JsonResponse inactiveDeployments(String... deployments) {
+ return new JsonResponse("[" + Arrays.stream(deployments)
+ .map(d -> "\"" + d + "\"")
+ .collect(Collectors.joining(",")) + "]", 200);
+ }
+
+}
diff --git a/routing-generator/src/test/resources/lbservices-config b/routing-generator/src/test/resources/lbservices-config
new file mode 100644
index 00000000000..d19fc5ee4ae
--- /dev/null
+++ b/routing-generator/src/test/resources/lbservices-config
@@ -0,0 +1,52 @@
+tenants.vespa.applications.music:prod:us-north-1:default.activeRotation true
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].hosts[0] "host1-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].hosts[1] "host2-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].dnsName "music.vespa.us-north-1.vespa.oath.cloud"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].scope "zone"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[0].weight 1
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].hosts[0] "host1-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].hosts[1] "host2-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].dnsName "music.vespa.global.vespa.oath.cloud"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].scope "global"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[1].weight 1
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].hosts[0] "host1-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].hosts[1] "host2-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].dnsName "rotation-02.vespa.global.routing"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].scope "global"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[2].weight 1
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].hosts[0] "host1-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].hosts[1] "host2-default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].dnsName "use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].scope "application"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:default.endpoints[3].weight 0
+tenants.vespa.applications.music:prod:us-north-1:beta.activeRotation true
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].hosts[0] "host3-beta"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].hosts[1] "host4-beta"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].dnsName "beta.music.vespa.us-north-1.vespa.oath.cloud"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].scope "zone"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[0].weight 1
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].hosts[0] "host3-beta"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].hosts[1] "host4-beta"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].dnsName "use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].clusterId "default"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].scope "application"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].routingMethod "sharedLayer4"
+tenants.vespa.applications.music:prod:us-north-1:beta.endpoints[1].weight 1
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.activeRotation true
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].hosts[0] "routing-host1"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].hosts[1] "routing-host2"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].dnsName "routing.hosted-vespa.cd-us-west-1.hosted-vespa.oath.cloud"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].clusterId "default"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].scope "zone"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].routingMethod "sharedLayer4"
+tenants.hosted-vespa.applications.routing:prod:cd-us-west-1:default.endpoints[0].weight 1
diff --git a/routing-generator/src/test/resources/nginx-health-multiple-tenants-application-metrics.json b/routing-generator/src/test/resources/nginx-health-multiple-tenants-application-metrics.json
new file mode 100644
index 00000000000..4f537336849
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-multiple-tenants-application-metrics.json
@@ -0,0 +1,18 @@
+
+{"servers": {
+ "total": 9,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 4, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 5, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 6, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 7, "upstream": "qrs.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 8, "upstream": "donbot.vespa.us-east-2.prod", "name": "10.201.8.47:4080", "status": "up", "rise": 50604, "fall": 0, "type": "http", "port": 0},
+ {"index": 9, "upstream": "donbot.vespa.us-east-2.prod", "name": "10.201.14.46:4080", "status": "down", "rise": 50834, "fall": 0, "type": "http", "port": 0},
+ {"index": 10, "upstream": "appearing-in-routing.not.us-east-2.prod", "name": "10.201.14.50:4080", "status": "down", "rise": 50834, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-all-down.json b/routing-generator/src/test/resources/nginx-health-output-all-down.json
new file mode 100644
index 00000000000..634bcf33148
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-all-down.json
@@ -0,0 +1,11 @@
+
+{"servers": {
+ "total": 4,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.115.68:4080", "status": "down", "rise": 1, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-all-up-but-other-down.json b/routing-generator/src/test/resources/nginx-health-output-all-up-but-other-down.json
new file mode 100644
index 00000000000..c8e15bffb25
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-all-up-but-other-down.json
@@ -0,0 +1,15 @@
+
+{"servers": {
+ "total": 2,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 0, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 4, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 5, "upstream": "frog.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-all-up.json b/routing-generator/src/test/resources/nginx-health-output-all-up.json
new file mode 100644
index 00000000000..a7a635d9ae3
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-all-up.json
@@ -0,0 +1,9 @@
+
+{"servers": {
+ "total": 2,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-policy-down.json b/routing-generator/src/test/resources/nginx-health-output-policy-down.json
new file mode 100644
index 00000000000..347042b034a
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-policy-down.json
@@ -0,0 +1,12 @@
+
+{"servers": {
+ "total": 5,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 4, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-policy-up.json b/routing-generator/src/test/resources/nginx-health-output-policy-up.json
new file mode 100644
index 00000000000..7dd015a7667
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-policy-up.json
@@ -0,0 +1,12 @@
+
+{"servers": {
+ "total": 5,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "up", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 4, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "down", "rise": 2, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output-stream.json b/routing-generator/src/test/resources/nginx-health-output-stream.json
new file mode 100644
index 00000000000..9439e3f1ac4
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output-stream.json
@@ -0,0 +1,12 @@
+
+{"servers": {
+ "total": 4,
+ "generation": 1,
+ "http": [],
+ "stream": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.115.68:4080", "status": "down", "rise": 1, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-health-output.json b/routing-generator/src/test/resources/nginx-health-output.json
new file mode 100644
index 00000000000..9c27a906a68
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-health-output.json
@@ -0,0 +1,11 @@
+
+{"servers": {
+ "total": 4,
+ "generation": 1,
+ "server": [
+ {"index": 0, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 1, "upstream": "gateway.prod.music.vespa.us-east-2.prod", "name": "10.78.115.68:4080", "status": "up", "rise": 2, "fall": 0, "type": "http", "port": 0},
+ {"index": 2, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.114.166:4080", "status": "down", "rise": 0, "fall": 1, "type": "http", "port": 0},
+ {"index": 3, "upstream": "gateway.prod.music.vespa.us-east-2.prod-feed", "name": "10.78.115.68:4080", "status": "down", "rise": 1, "fall": 0, "type": "http", "port": 0}
+ ]
+}}
diff --git a/routing-generator/src/test/resources/nginx-updated.conf b/routing-generator/src/test/resources/nginx-updated.conf
new file mode 100644
index 00000000000..5c90226cdb2
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx-updated.conf
@@ -0,0 +1,56 @@
+map $ssl_preread_server_name $name {
+ beta.music.vespa.us-north-1.vespa.oath.cloud beta.music.vespa.us-north-1.prod;
+ endpoint1 i1.a1.t1.us-north-1.prod;
+ music.vespa.global.vespa.oath.cloud music.vespa.us-north-1.prod;
+ music.vespa.us-north-1.vespa.oath.cloud music.vespa.us-north-1.prod;
+ rotation-02.vespa.global.routing music.vespa.us-north-1.prod;
+ use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa;
+ '' default;
+}
+
+upstream application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa {
+ server host1-default:4443 backup;
+ server host2-default:4443 backup;
+ server host3-beta:4443 weight=1;
+ server host4-beta:4443 weight=1;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa\r\n\r\n";
+ random two;
+}
+
+upstream beta.music.vespa.us-north-1.prod {
+ server host3-beta:4443;
+ server host4-beta:4443;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: beta.music.vespa.us-north-1.prod\r\n\r\n";
+ random two;
+}
+
+upstream i1.a1.t1.us-north-1.prod {
+ server host42:4443;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: i1.a1.t1.us-north-1.prod\r\n\r\n";
+ random two;
+}
+
+upstream music.vespa.us-north-1.prod {
+ server host1-default:4443;
+ server host2-default:4443;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: music.vespa.us-north-1.prod\r\n\r\n";
+ random two;
+}
+
+upstream default {
+ server localhost:4445;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4080;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: localhost\r\n\r\n";
+}
+
+server {
+ listen 443 reuseport;
+ listen [::]:443 reuseport;
+ proxy_pass $name;
+ ssl_preread on;
+ proxy_protocol on;
+}
diff --git a/routing-generator/src/test/resources/nginx.conf b/routing-generator/src/test/resources/nginx.conf
new file mode 100644
index 00000000000..3064bde480a
--- /dev/null
+++ b/routing-generator/src/test/resources/nginx.conf
@@ -0,0 +1,48 @@
+map $ssl_preread_server_name $name {
+ beta.music.vespa.us-north-1.vespa.oath.cloud beta.music.vespa.us-north-1.prod;
+ music.vespa.global.vespa.oath.cloud music.vespa.us-north-1.prod;
+ music.vespa.us-north-1.vespa.oath.cloud music.vespa.us-north-1.prod;
+ rotation-02.vespa.global.routing music.vespa.us-north-1.prod;
+ use-weighted.music.vespa.us-north-1-r.vespa.oath.cloud application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa;
+ '' default;
+}
+
+upstream application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa {
+ server host1-default:4443 backup;
+ server host2-default:4443 backup;
+ server host3-beta:4443 weight=1;
+ server host4-beta:4443 weight=1;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: application-b53398a37399e67cf8c12017e0db764d145f9660.music.vespa\r\n\r\n";
+ random two;
+}
+
+upstream beta.music.vespa.us-north-1.prod {
+ server host3-beta:4443;
+ server host4-beta:4443;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: beta.music.vespa.us-north-1.prod\r\n\r\n";
+ random two;
+}
+
+upstream music.vespa.us-north-1.prod {
+ server host1-default:4443;
+ server host2-default:4443;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4082;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: music.vespa.us-north-1.prod\r\n\r\n";
+ random two;
+}
+
+upstream default {
+ server localhost:4445;
+ check interval=2000 fall=5 rise=2 timeout=3000 default_down=true type=http port=4080;
+ check_http_send "GET /status.html HTTP/1.0\r\nHost: localhost\r\n\r\n";
+}
+
+server {
+ listen 443 reuseport;
+ listen [::]:443 reuseport;
+ proxy_pass $name;
+ ssl_preread on;
+ proxy_protocol on;
+}