diff --git a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/ProviderRoutingHandler.java b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/ProviderRoutingHandler.java index 73b82b9fd8..79105f3d11 100644 --- a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/ProviderRoutingHandler.java +++ b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/ProviderRoutingHandler.java @@ -15,6 +15,12 @@ */ package com.hotels.styx.admin.handlers; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.hotels.styx.StyxObjectRecord; import com.hotels.styx.api.Eventual; import com.hotels.styx.api.HttpInterceptor; @@ -29,6 +35,19 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.fasterxml.jackson.core.JsonParser.Feature.AUTO_CLOSE_SOURCE; +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.hotels.styx.admin.handlers.UrlPatternRouter.placeholders; +import static com.hotels.styx.api.HttpResponse.response; +import static com.hotels.styx.api.HttpResponseStatus.NOT_FOUND; +import static com.hotels.styx.api.HttpResponseStatus.OK; +import static com.hotels.styx.infrastructure.configuration.json.ObjectMappers.addStyxMixins; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * Routes admin requests to the admin endpoints of each {@link com.hotels.styx.api.extension.service.spi.StyxService} * in the Provider {@link ObjectStore}, and to the index page that organizes and lists these endpoints. @@ -38,6 +57,9 @@ public class ProviderRoutingHandler implements WebServiceHandler { private static final Logger LOG = LoggerFactory.getLogger(ProviderRoutingHandler.class); private static final int MEGABYTE = 1024 * 1024; + private static final ObjectMapper YAML_MAPPER = addStyxMixins(new ObjectMapper(new YAMLFactory())) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(AUTO_CLOSE_SOURCE, true); private final String pathPrefix; private volatile UrlPatternRouter router; @@ -67,16 +89,80 @@ private void refreshRoutes(ObjectStore> db) { UrlPatternRouter.Builder routeBuilder = new UrlPatternRouter.Builder(pathPrefix) - .get("", new ProviderListHandler(db)); + .get("", new ProviderListHandler(db)) + .get("objects", (request, context) -> handleRequestForAllObjects(db)) + .get("objects/:objectName", (request, context) -> { + String name = placeholders(context).get("objectName"); + return handleRequestForOneObject(db, name); + }); db.entrySet().forEach(entry -> { - String providerName = entry.getKey(); - entry.getValue().getStyxService().adminInterfaceHandlers(pathPrefix + "/" + providerName) - .forEach((relPath, handler) -> - routeBuilder.get(providerName + "/" + relPath, new HttpStreamer(MEGABYTE, handler)) - ); + String providerName = entry.getKey(); + entry.getValue().getStyxService().adminInterfaceHandlers(pathPrefix + "/" + providerName) + .forEach((relPath, handler) -> + routeBuilder.get(providerName + "/" + relPath, new HttpStreamer(MEGABYTE, handler)) + ); }); return routeBuilder.build(); } + + private Eventual handleRequestForAllObjects(ObjectStore> db) { + Map objects = db.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + String output = serialise(objects); + + return Eventual.of(response(OK) + .body(output, UTF_8) + .build()); + } + + private Eventual handleRequestForOneObject(ObjectStore> db, + String name) { + try { + String object = db.get(name) + .map(ProviderRoutingHandler::serialise) + .orElseThrow(ProviderRoutingHandler.ResourceNotFoundException::new); + + return Eventual.of(response(OK).body(object, UTF_8).build()); + } catch (ProviderRoutingHandler.ResourceNotFoundException e) { + return Eventual.of(response(NOT_FOUND).build()); + } + } + + /** + * Serializes either a single {@link com.hotels.styx.routing.config.StyxObjectDefinition} or + * a collection of them. + */ + private static String serialise(Object object) { + JsonNode json = YAML_MAPPER + .addMixIn(StyxObjectRecord.class, ProviderObjectDefMixin.class) + .valueToTree(object); + + try { + return YAML_MAPPER.writeValueAsString(json); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private static class ResourceNotFoundException extends RuntimeException { + } + + private abstract static class ProviderObjectDefMixin { + + @JsonProperty("type") + public abstract String type(); + + @JsonProperty("tags") + public abstract List tags(); + + @JsonProperty("config") + public abstract JsonNode config(); + + @JsonIgnore + public abstract Object getStyxService(); + } + + } diff --git a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/RoutingObjectHandler.java b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/RoutingObjectHandler.java index f9ec2e0187..8c074254dd 100644 --- a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/RoutingObjectHandler.java +++ b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/RoutingObjectHandler.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,11 +15,11 @@ */ package com.hotels.styx.admin.handlers; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -34,10 +34,11 @@ import com.hotels.styx.routing.config.RoutingObjectFactory; import com.hotels.styx.routing.config.StyxObjectDefinition; import com.hotels.styx.routing.db.StyxObjectStore; -import org.slf4j.Logger; import java.io.IOException; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static com.fasterxml.jackson.core.JsonParser.Feature.AUTO_CLOSE_SOURCE; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; @@ -49,14 +50,11 @@ import static com.hotels.styx.api.HttpResponseStatus.OK; import static com.hotels.styx.infrastructure.configuration.json.ObjectMappers.addStyxMixins; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.stream.Collectors.joining; -import static org.slf4j.LoggerFactory.getLogger; /** * Provides admin interface access to Styx routing configuration. */ public class RoutingObjectHandler implements WebServiceHandler { - private static final Logger LOGGER = getLogger(RoutingObjectHandler.class); private static final ObjectMapper YAML_MAPPER = addStyxMixins(new ObjectMapper(new YAMLFactory())) .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) @@ -68,10 +66,9 @@ public class RoutingObjectHandler implements WebServiceHandler { public RoutingObjectHandler(StyxObjectStore routeDatabase, RoutingObjectFactory.Context routingObjectFactoryContext) { urlRouter = new UrlPatternRouter.Builder() .get("/admin/routing/objects", (request, context) -> { - String output = routeDatabase.entrySet() - .stream() - .map(entry -> serialise(entry.getKey(), entry.getValue())) - .collect(joining("\n")); + Map objects = routeDatabase.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + String output = serialise(objects); return Eventual.of(response(OK) .body(output, UTF_8) @@ -82,7 +79,7 @@ public RoutingObjectHandler(StyxObjectStore routeDatabase, try { String object = routeDatabase.get(name) - .map(record -> serialise(name, record)) + .map(RoutingObjectHandler::serialise) .orElseThrow(ResourceNotFoundException::new); return Eventual.of(response(OK).body(object, UTF_8).build()); @@ -117,15 +114,17 @@ public RoutingObjectHandler(StyxObjectStore routeDatabase, .build(); } - private static String serialise(String name, RoutingObjectRecord record) { - JsonNode node = YAML_MAPPER - .addMixIn(StyxObjectDefinition.class, RoutingObjectDefMixin.class) - .valueToTree(new StyxObjectDefinition(name, record.getType(), ImmutableList.copyOf(record.getTags()), record.getConfig())); - - ((ObjectNode) node).set("config", record.getConfig()); + /** + * Serializes either a single {@link com.hotels.styx.routing.config.StyxObjectDefinition} or + * a collection of them. + */ + private static String serialise(Object object) { + JsonNode json = YAML_MAPPER + .addMixIn(RoutingObjectRecord.class, RoutingObjectDefMixin.class) + .valueToTree(object); try { - return YAML_MAPPER.writeValueAsString(node); + return YAML_MAPPER.writeValueAsString(json); } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -140,14 +139,17 @@ private static class ResourceNotFoundException extends RuntimeException { } private abstract static class RoutingObjectDefMixin { - @JsonProperty("name") - public abstract String name(); - @JsonProperty("type") public abstract String type(); @JsonProperty("tags") public abstract List tags(); + + @JsonProperty("config") + public abstract JsonNode config(); + + @JsonIgnore + public abstract Object getRoutingObject(); } } diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/admin/handlers/RoutingObjectHandlerTest.kt b/components/proxy/src/test/kotlin/com/hotels/styx/admin/handlers/RoutingObjectHandlerTest.kt index e2066d7896..b710ab85e5 100644 --- a/components/proxy/src/test/kotlin/com/hotels/styx/admin/handlers/RoutingObjectHandlerTest.kt +++ b/components/proxy/src/test/kotlin/com/hotels/styx/admin/handlers/RoutingObjectHandlerTest.kt @@ -28,6 +28,7 @@ import com.hotels.styx.routing.RoutingObjectRecord import com.hotels.styx.routing.db.StyxObjectStore import com.hotels.styx.handle import com.hotels.styx.mockObject +import io.kotlintest.matchers.string.shouldContain import io.kotlintest.matchers.types.shouldBeTypeOf import io.kotlintest.shouldBe import io.kotlintest.specs.FeatureSpec @@ -85,7 +86,6 @@ class RoutingObjectHandlerTest : FeatureSpec({ it!!.status() shouldBe OK it.bodyAs(UTF_8).trim() shouldBe """ --- - name: "staticResponse" type: "StaticResponseHandler" tags: [] config: @@ -133,25 +133,25 @@ class RoutingObjectHandlerTest : FeatureSpec({ .block() .let { it!!.status() shouldBe OK - it.bodyAs(UTF_8).trim() shouldBe """ - --- - name: "conditionRouter" - type: "ConditionRouter" - tags: [] - config: - routes: - - condition: "path() == \"/bar\"" - destination: "b" - fallback: "fb" - - --- - name: "staticResponse" - type: "StaticResponseHandler" - tags: [] - config: - status: 200 - content: "Hello, world!" - """.trimIndent().trim() + it.bodyAs(UTF_8) shouldContain """ + conditionRouter: + type: "ConditionRouter" + tags: [] + config: + routes: + - condition: "path() == \"/bar\"" + destination: "b" + fallback: "fb" + """.trimIndent() + + it.bodyAs(UTF_8) shouldContain """ + staticResponse: + type: "StaticResponseHandler" + tags: [] + config: + status: 200 + content: "Hello, world!" + """.trimIndent() } } diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt index ca5690c18a..783f295bb8 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt @@ -119,6 +119,64 @@ class ProviderAdminInterfaceSpec : FeatureSpec() { body shouldContain "/admin/providers/myMonitor/status" body shouldContain "/admin/providers/mySecondMonitor/status" } + + scenario("YAML configuration for all providers is available") { + val body = styxServer.adminRequest("/admin/providers/objects") + .bodyAs(UTF_8) + body shouldContain """ + mySecondMonitor: + type: "HealthCheckMonitor" + tags: [] + config: + objects: "bbb" + path: "/healthCheck/y" + timeoutMillis: 250 + intervalMillis: 500 + healthyThreshold: 3 + unhealthyThreshold: 2 + """.trimIndent() + + body shouldContain """ + myMonitor: + type: "HealthCheckMonitor" + tags: [] + config: + objects: "aaa" + path: "/healthCheck/x" + timeoutMillis: 250 + intervalMillis: 500 + healthyThreshold: 3 + unhealthyThreshold: 2 + """.trimIndent() + + body shouldContain """ + originsFileLoader: + type: "YamlFileConfigurationService" + tags: [] + config: + originsFile: "${originsFile.absolutePath}" + ingressObject: "pathPrefixRouter" + monitor: true + pollInterval: "PT0.1S" + """.trimIndent() + } + + scenario("YAML configuration for a single provider is available") { + val body = styxServer.adminRequest("/admin/providers/objects/myMonitor") + .bodyAs(UTF_8) + body.trim() shouldBe """ + --- + type: "HealthCheckMonitor" + tags: [] + config: + objects: "aaa" + path: "/healthCheck/x" + timeoutMillis: 250 + intervalMillis: 500 + healthyThreshold: 3 + unhealthyThreshold: 2 + """.trimIndent() + } } feature("Endpoints for dynamically added Styx services are available in the Admin interface") { diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/RoutingRestApiSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/RoutingRestApiSpec.kt index 350be177f1..6b10d7787a 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/RoutingRestApiSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/RoutingRestApiSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,18 +16,15 @@ package com.hotels.styx.routing import com.hotels.styx.api.HttpHeaderNames.HOST -import com.hotels.styx.api.HttpRequest.delete -import com.hotels.styx.api.HttpRequest.get -import com.hotels.styx.api.HttpRequest.put -import com.hotels.styx.api.HttpResponseStatus.CREATED -import com.hotels.styx.api.HttpResponseStatus.NOT_FOUND -import com.hotels.styx.api.HttpResponseStatus.OK +import com.hotels.styx.api.HttpRequest.* +import com.hotels.styx.api.HttpResponseStatus.* import com.hotels.styx.client.StyxHttpClient import com.hotels.styx.support.StyxServerProvider import com.hotels.styx.support.adminHostHeader import com.hotels.styx.support.proxyHttpHostHeader import io.kotlintest.Spec import io.kotlintest.TestCase +import io.kotlintest.matchers.string.shouldContain import io.kotlintest.shouldBe import io.kotlintest.specs.StringSpec import reactor.core.publisher.toMono @@ -118,23 +115,22 @@ class RoutingRestApiSpec : StringSpec() { .block() .let { it!!.status() shouldBe OK - it.bodyAs(UTF_8).trim() shouldBe """ - --- - name: "responder" - type: "StaticResponseHandler" - tags: [] - config: - status: 200 - content: "Responder" - - --- - name: "root" - type: "StaticResponseHandler" - tags: [] - config: - status: 200 - content: "Root" - """.trimIndent().trim() + it.bodyAs(UTF_8) shouldContain """ + responder: + type: "StaticResponseHandler" + tags: [] + config: + status: 200 + content: "Responder" + """.trimIndent() + it.bodyAs(UTF_8) shouldContain """ + root: + type: "StaticResponseHandler" + tags: [] + config: + status: 200 + content: "Root" + """.trimIndent() } } }