diff options
Diffstat (limited to 'routing-generator/src/test')
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; +} |