summaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2019-07-12 15:49:04 +0200
committerMartin Polden <mpolden@mpolden.no>2019-07-12 15:49:04 +0200
commitc10ba446e46fe390b052958e3148cbf9bbb74a9b (patch)
tree7d422465c5c46c8e5ca8fedce4b8e1bb4f8cf68e /configserver
parent12b55be208be4199178c7c1387c308fe8d80f947 (diff)
Revert "Decouple flags REST API from config server"
This reverts commit b81b21546cdff92d360cbdf7dda27e6ed7bc7170.
Diffstat (limited to 'configserver')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java47
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java45
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java59
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java31
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java120
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java20
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java34
-rw-r--r--configserver/src/main/resources/configserver-app/services.xml6
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java200
9 files changed, 561 insertions, 1 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java
new file mode 100644
index 00000000000..92397fc84a7
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java
@@ -0,0 +1,47 @@
+// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.flags.FlagDefinition;
+import com.yahoo.vespa.flags.json.DimensionHelper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author hakonhall
+ */
+public class DefinedFlag extends HttpResponse {
+ private static ObjectMapper mapper = new ObjectMapper();
+
+ private final FlagDefinition flagDefinition;
+
+ public DefinedFlag(FlagDefinition flagDefinition) {
+ super(Response.Status.OK);
+ this.flagDefinition = flagDefinition;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ ObjectNode rootNode = mapper.createObjectNode();
+ renderFlagDefinition(flagDefinition, rootNode);
+ mapper.writeValue(outputStream, rootNode);
+ }
+
+ static void renderFlagDefinition(FlagDefinition flagDefinition, ObjectNode definitionNode) {
+ definitionNode.put("description", flagDefinition.getDescription());
+ definitionNode.put("modification-effect", flagDefinition.getModificationEffect());
+ ArrayNode dimensionsNode = definitionNode.putArray("dimensions");
+ flagDefinition.getDimensions().forEach(dimension -> dimensionsNode.add(DimensionHelper.toWire(dimension)));
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java
new file mode 100644
index 00000000000..9604c51ee4b
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java
@@ -0,0 +1,45 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.flags.FlagDefinition;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+public class DefinedFlags extends HttpResponse {
+ private static ObjectMapper mapper = new ObjectMapper();
+ private static final Comparator<FlagDefinition> sortByFlagId =
+ (left, right) -> left.getUnboundFlag().id().compareTo(right.getUnboundFlag().id());
+
+ private final List<FlagDefinition> flags;
+
+ public DefinedFlags(List<FlagDefinition> flags) {
+ super(Response.Status.OK);
+ this.flags = flags;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ ObjectNode rootNode = mapper.createObjectNode();
+ flags.stream().sorted(sortByFlagId).forEach(flagDefinition -> {
+ ObjectNode definitionNode = rootNode.putObject(flagDefinition.getUnboundFlag().id().toString());
+ DefinedFlag.renderFlagDefinition(flagDefinition, definitionNode);
+ });
+ mapper.writeValue(outputStream, rootNode);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java
new file mode 100644
index 00000000000..b33fc7c2b04
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java
@@ -0,0 +1,59 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.json.FlagData;
+import com.yahoo.vespa.flags.json.wire.WireFlagDataList;
+
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+public class FlagDataListResponse extends HttpResponse {
+ private static ObjectMapper mapper = new ObjectMapper();
+
+ private final String flagsV1Uri;
+ private final TreeMap<FlagId, FlagData> flags;
+ private final boolean recursive;
+
+ public FlagDataListResponse(String flagsV1Uri, Map<FlagId, FlagData> flags, boolean recursive) {
+ super(Response.Status.OK);
+ this.flagsV1Uri = flagsV1Uri;
+ this.flags = new TreeMap<>(flags);
+ this.recursive = recursive;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) {
+ if (recursive) {
+ WireFlagDataList list = new WireFlagDataList();
+ flags.values().forEach(flagData -> list.flags.add(flagData.toWire()));
+ list.serializeToOutputStream(outputStream);
+ } else {
+ ObjectNode rootNode = mapper.createObjectNode();
+ ArrayNode flagsArray = rootNode.putArray("flags");
+ flags.forEach((flagId, flagData) -> {
+ ObjectNode object = flagsArray.addObject();
+ object.put("id", flagId.toString());
+ object.put("url", flagsV1Uri + "/data/" + flagId.toString());
+ });
+ uncheck(() -> mapper.writeValue(outputStream, rootNode));
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java
new file mode 100644
index 00000000000..054b218ff2d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java
@@ -0,0 +1,31 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.flags.json.FlagData;
+
+import java.io.OutputStream;
+
+/**
+ * @author hakonhall
+ */
+public class FlagDataResponse extends HttpResponse {
+ private final FlagData data;
+
+ FlagDataResponse(FlagData data) {
+ super(Response.Status.OK);
+ this.data = data;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) {
+ data.serializeToOutputStream(outputStream);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java
new file mode 100644
index 00000000000..00f3d457d3d
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java
@@ -0,0 +1,120 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.restapi.Path;
+import com.yahoo.vespa.config.server.http.HttpErrorResponse;
+import com.yahoo.vespa.config.server.http.HttpHandler;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.configserver.flags.FlagsDb;
+import com.yahoo.vespa.flags.FlagDefinition;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.json.FlagData;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * Handles /flags/v1 requests
+ *
+ * @author hakonhall
+ */
+public class FlagsHandler extends HttpHandler {
+ private final FlagsDb flagsDb;
+
+ @Inject
+ public FlagsHandler(LoggingRequestHandler.Context context, FlagsDb flagsDb) {
+ super(context);
+ this.flagsDb = flagsDb;
+ }
+
+ @Override
+ protected HttpResponse handleGET(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/flags/v1")) return new V1Response(flagsV1Uri(request), "data", "defined");
+ if (path.matches("/flags/v1/data")) return getFlagDataList(request);
+ if (path.matches("/flags/v1/data/{flagId}")) return getFlagData(findFlagId(request, path));
+ if (path.matches("/flags/v1/defined")) return new DefinedFlags(Flags.getAllFlags());
+ if (path.matches("/flags/v1/defined/{flagId}")) return getDefinedFlag(findFlagId(request, path));
+ throw new NotFoundException("Nothing at path '" + path + "'");
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/flags/v1/data/{flagId}")) return putFlagData(request, findFlagId(request, path));
+ throw new NotFoundException("Nothing at path '" + path + "'");
+ }
+
+ @Override
+ protected HttpResponse handleDELETE(HttpRequest request) {
+ Path path = new Path(request.getUri());
+ if (path.matches("/flags/v1/data/{flagId}")) return deleteFlagData(findFlagId(request, path));
+ throw new NotFoundException("Nothing at path '" + path + "'");
+ }
+
+ private String flagsV1Uri(HttpRequest request) {
+ URI uri = request.getUri();
+ String port = uri.getPort() < 0 ? "" : ":" + uri.getPort();
+ return uri.getScheme() + "://" + uri.getHost() + port + "/flags/v1";
+ }
+
+ private HttpResponse getDefinedFlag(FlagId flagId) {
+ FlagDefinition definition = Flags.getFlag(flagId)
+ .orElseThrow(() -> new NotFoundException("Flag " + flagId + " not defined"));
+ return new DefinedFlag(definition);
+ }
+
+ private HttpResponse getFlagDataList(HttpRequest request) {
+ return new FlagDataListResponse(flagsV1Uri(request), flagsDb.getAllFlags(),
+ Objects.equals(request.getProperty("recursive"), "true"));
+ }
+
+ private HttpResponse getFlagData(FlagId flagId) {
+ FlagData data = flagsDb.getValue(flagId).orElseThrow(() -> new NotFoundException("Flag " + flagId + " not set"));
+ return new FlagDataResponse(data);
+ }
+
+ private HttpResponse putFlagData(HttpRequest request, FlagId flagId) {
+ FlagData data;
+ try {
+ data = FlagData.deserialize(request.getData());
+ } catch (UncheckedIOException e) {
+ return HttpErrorResponse.badRequest("Failed to deserialize request data: " + Exceptions.toMessageString(e));
+ }
+
+ if (!isForce(request)) {
+ FlagDefinition definition = Flags.getFlag(flagId).get(); // FlagId has been validated in findFlagId()
+ data.validate(definition.getUnboundFlag().serializer());
+ }
+
+ flagsDb.setValue(flagId, data);
+ return new OKResponse();
+ }
+
+ private HttpResponse deleteFlagData(FlagId flagId) {
+ flagsDb.removeValue(flagId);
+ return new OKResponse();
+ }
+
+ private FlagId findFlagId(HttpRequest request, Path path) {
+ FlagId flagId = new FlagId(path.get("flagId"));
+
+ if (!isForce(request)) {
+ Flags.getFlag(flagId).orElseThrow(() ->
+ new NotFoundException("There is no flag '" + flagId + "' (use ?force=true to override)"));
+ }
+
+ return flagId;
+ }
+
+ private boolean isForce(HttpRequest request) {
+ return Objects.equals(request.getProperty("force"), "true");
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java
new file mode 100644
index 00000000000..87c02ae56f1
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java
@@ -0,0 +1,20 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.yahoo.container.jdisc.EmptyResponse;
+import com.yahoo.jdisc.Response;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+
+/**
+ * @author hakonhall
+ */
+public class OKResponse extends EmptyResponse {
+ public OKResponse() {
+ super(Response.Status.OK);
+ }
+
+ @Override
+ public String getContentType() {
+ return HttpConfigResponse.JSON_CONTENT_TYPE;
+ }
+}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java
new file mode 100644
index 00000000000..3594c801ca8
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java
@@ -0,0 +1,34 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.SlimeUtils;
+import com.yahoo.vespa.config.server.http.HttpConfigResponse;
+import com.yahoo.vespa.config.server.http.StaticResponse;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+public class V1Response extends StaticResponse {
+ public V1Response(String flagsV1Uri, String... names) {
+ super(Response.Status.OK, HttpConfigResponse.JSON_CONTENT_TYPE, generateBody(flagsV1Uri, Arrays.asList(names)));
+ }
+
+ private static String generateBody(String flagsV1Uri, List<String> names) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ names.forEach(name -> {
+ Cursor data = root.setObject(name);
+ data.setString("url", flagsV1Uri + "/" + name);
+ });
+ return Utf8.toString(uncheck(() -> SlimeUtils.toJsonBytes(slime)));
+ }
+}
diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml
index b429e220c33..cfacd6ff8c9 100644
--- a/configserver/src/main/resources/configserver-app/services.xml
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -54,7 +54,7 @@
<preprocess:include file='model-integration.xml' required='true' />
<component id="com.yahoo.vespa.configserver.flags.ConfigServerFlagSource" bundle="configserver-flags"/>
- <component id="com.yahoo.vespa.flags.persistence.FlagsDb" bundle="flags"/>
+ <component id="com.yahoo.vespa.configserver.flags.db.FlagsDbImpl" bundle="configserver-flags"/>
<preprocess:include file='metrics-packets.xml' required='false' />
<preprocess:include file='container.xml' required='false' />
@@ -92,6 +92,10 @@
<handler id='com.yahoo.vespa.config.server.http.status.StatusHandler' bundle='configserver'>
<binding>http://*/status</binding>
</handler>
+ <handler id='com.yahoo.vespa.config.server.http.flags.FlagsHandler' bundle='configserver'>
+ <binding>http://*/flags/v1</binding>
+ <binding>http://*/flags/v1/*</binding>
+ </handler>
<handler id='com.yahoo.vespa.config.server.http.v2.TenantHandler' bundle='configserver'>
<binding>http://*/application/v2/tenant/</binding>
<binding>http://*/application/v2/tenant/*</binding>
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java
new file mode 100644
index 00000000000..5ae6ce9820b
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java
@@ -0,0 +1,200 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server.http.flags;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.configserver.flags.db.FlagsDbImpl;
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.UnboundBooleanFlag;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsHandlerTest {
+ private static final UnboundBooleanFlag FLAG1 = Flags.defineFeatureFlag(
+ "id1", false, "desc1", "mod1");
+ private static final UnboundBooleanFlag FLAG2 = Flags.defineFeatureFlag(
+ "id2", true, "desc2", "mod2",
+ FetchVector.Dimension.HOSTNAME, FetchVector.Dimension.APPLICATION_ID);
+
+ private static final String FLAGS_V1_URL = "https://foo.com:4443/flags/v1";
+
+ private final FlagsDbImpl flagsDb = new FlagsDbImpl(new MockCurator());
+ private final FlagsHandler handler = new FlagsHandler(FlagsHandler.testOnlyContext(), flagsDb);
+
+ @Test
+ public void testV1() {
+ String expectedResponse = "{" +
+ Stream.of("data", "defined")
+ .map(name -> "\"" + name + "\":{\"url\":\"https://foo.com:4443/flags/v1/" + name + "\"}")
+ .collect(Collectors.joining(",")) +
+ "}";
+ verifySuccessfulRequest(Method.GET, "", "", expectedResponse);
+ verifySuccessfulRequest(Method.GET, "/", "", expectedResponse);
+ }
+
+ @Test
+ public void testDefined() {
+ try (Flags.Replacer replacer = Flags.clearFlagsForTesting()) {
+ fixUnusedWarning(replacer);
+ Flags.defineFeatureFlag("id", false, "desc", "mod", FetchVector.Dimension.HOSTNAME);
+ verifySuccessfulRequest(Method.GET, "/defined", "",
+ "{\"id\":{\"description\":\"desc\",\"modification-effect\":\"mod\",\"dimensions\":[\"hostname\"]}}");
+
+ verifySuccessfulRequest(Method.GET, "/defined/id", "",
+ "{\"description\":\"desc\",\"modification-effect\":\"mod\",\"dimensions\":[\"hostname\"]}");
+ }
+ }
+
+ private void fixUnusedWarning(Flags.Replacer replacer) { }
+
+ @Test
+ public void testData() {
+ // PUT flag with ID id1
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG1.id(),
+ "{\n" +
+ " \"id\": \"id1\",\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ]\n" +
+ "}",
+ "");
+
+ // GET on ID id1 should return the same as the put.
+ verifySuccessfulRequest(Method.GET, "/data/" + FLAG1.id(),
+ "", "{\"id\":\"id1\",\"rules\":[{\"value\":true}]}");
+
+ // List all flags should list only id1
+ verifySuccessfulRequest(Method.GET, "/data",
+ "", "{\"flags\":[{\"id\":\"id1\",\"url\":\"https://foo.com:4443/flags/v1/data/id1\"}]}");
+
+ // Should be identical to above: suffix / on path should be ignored
+ verifySuccessfulRequest(Method.GET, "/data/",
+ "", "{\"flags\":[{\"id\":\"id1\",\"url\":\"https://foo.com:4443/flags/v1/data/id1\"}]}");
+
+ // Verify absent port => absent in response
+ assertThat(handleWithPort(Method.GET, -1, "/data", "", 200),
+ is("{\"flags\":[{\"id\":\"id1\",\"url\":\"https://foo.com/flags/v1/data/id1\"}]}"));
+
+ // PUT id2
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG2.id(),
+ "{\n" +
+ " \"id\": \"id2\",\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"host1\", \"host2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"app1\", \"app2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"zone1\"\n" +
+ " }\n" +
+ "}\n",
+ "");
+
+ // GET on id2 should now return what was put
+ verifySuccessfulRequest(Method.GET, "/data/" + FLAG2.id(), "",
+ "{\"id\":\"id2\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"hostname\",\"values\":[\"host1\",\"host2\"]},{\"type\":\"blacklist\",\"dimension\":\"application\",\"values\":[\"app1\",\"app2\"]}],\"value\":true}],\"attributes\":{\"zone\":\"zone1\"}}");
+
+ // The list of flag data should return id1 and id2
+ verifySuccessfulRequest(Method.GET, "/data",
+ "",
+ "{\"flags\":[{\"id\":\"id1\",\"url\":\"https://foo.com:4443/flags/v1/data/id1\"},{\"id\":\"id2\",\"url\":\"https://foo.com:4443/flags/v1/data/id2\"}]}");
+
+ // Putting (overriding) id1 should work silently
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG1.id(),
+ "{\n" +
+ " \"id\": \"id1\",\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"value\": false\n" +
+ " }\n" +
+ " ]\n" +
+ "}\n",
+ "");
+
+ // Verify PUT
+ verifySuccessfulRequest(Method.GET, "/data/" + FLAG1.id(), "", "{\"id\":\"id1\",\"rules\":[{\"value\":false}]}");
+
+ // Get all recursivelly displays all flag data
+ verifySuccessfulRequest(Method.GET, "/data?recursive=true", "",
+ "{\"flags\":[{\"id\":\"id1\",\"rules\":[{\"value\":false}]},{\"id\":\"id2\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"hostname\",\"values\":[\"host1\",\"host2\"]},{\"type\":\"blacklist\",\"dimension\":\"application\",\"values\":[\"app1\",\"app2\"]}],\"value\":true}],\"attributes\":{\"zone\":\"zone1\"}}]}");
+
+ // Deleting both flags
+ verifySuccessfulRequest(Method.DELETE, "/data/" + FLAG1.id(), "", "");
+ verifySuccessfulRequest(Method.DELETE, "/data/" + FLAG2.id(), "", "");
+
+ // And the list of data flags should now be empty
+ verifySuccessfulRequest(Method.GET, "/data", "", "{\"flags\":[]}");
+ }
+
+ @Test
+ public void testForcing() {
+ assertThat(handle(Method.PUT, "/data/" + new FlagId("undef"), "", 404),
+ containsString("There is no flag 'undef'"));
+
+ assertThat(handle(Method.PUT, "/data/" + new FlagId("undef") + "?force=true", "", 400),
+ containsString("No content to map due to end-of-input"));
+
+ assertThat(handle(Method.PUT, "/data/" + FLAG1.id(), "{}", 400),
+ containsString("Flag ID missing"));
+
+ assertThat(handle(Method.PUT, "/data/" + FLAG1.id(), "{\"id\": \"id1\",\"rules\": [{\"value\":\"string\"}]}", 400),
+ containsString("Wrong type of JsonNode: STRING"));
+
+ assertThat(handle(Method.PUT, "/data/" + FLAG1.id() + "?force=true", "{\"id\": \"id1\",\"rules\": [{\"value\":\"string\"}]}", 200),
+ is(""));
+ }
+
+ private void verifySuccessfulRequest(Method method, String pathSuffix, String requestBody, String expectedResponseBody) {
+ assertThat(handle(method, pathSuffix, requestBody, 200), is(expectedResponseBody));
+ }
+
+ private String handle(Method method, String pathSuffix, String requestBody, int expectedStatus) {
+ return handleWithPort(method, 4443, pathSuffix, requestBody, expectedStatus);
+ }
+
+ private String handleWithPort(Method method, int port, String pathSuffix, String requestBody, int expectedStatus) {
+ String uri = "https://foo.com" + (port < 0 ? "" : ":" + port) + "/flags/v1" + pathSuffix;
+ HttpRequest request = HttpRequest.createTestRequest(uri, method, makeInputStream(requestBody));
+ HttpResponse response = handler.handle(request);
+ assertEquals(expectedStatus, response.getStatus());
+ assertEquals("application/json", response.getContentType());
+ return uncheck(() -> SessionHandlerTest.getRenderedString(response));
+ }
+
+ private InputStream makeInputStream(String content) {
+ return new ByteArrayInputStream(Utf8.toBytes(content));
+ }
+} \ No newline at end of file