summaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
Diffstat (limited to 'configserver')
-rw-r--r--configserver/CMakeLists.txt1
-rw-r--r--configserver/pom.xml6
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java8
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java18
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java50
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java57
-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.java116
-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.xml9
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java189
12 files changed, 527 insertions, 12 deletions
diff --git a/configserver/CMakeLists.txt b/configserver/CMakeLists.txt
index 5d3f88ac777..fe905ced973 100644
--- a/configserver/CMakeLists.txt
+++ b/configserver/CMakeLists.txt
@@ -14,6 +14,7 @@ install(CODE "execute_process(COMMAND mkdir -p \$ENV{DESTDIR}/\${CMAKE_INSTALL_P
install(CODE "execute_process(COMMAND mkdir -p \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/config-models)")
install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/config-model-fat.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/config-model-fat.jar)")
install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/configserver-jar-with-dependencies.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/configserver.jar)")
+install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/configserver-flags-jar-with-dependencies.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/configserver-flags.jar)")
install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/flags-jar-with-dependencies.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/flags.jar)")
install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/orchestrator-jar-with-dependencies.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/orchestrator.jar)")
install(CODE "execute_process(COMMAND ln -snf \${CMAKE_INSTALL_PREFIX}/lib/jars/service-monitor-jar-with-dependencies.jar \$ENV{DESTDIR}/\${CMAKE_INSTALL_PREFIX}/conf/configserver-app/components/service-monitor.jar)")
diff --git a/configserver/pom.xml b/configserver/pom.xml
index f932a963d14..210e584fd85 100644
--- a/configserver/pom.xml
+++ b/configserver/pom.xml
@@ -90,6 +90,12 @@
</dependency>
<dependency>
<groupId>com.yahoo.vespa</groupId>
+ <artifactId>configserver-flags</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
<artifactId>config-application-package</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
index 0b1684b6735..f240129eda1 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java
@@ -12,8 +12,8 @@ import com.yahoo.container.jdisc.state.StateMonitor;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.config.server.rpc.RpcServer;
import com.yahoo.vespa.config.server.version.VersionState;
-import com.yahoo.vespa.flags.FeatureFlag;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import java.time.Duration;
import java.time.Instant;
@@ -30,7 +30,8 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
-import static com.yahoo.vespa.config.server.ConfigServerBootstrap.RedeployingApplicationsFails.*;
+import static com.yahoo.vespa.config.server.ConfigServerBootstrap.RedeployingApplicationsFails.CONTINUE;
+import static com.yahoo.vespa.config.server.ConfigServerBootstrap.RedeployingApplicationsFails.EXIT_JVM;
/**
* Main component that bootstraps and starts config server threads.
@@ -45,7 +46,6 @@ import static com.yahoo.vespa.config.server.ConfigServerBootstrap.RedeployingApp
public class ConfigServerBootstrap extends AbstractComponent implements Runnable {
private static final Logger log = Logger.getLogger(ConfigServerBootstrap.class.getName());
- private static final String bootstrapFeatureFlag = "config-server-bootstrap-in-separate-thread";
// INITIALIZE_ONLY is for testing only
enum Mode {BOOTSTRAP_IN_CONSTRUCTOR, BOOTSTRAP_IN_SEPARATE_THREAD, INITIALIZE_ONLY}
@@ -69,7 +69,7 @@ public class ConfigServerBootstrap extends AbstractComponent implements Runnable
VersionState versionState, StateMonitor stateMonitor, VipStatus vipStatus,
FlagSource flagSource) {
this(applicationRepository, server, versionState, stateMonitor, vipStatus,
- new FeatureFlag(bootstrapFeatureFlag, true, flagSource).value()
+ Flags.CONFIG_SERVER_BOOTSTRAP_IN_SEPARATE_THREAD.bindTo(flagSource).value()
? Mode.BOOTSTRAP_IN_SEPARATE_THREAD
: Mode.BOOTSTRAP_IN_CONSTRUCTOR,
EXIT_JVM);
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
index 032bb88eaaa..2463cca190a 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java
@@ -1,11 +1,11 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.config.server.application;
+import com.yahoo.component.Version;
import com.yahoo.config.ConfigurationRuntimeException;
import com.yahoo.config.model.api.ApplicationInfo;
import com.yahoo.config.model.api.Model;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.component.Version;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.config.ConfigCacheKey;
import com.yahoo.vespa.config.ConfigDefinitionKey;
@@ -15,17 +15,19 @@ import com.yahoo.vespa.config.GetConfigRequest;
import com.yahoo.vespa.config.buildergen.ConfigDefinition;
import com.yahoo.vespa.config.protocol.ConfigResponse;
import com.yahoo.vespa.config.protocol.DefContent;
-import com.yahoo.vespa.config.server.rpc.ConfigResponseFactory;
import com.yahoo.vespa.config.server.ServerCache;
-import com.yahoo.vespa.config.server.tenant.TenantRepository;
-import com.yahoo.vespa.config.server.rpc.UncompressedConfigResponseFactory;
import com.yahoo.vespa.config.server.UnknownConfigDefinitionException;
import com.yahoo.vespa.config.server.modelfactory.ModelResult;
import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
+import com.yahoo.vespa.config.server.rpc.ConfigResponseFactory;
+import com.yahoo.vespa.config.server.rpc.UncompressedConfigResponseFactory;
+import com.yahoo.vespa.config.server.tenant.TenantRepository;
import com.yahoo.vespa.config.util.ConfigUtils;
-import com.yahoo.vespa.flags.FeatureFlag;
+import com.yahoo.vespa.flags.BooleanFlag;
+import com.yahoo.vespa.flags.FetchVector;
import com.yahoo.vespa.flags.FileFlagSource;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import java.util.Objects;
import java.util.Set;
@@ -47,7 +49,7 @@ public class Application implements ModelResult {
private final ServerCache cache;
private final MetricUpdater metricUpdater;
private final ApplicationId app;
- private final FeatureFlag useConfigServerCache;
+ private final BooleanFlag useConfigServerCache;
public Application(Model model, ServerCache cache, long appGeneration, boolean internalRedeploy,
Version vespaVersion, MetricUpdater metricUpdater, ApplicationId app) {
@@ -64,7 +66,9 @@ public class Application implements ModelResult {
this.vespaVersion = vespaVersion;
this.metricUpdater = metricUpdater;
this.app = app;
- this.useConfigServerCache = new FeatureFlag("use-config-server-cache", true, flagSource);
+ this.useConfigServerCache = Flags.USE_CONFIG_SERVER_CACHE
+ .with(FetchVector.Dimension.APPLICATION_ID, app.serializedForm())
+ .bindTo(flagSource);
}
/**
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..6d5d0df8f1a
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java
@@ -0,0 +1,50 @@
+// 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.FlagDefinition;
+import com.yahoo.vespa.flags.json.DimensionHelper;
+
+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());
+ 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)));
+ });
+ 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..ae72660ce9f
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java
@@ -0,0 +1,57 @@
+// 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 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 Map<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 = flags;
+ this.recursive = recursive;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) {
+ ObjectNode rootNode = mapper.createObjectNode();
+ ArrayNode flagsArray = rootNode.putArray("flags");
+ // Order flags by ID
+ new TreeMap<>(this.flags).forEach((flagId, flagData) -> {
+ if (recursive) {
+ flagsArray.add(flagData.toJsonNode());
+ } else {
+ 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..e5efc74fb75
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java
@@ -0,0 +1,116 @@
+// 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().getPath());
+ 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 getDefinedFlagList(request);
+ throw new NotFoundException("Nothing at path '" + path + "'");
+ }
+
+ @Override
+ protected HttpResponse handlePUT(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ 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().getPath());
+ if (path.matches("/flags/v1/data/{flagId}")) return deleteFlagData(findFlagId(request, path));
+ throw new NotFoundException("Nothing at path '" + path + "'");
+ }
+
+ private HttpResponse getDefinedFlagList(HttpRequest request) {
+ return new DefinedFlags(Flags.getAllFlags());
+ }
+
+ private String flagsV1Uri(HttpRequest request) {
+ URI uri = request.getUri();
+ return uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + "/flags/v1";
+ }
+
+ 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 5ba05a629c6..8c64bb2cdfb 100644
--- a/configserver/src/main/resources/configserver-app/services.xml
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -60,7 +60,8 @@
<!-- TODO Vespa 7: Remove scoreboard.xml, replaced by metrics-packets.xml -->
<preprocess:include file='hosted-vespa/scoreboard.xml' required='false' />
- <component id="com.yahoo.vespa.flags.FileFlagSource" bundle="flags"/>
+ <component id="com.yahoo.vespa.configserver.flags.ConfigServerFlagSource" bundle="configserver-flags"/>
+ <component id="com.yahoo.vespa.configserver.flags.db.FlagsDbImpl" bundle="configserver-flags"/>
<preprocess:include file='hosted-vespa/metrics-packets.xml' required='false' />
<preprocess:include file='controller/container.xml' required='false' />
@@ -103,6 +104,12 @@
<binding>http://*/status</binding>
<binding>https://*/status</binding>
</handler>
+ <handler id='com.yahoo.vespa.config.server.http.flags.FlagsHandler' bundle='configserver'>
+ <binding>http://*/flags/v1</binding>
+ <binding>https://*/flags/v1</binding>
+ <binding>http://*/flags/v1/*</binding>
+ <binding>https://*/flags/v1/*</binding>
+ </handler>
<handler id='com.yahoo.vespa.config.server.http.v2.TenantHandler' bundle='configserver'>
<binding>http://*/application/v2/tenant/</binding>
<binding>https://*/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..71caed77dbe
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java
@@ -0,0 +1,189 @@
+// 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\"]}}");
+ }
+ }
+
+ 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\"}]}");
+
+ // 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\":[\"app2\",\"app1\"]}],\"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\":[\"app2\",\"app1\"]}],\"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) {
+ String uri = FLAGS_V1_URL + 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