Skip to content
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

Open a RESTful API for Styx Object Store #406

Merged
merged 8 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ protected void doStop() {
}

private static HttpServer createAdminServer(StyxServerComponents config) {
return new AdminServerBuilder(config.environment())
return new AdminServerBuilder(config)
.backendServicesRegistry((Registry<BackendService>) config.services().get("backendServiceRegistry"))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.hotels.styx.admin.handlers.PingHandler;
import com.hotels.styx.admin.handlers.PluginListHandler;
import com.hotels.styx.admin.handlers.PluginToggleHandler;
import com.hotels.styx.admin.handlers.RoutingObjectHandler;
import com.hotels.styx.admin.handlers.StartupConfigHandler;
import com.hotels.styx.admin.handlers.StyxConfigurationHandler;
import com.hotels.styx.admin.handlers.ThreadsHandler;
Expand All @@ -45,12 +46,16 @@
import com.hotels.styx.common.http.handler.HttpMethodFilteringHandler;
import com.hotels.styx.common.http.handler.StaticBodyHttpHandler;
import com.hotels.styx.proxy.plugin.NamedPlugin;
import com.hotels.styx.routing.RoutingObjectRecord;
import com.hotels.styx.routing.config.RoutingObjectFactory;
import com.hotels.styx.routing.db.StyxObjectStore;
import com.hotels.styx.server.HttpServer;
import com.hotels.styx.server.StandardHttpRouter;
import com.hotels.styx.server.handlers.ClassPathResourceHandler;
import com.hotels.styx.server.netty.NettyServerBuilderSpec;
import com.hotels.styx.server.netty.WebServerConnectorFactory;
import com.hotels.styx.server.track.CurrentRequestTracker;
import com.hotels.styx.startup.StyxServerComponents;
import org.slf4j.Logger;

import java.time.Duration;
Expand Down Expand Up @@ -78,12 +83,16 @@ public class AdminServerBuilder {

private final Environment environment;
private final Configuration configuration;
private final RoutingObjectFactory routingObjectFactory;
private StyxObjectStore<RoutingObjectRecord> routeDatabase;

private Registry<BackendService> backendServicesRegistry;

public AdminServerBuilder(Environment environment) {
this.environment = environment;
this.configuration = environment.configuration();
public AdminServerBuilder(StyxServerComponents serverComponents) {
this.environment = requireNonNull(serverComponents.environment());
this.routeDatabase = requireNonNull(serverComponents.routeDatabase());
this.routingObjectFactory = requireNonNull(serverComponents.routingObjectFactory());
this.configuration = this.environment.configuration();
}

public AdminServerBuilder backendServicesRegistry(Registry<BackendService> backendServicesRegistry) {
Expand Down Expand Up @@ -122,6 +131,10 @@ private StandardHttpRouter adminEndpoints(StyxConfig styxConfig) {
httpRouter.add("/admin/configuration/logging", new LoggingConfigurationHandler(styxConfig.startupConfig().logConfigLocation()));
httpRouter.add("/admin/configuration/startup", new StartupConfigHandler(styxConfig.startupConfig()));

RoutingObjectHandler routingObjectHandler = new RoutingObjectHandler(routeDatabase, routingObjectFactory);
httpRouter.add("/admin/routing", routingObjectHandler);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If /admin/routing and /admin/routing/ are not considered equivalent, we should make a change to the httpRouter instead of duplicating code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is necessary because StandardHttpRouter treats the two endpoints differently. But I'll have another look.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we have this also for the metrics handler, but not for any others (
httpRouter.add("/admin/metrics", metricsHandler);
httpRouter.add("/admin/metrics/", metricsHandler);
)
I think this needs reviewing to identify any fixes we might need. I would say fixes can go in another PR.

httpRouter.add("/admin/routing/", routingObjectHandler);

// Dashboard
httpRouter.add("/admin/dashboard/data.json", dashboardDataHandler(styxConfig));
httpRouter.add("/admin/dashboard/", new ClassPathResourceHandler("/admin/dashboard/"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.HttpHandler;
import com.hotels.styx.api.HttpInterceptor;
import com.hotels.styx.api.HttpRequest;
import com.hotels.styx.api.HttpResponse;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.routing.RoutingObjectRecord;
import com.hotels.styx.routing.config.RoutingObjectDefinition;
import com.hotels.styx.routing.config.RoutingObjectFactory;
import com.hotels.styx.routing.db.StyxObjectStore;
import org.slf4j.Logger;

import java.io.IOException;
import java.util.List;

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.BAD_REQUEST;
import static com.hotels.styx.api.HttpResponseStatus.CREATED;
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;
import static java.util.Collections.emptyList;
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 HttpHandler {
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)
.configure(AUTO_CLOSE_SOURCE, true);

private final UrlPatternRouter urlRouter;


public RoutingObjectHandler(StyxObjectStore<RoutingObjectRecord> routeDatabase, RoutingObjectFactory objectFactory) {
urlRouter = new UrlPatternRouter.Builder()
.get("/admin/routing/objects", httpHandler((request, context) -> {
String output = routeDatabase.entrySet()
.stream()
.map(entry -> serialise(entry.getFirst(), entry.getSecond()))
.collect(joining("\n"));

return Eventual.of(response(OK)
.body(output, UTF_8)
.build());
}))
.get("/admin/routing/objects/:objectName", httpHandler((request, context) -> {
String name = placeholders(context).get("objectName");

try {
String object = routeDatabase.get(name)
.map(record -> serialise(name, record))
.orElseThrow(ResourceNotFoundException::new);
Copy link
Contributor

@dvlato dvlato May 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we logging the name of the resource that couldn't be found anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.


return Eventual.of(response(OK).body(object, UTF_8).build());
} catch (ResourceNotFoundException e) {
return Eventual.of(response(NOT_FOUND).build());
}
}))
.put("/admin/routing/objects/:objectName", httpHandler((request, context) -> {
String body = request.bodyAs(UTF_8);
String name = placeholders(context).get("objectName");

try {
RoutingObjectDefinition payload = YAML_MAPPER.readValue(body, RoutingObjectDefinition.class);
HttpHandler httpHandler = objectFactory.build(emptyList(), payload);

routeDatabase.insert(name, new RoutingObjectRecord(payload.type(), payload.config(), httpHandler));

return Eventual.of(response(CREATED).build());
} catch (IOException | RuntimeException cause) {
return Eventual.of(response(BAD_REQUEST).body(cause.toString(), UTF_8).build());
}
}))
.delete("/admin/routing/objects/:objectName", httpHandler((request, context) -> {
String name = placeholders(context).get("objectName");

if (!routeDatabase.get(name).isPresent()) {
return Eventual.of(response(NOT_FOUND).build());
}

routeDatabase.remove(name);
return Eventual.of(response(OK).build());
}))
.build();
}

private static String serialise(String name, RoutingObjectRecord app) {
JsonNode node = YAML_MAPPER
.addMixIn(RoutingObjectDefinition.class, RoutingObjectDefMixin.class)
.valueToTree(new RoutingObjectDefinition(name, app.getType(), emptyList(), app.getConfig()));

((ObjectNode) node).set("config", app.getConfig());

try {
return YAML_MAPPER.writeValueAsString(node);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

private static HttpHandler httpHandler(FullHttpHandler delegate) {
return (request, context) -> request.aggregate(1000000)
.flatMap(fullRequest -> delegate.handle(fullRequest, context))
.map(HttpResponse::stream);
}

@Override
public Eventual<LiveHttpResponse> handle(LiveHttpRequest request, HttpInterceptor.Context context) {
return urlRouter.handle(request, context);
}

private interface FullHttpHandler {
Eventual<HttpResponse> handle(HttpRequest request, HttpInterceptor.Context context);
}

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<String> tags();
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.hotels.styx.admin.handlers;

import com.google.common.collect.ImmutableList;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.HttpHandler;
import com.hotels.styx.api.HttpInterceptor;
import com.hotels.styx.api.HttpMethod;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.hotels.styx.api.HttpMethod.DELETE;
import static com.hotels.styx.api.HttpMethod.GET;
import static com.hotels.styx.api.HttpMethod.POST;
import static com.hotels.styx.api.HttpMethod.PUT;
import static com.hotels.styx.api.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static com.hotels.styx.api.HttpResponseStatus.NOT_FOUND;
import static com.hotels.styx.api.LiveHttpResponse.response;
import static java.util.stream.Collectors.toMap;
import static org.slf4j.LoggerFactory.getLogger;

/**
* A configurable router.
*/
public class UrlPatternRouter implements HttpHandler {
private static final Logger LOGGER = getLogger(UrlPatternRouter.class);
private static final String PLACEHOLDERS_KEY = "UrlRouter.placeholders";
private final List<RouteDescriptor> alternatives;

private UrlPatternRouter(List<RouteDescriptor> alternatives) {
this.alternatives = ImmutableList.copyOf(alternatives);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally ImmutableList.copyOf has been exempt from static imports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, we try to use static imports when the key information is in the method name, which makes the class name just clutter. In this case, the method name alone is insufficient to demonstrate that we are instantiating an immutable list.

}

public static Map<String, String> placeholders(HttpInterceptor.Context context) {
return context.getIfAvailable(PLACEHOLDERS_KEY, Map.class).get();
}

@Override
public Eventual<LiveHttpResponse> handle(LiveHttpRequest request, HttpInterceptor.Context context) {
for (RouteDescriptor route : alternatives) {
if (request.method().equals(route.method())) {
Matcher match = route.uriPattern().matcher(request.path());

LOGGER.debug("Request path '{}' matching against route pattern '{}' matches: {}", new Object[] {
request.path(), route.uriPattern(), match.matches()});

if (match.matches()) {
Map<String, String> placeholders = route.placeholderNames().stream()
.collect(toMap(name -> name, match::group));

context.add(PLACEHOLDERS_KEY, placeholders);

try {
return route.handler().handle(request, context);
} catch (Exception cause) {
LOGGER.error("ERROR: {} {}", new Object[] {request.method(), request.path(), cause});
return Eventual.of(response(INTERNAL_SERVER_ERROR).build());
}
}
}
}

return Eventual.of(response(NOT_FOUND).build());
}

/**
* A builder class.
*/
public static class Builder {
private final List<RouteDescriptor> alternatives = new LinkedList<>();

public Builder get(String uriPattern, HttpHandler handler) {
alternatives.add(new RouteDescriptor(GET, uriPattern, handler));
return this;
}

public Builder post(String uriPattern, HttpHandler handler) {
alternatives.add(new RouteDescriptor(POST, uriPattern, handler));
return this;
}

public Builder put(String uriPattern, HttpHandler handler) {
alternatives.add(new RouteDescriptor(PUT, uriPattern, handler));
return this;
}

public Builder delete(String uriPattern, HttpHandler handler) {
alternatives.add(new RouteDescriptor(DELETE, uriPattern, handler));
return this;
}

public UrlPatternRouter build() {
return new UrlPatternRouter(alternatives);
}
}

private static class RouteDescriptor {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(":([a-zA-Z0-9-_]+)");

private final HttpMethod method;
private final Pattern uriPattern;
private final HttpHandler handler;
private final List<String> placeholderNames;

public RouteDescriptor(HttpMethod method, String uriPattern, HttpHandler handler) {
this.method = method;
this.handler = handler;
this.placeholderNames = placeholders(uriPattern);
this.uriPattern = compilePattern(uriPattern);
}

public HttpMethod method() {
return method;
}

public Pattern uriPattern() {
return uriPattern;
}

public HttpHandler handler() {
return handler;
}

public List<String> placeholderNames() {
return placeholderNames;
}

private static Pattern compilePattern(String pattern) {
Matcher matcher = PLACEHOLDER_PATTERN.matcher(pattern);

return Pattern.compile(matcher.replaceAll("(?<$1>[a-zA-Z0-9-_]+)"));
}

private static List<String> placeholders(String pattern) {
Matcher matcher = PLACEHOLDER_PATTERN.matcher(pattern);

List<String> outcome = new ArrayList<>();

while (matcher.find()) {
outcome.add(matcher.group(1));
}

return outcome;
}

}
}
Loading