-
Notifications
You must be signed in to change notification settings - Fork 79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose styx provider object database in YAML via admin interface #637
Changes from 5 commits
dcd373e
dfe03a3
88a031e
52df4f7
f5056c4
dd8af80
9435f0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,12 @@ | |
*/ | ||
package com.hotels.styx.admin.handlers; | ||
|
||
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.google.common.collect.ImmutableList; | ||
import com.hotels.styx.StyxObjectRecord; | ||
import com.hotels.styx.api.Eventual; | ||
import com.hotels.styx.api.HttpInterceptor; | ||
|
@@ -24,11 +30,24 @@ | |
import com.hotels.styx.api.configuration.ObjectStore; | ||
import com.hotels.styx.api.extension.service.spi.StyxService; | ||
import com.hotels.styx.common.http.handler.HttpStreamer; | ||
import com.hotels.styx.routing.config.StyxObjectDefinition; | ||
import com.hotels.styx.routing.db.StyxObjectStore; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import reactor.core.publisher.Flux; | ||
|
||
import java.util.List; | ||
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,90 @@ private void refreshRoutes(ObjectStore<? extends StyxObjectRecord<? extends Styx | |
|
||
private UrlPatternRouter buildRouter(ObjectStore<? extends StyxObjectRecord<? extends StyxService>> db) { | ||
UrlPatternRouter.Builder routeBuilder = new UrlPatternRouter.Builder(pathPrefix) | ||
.get("", new ProviderListHandler(db)); | ||
.get("", new ProviderListHandler(db)) | ||
.get("objects", (request, context) -> { | ||
return handleRequestForAllObjects(request, context, db); | ||
}) | ||
.get("objects/:objectName", (request, context) -> { | ||
String name = placeholders(context).get("objectName"); | ||
return handleRequestForOneObject(request, context, 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<HttpResponse> handleRequestForAllObjects(HttpRequest request, HttpInterceptor.Context context, | ||
ObjectStore<? extends StyxObjectRecord<? extends StyxService>> db) { | ||
List<StyxObjectDefinition> objects = db.entrySet() | ||
.stream() | ||
.map(entry -> createObjectDef(entry.getKey(), entry.getValue())) | ||
.collect(Collectors.toList()); | ||
String output = serialise(objects); | ||
|
||
return Eventual.of(response(OK) | ||
.body(output, UTF_8) | ||
.build()); | ||
} | ||
|
||
private Eventual<HttpResponse> handleRequestForOneObject(HttpRequest request, HttpInterceptor.Context context, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not exactly sure why I left them in. |
||
ObjectStore<? extends StyxObjectRecord<? extends StyxService>> db, | ||
String name) { | ||
try { | ||
String object = db.get(name) | ||
.map(record -> createObjectDef(name, record)) | ||
.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()); | ||
} | ||
} | ||
|
||
private static StyxObjectDefinition createObjectDef(String name, StyxObjectRecord<? extends StyxService> record) { | ||
return new StyxObjectDefinition(name, record.getType(), ImmutableList.copyOf(record.getTags()), 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(StyxObjectDefinition.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("name") | ||
public abstract String name(); | ||
|
||
@JsonProperty("type") | ||
public abstract String type(); | ||
|
||
@JsonProperty("tags") | ||
public abstract List<String> tags(); | ||
|
||
@JsonProperty("config") | ||
public abstract JsonNode config(); | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
@@ -19,7 +19,6 @@ | |
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 +33,10 @@ | |
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.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 +48,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 +64,11 @@ public class RoutingObjectHandler implements WebServiceHandler { | |
public RoutingObjectHandler(StyxObjectStore<RoutingObjectRecord> routeDatabase, RoutingObjectFactory.Context routingObjectFactoryContext) { | ||
urlRouter = new UrlPatternRouter.Builder() | ||
.get("/admin/routing/objects", (request, context) -> { | ||
String output = routeDatabase.entrySet() | ||
List<StyxObjectDefinition> objects = routeDatabase.entrySet() | ||
.stream() | ||
.map(entry -> serialise(entry.getKey(), entry.getValue())) | ||
.collect(joining("\n")); | ||
.map(entry -> createObjectDef(entry.getKey(), entry.getValue())) | ||
.collect(Collectors.toList()); | ||
String output = serialise(objects); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opinionated: I would inline There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly below, too. |
||
|
||
return Eventual.of(response(OK) | ||
.body(output, UTF_8) | ||
|
@@ -82,7 +79,8 @@ public RoutingObjectHandler(StyxObjectStore<RoutingObjectRecord> routeDatabase, | |
|
||
try { | ||
String object = routeDatabase.get(name) | ||
.map(record -> serialise(name, record)) | ||
.map(record -> createObjectDef(name, record)) | ||
.map(RoutingObjectHandler::serialise) | ||
.orElseThrow(ResourceNotFoundException::new); | ||
|
||
return Eventual.of(response(OK).body(object, UTF_8).build()); | ||
|
@@ -117,15 +115,21 @@ public RoutingObjectHandler(StyxObjectStore<RoutingObjectRecord> 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())); | ||
private static StyxObjectDefinition createObjectDef(String name, RoutingObjectRecord record) { | ||
return 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(StyxObjectDefinition.class, RoutingObjectDefMixin.class) | ||
.valueToTree(object); | ||
|
||
try { | ||
return YAML_MAPPER.writeValueAsString(node); | ||
return YAML_MAPPER.writeValueAsString(json); | ||
} catch (JsonProcessingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
@@ -148,6 +152,9 @@ private abstract static class RoutingObjectDefMixin { | |
|
||
@JsonProperty("tags") | ||
public abstract List<String> tags(); | ||
|
||
@JsonProperty("config") | ||
public abstract JsonNode config(); | ||
} | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -119,6 +119,45 @@ 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 """ | ||
- name: "mySecondMonitor" | ||
type: "HealthCheckMonitor" | ||
tags: [] | ||
config: | ||
objects: "bbb" | ||
path: "/healthCheck/y" | ||
timeoutMillis: 250 | ||
intervalMillis: 500 | ||
healthyThreshold: 3 | ||
unhealthyThreshold: 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add here another object. So that the list would render at least two or more objects. |
||
""".trimIndent() | ||
|
||
body shouldContain """ - name: "myMonitor" """.trim() | ||
body shouldContain """ - name: "originsFileLoader" """.trim() | ||
} | ||
|
||
scenario("YAML configuration for a single provider is available") { | ||
val body = styxServer.adminRequest("/admin/providers/objects/myMonitor") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A small nag: IMHO the url should be Do you think we could change it as a part of this PR, or would this warrant a separate PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a number of related URLs that could be changed like this - raised a new issue #641 to cover this. |
||
.bodyAs(UTF_8) | ||
body shouldNotContain """ name: "mySecondMonitor" """.trim() | ||
body shouldNotContain """ name: "originsFileLoader" """.trim() | ||
body shouldContain """ | ||
name: "myMonitor" | ||
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") { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
@@ -120,20 +120,18 @@ class RoutingRestApiSpec : StringSpec() { | |
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" | ||
- name: "responder" | ||
type: "StaticResponseHandler" | ||
tags: [] | ||
config: | ||
status: 200 | ||
content: "Responder" | ||
- name: "root" | ||
type: "StaticResponseHandler" | ||
tags: [] | ||
config: | ||
status: 200 | ||
content: "Root" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... 🤔 So conceptually this is a map. Also in styx yaml configuration this is represented as a map. But now this implementation renders it as a list. For consistency, I would prefer the admin interface also to render it as a map. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, of course. I should have noticed that the config yaml uses maps, not lists here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the case where we are requesting a single object, should it be as now (with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh, forget that, it should be the single object payload with no |
||
""".trimIndent().trim() | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An opinionated comment, but I would inline the
objects
because it is only ever passed on toserialise
method. ;-)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. In this case I find it flows better as it is, because the stream construction is (in my mind) quite different to the method call, and inlining just results in my eye bouncing around a little more than I'd like.
Ideally I'd have liked to be able to follow the .collect() with .serialise() (or some construction with ::serialise) but that's not available. Visually, I think this is the next best thing. But I'll change it (and the others) if you feel moderately strongly about it.