summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--configserver-flags/CMakeLists.txt2
-rw-r--r--configserver-flags/OWNERS1
-rw-r--r--configserver-flags/README.md3
-rw-r--r--configserver-flags/pom.xml100
-rw-r--r--configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSource.java18
-rw-r--r--configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/FlagsDb.java25
-rw-r--r--configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImpl.java56
-rw-r--r--configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/ZooKeeperFlagSource.java25
-rw-r--r--configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java7
-rw-r--r--configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java56
-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/FlagDataListResponse.java52
-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.java94
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java19
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java29
-rw-r--r--configserver/src/main/resources/configserver-app/services.xml8
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java161
-rw-r--r--flags/pom.xml11
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java10
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java62
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java82
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java43
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flag.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java41
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagId.java7
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java8
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java3
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flags.java130
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java58
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java23
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java48
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java33
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java14
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Serializer.java10
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java39
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java38
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java65
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java104
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java58
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java5
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java48
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java27
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java19
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java55
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java19
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java47
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java131
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java66
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java50
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java38
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java82
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java130
-rw-r--r--parent/pom.xml28
-rw-r--r--pom.xml1
-rw-r--r--service-monitor/src/main/java/com/yahoo/vespa/service/duper/DuperModelManager.java10
-rw-r--r--service-monitor/src/main/java/com/yahoo/vespa/service/health/HealthMonitorManager.java15
-rw-r--r--service-monitor/src/test/java/com/yahoo/vespa/service/health/HealthMonitorManagerTest.java5
65 files changed, 2165 insertions, 405 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7676bc6e886..476915d44a9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -39,6 +39,7 @@ add_subdirectory(config-model-fat)
add_subdirectory(configd)
add_subdirectory(configdefinitions)
add_subdirectory(configserver)
+add_subdirectory(configserver-flags)
add_subdirectory(configutil)
add_subdirectory(container-accesslogging)
add_subdirectory(container-core)
diff --git a/configserver-flags/CMakeLists.txt b/configserver-flags/CMakeLists.txt
new file mode 100644
index 00000000000..75deaf42d9b
--- /dev/null
+++ b/configserver-flags/CMakeLists.txt
@@ -0,0 +1,2 @@
+# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+install_fat_java_artifact(configserver-flags)
diff --git a/configserver-flags/OWNERS b/configserver-flags/OWNERS
new file mode 100644
index 00000000000..e131dacde49
--- /dev/null
+++ b/configserver-flags/OWNERS
@@ -0,0 +1 @@
+hakonhall
diff --git a/configserver-flags/README.md b/configserver-flags/README.md
new file mode 100644
index 00000000000..e23452a1f0c
--- /dev/null
+++ b/configserver-flags/README.md
@@ -0,0 +1,3 @@
+<!-- Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+# Config Server Flags
+Manages flags backed by the Config Server's ZooKeeper.
diff --git a/configserver-flags/pom.xml b/configserver-flags/pom.xml
new file mode 100644
index 00000000000..c6fdbe89b95
--- /dev/null
+++ b/configserver-flags/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>configserver-flags</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>container-plugin</packaging>
+ <name>${project.artifactId}</name>
+ <description>Config Server Flags.</description>
+
+ <dependencies>
+ <!-- provided -->
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>zkfacade</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>flags</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>yolean</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>provided</scope>
+ <classifier>no_aop</classifier>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- test -->
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.curator</groupId>
+ <artifactId>curator-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSource.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSource.java
new file mode 100644
index 00000000000..9768c42b477
--- /dev/null
+++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSource.java
@@ -0,0 +1,18 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configserver.flags;
+
+import com.google.inject.Inject;
+import com.yahoo.vespa.configserver.flags.db.FlagsDbImpl;
+import com.yahoo.vespa.configserver.flags.db.ZooKeeperFlagSource;
+import com.yahoo.vespa.flags.FileFlagSource;
+import com.yahoo.vespa.flags.OrderedFlagSource;
+
+/**
+ * @author hakonhall
+ */
+public class ConfigServerFlagSource extends OrderedFlagSource {
+ @Inject
+ public ConfigServerFlagSource(FlagsDbImpl flagsDb) {
+ super(new FileFlagSource(), new ZooKeeperFlagSource(flagsDb));
+ }
+}
diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/FlagsDb.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/FlagsDb.java
new file mode 100644
index 00000000000..2c29ae0b818
--- /dev/null
+++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/FlagsDb.java
@@ -0,0 +1,25 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configserver.flags;
+
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.json.FlagData;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author hakonhall
+ */
+public interface FlagsDb {
+ /** Get the String value of the flag. */
+ Optional<FlagData> getValue(FlagId flagId);
+
+ /** Set the String value of the flag. */
+ void setValue(FlagId flagId, FlagData data);
+
+ /** Remove the flag value if it exists. */
+ void removeValue(FlagId flagId);
+
+ /** Get all flags that have been set. */
+ Map<FlagId, FlagData> getAllFlags();
+}
diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImpl.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImpl.java
new file mode 100644
index 00000000000..7b0a2f632cc
--- /dev/null
+++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImpl.java
@@ -0,0 +1,56 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configserver.flags.db;
+
+import com.google.inject.Inject;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.configserver.flags.FlagsDb;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.json.FlagData;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsDbImpl implements FlagsDb {
+ private static final Path ROOT_PATH = Path.fromString("/flags/v1");
+
+ private final Curator curator;
+
+ @Inject
+ public FlagsDbImpl(Curator curator) {
+ this.curator = curator;
+ }
+
+ @Override
+ public Optional<FlagData> getValue(FlagId flagId) {
+ return curator.getData(getZkPathFor(flagId)).map(FlagData::deserializeUtf8Json);
+ }
+
+ @Override
+ public void setValue(FlagId flagId, FlagData data) {
+ curator.set(getZkPathFor(flagId), data.serializeToUtf8Json());
+ }
+
+ @Override
+ public Map<FlagId, FlagData> getAllFlags() {
+ Map<FlagId, FlagData> flags = new HashMap<>();
+ for (String flagId : curator.getChildren(ROOT_PATH)) {
+ FlagId id = new FlagId(flagId);
+ getValue(id).ifPresent(data -> flags.put(id, data));
+ }
+ return flags;
+ }
+
+ @Override
+ public void removeValue(FlagId flagId) {
+ curator.delete(getZkPathFor(flagId));
+ }
+
+ private static Path getZkPathFor(FlagId flagId) {
+ return ROOT_PATH.append(flagId.toString());
+ }
+}
diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/ZooKeeperFlagSource.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/ZooKeeperFlagSource.java
new file mode 100644
index 00000000000..bd99ac6eca9
--- /dev/null
+++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/db/ZooKeeperFlagSource.java
@@ -0,0 +1,25 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configserver.flags.db;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.RawFlag;
+
+import java.util.Optional;
+
+/**
+ * @author hakonhall
+ */
+public class ZooKeeperFlagSource implements FlagSource {
+ private final FlagsDbImpl flagsDb;
+
+ public ZooKeeperFlagSource(FlagsDbImpl flagsDb) {
+ this.flagsDb = flagsDb;
+ }
+
+ @Override
+ public Optional<RawFlag> fetch(FlagId id, FetchVector vector) {
+ return flagsDb.getValue(id).flatMap(data -> data.resolve(vector));
+ }
+}
diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java
new file mode 100644
index 00000000000..97e66d95715
--- /dev/null
+++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.configserver.flags;
+
+import com.yahoo.osgi.annotation.ExportPackage;
+
+/** The node repository controls and allocates the nodes available in a hosted Vespa zone */
diff --git a/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java
new file mode 100644
index 00000000000..1fe61130348
--- /dev/null
+++ b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/db/FlagsDbImplTest.java
@@ -0,0 +1,56 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configserver.flags.db;
+
+import com.yahoo.vespa.curator.mock.MockCurator;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.JsonNodeRawFlag;
+import com.yahoo.vespa.flags.json.Condition;
+import com.yahoo.vespa.flags.json.FlagData;
+import com.yahoo.vespa.flags.json.Rule;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsDbImplTest {
+ @Test
+ public void test() {
+ MockCurator curator = new MockCurator();
+ FlagsDbImpl db = new FlagsDbImpl(curator);
+
+ Condition condition1 = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME, "host1");
+ Rule rule1 = new Rule(Optional.of(JsonNodeRawFlag.fromJson("13")), condition1);
+ FlagData data = new FlagData(new FetchVector().with(FetchVector.Dimension.ZONE_ID, "zone-a"), rule1);
+ FlagId flagId = new FlagId("id");
+ db.setValue(flagId, data);
+
+ assertTrue(db.getValue(flagId).isPresent());
+ Optional<FlagData> dataCopy = db.getValue(flagId);
+ assertTrue(dataCopy.isPresent());
+
+ assertEquals("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"hostname\"," +
+ "\"values\":[\"host1\"]}],\"value\":13}],\"attributes\":{\"zone\":\"zone-a\"}}",
+ dataCopy.get().serializeToJson());
+
+ FlagId flagId2 = new FlagId("id2");
+ db.setValue(flagId2, data);
+ Map<FlagId, FlagData> flags = db.getAllFlags();
+ assertThat(flags.size(), equalTo(2));
+ assertThat(flags.get(flagId), notNullValue());
+ assertThat(flags.get(flagId2), notNullValue());
+
+ db.removeValue(flagId2);
+ assertFalse(db.getValue(flagId2).isPresent());
+ }
+} \ No newline at end of file
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..2d5d5296762 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.FetchVector;
import com.yahoo.vespa.flags.FileFlagSource;
+import com.yahoo.vespa.flags.Flag;
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 Flag<Boolean> 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/FlagDataListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java
new file mode 100644
index 00000000000..4d9ba96b791
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java
@@ -0,0 +1,52 @@
+// 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.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 showDataInsteadOfUrl;
+
+ public FlagDataListResponse(String flagsV1Uri, Map<FlagId, FlagData> flags, boolean showDataInsteadOfUrl) {
+ super(Response.Status.OK);
+ this.flagsV1Uri = flagsV1Uri;
+ this.flags = flags;
+ this.showDataInsteadOfUrl = showDataInsteadOfUrl;
+ }
+
+ @Override
+ public void render(OutputStream outputStream) {
+ ObjectNode rootNode = mapper.createObjectNode();
+ // Order flags by ID
+ new TreeMap<>(flags).forEach((flagId, flagData) -> {
+ if (showDataInsteadOfUrl) {
+ rootNode.set(flagId.toString(), flagData.toJsonNode());
+ } else {
+ rootNode.putObject(flagId.toString()).put("url", flagsV1Uri + "/data/" + flagId.toString());
+ }
+ });
+ uncheck(() -> mapper.writeValue(outputStream, rootNode));
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/json";
+ }
+}
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..3b4194fc474
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java
@@ -0,0 +1,94 @@
+// 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.HttpHandler;
+import com.yahoo.vespa.config.server.http.NotFoundException;
+import com.yahoo.vespa.configserver.flags.FlagsDb;
+import com.yahoo.vespa.flags.FlagId;
+import com.yahoo.vespa.flags.Flags;
+import com.yahoo.vespa.flags.json.FlagData;
+
+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));
+ if (path.matches("/flags/v1/data")) return getFlagDataList(request);
+ if (path.matches("/flags/v1/data/{flagId}")) return getFlagData(findFlagId(request, path));
+ 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 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) {
+ flagsDb.setValue(flagId, FlagData.deserialize(request.getData()));
+
+ // The set & get is not atomic, but no harm is done in showing an outdated flag value in the response.
+ return getFlagData(flagId);
+ }
+
+ 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 (!Objects.equals(request.getProperty("force"), "true")) {
+ if (Flags.getAllFlags().stream().noneMatch(definition -> flagId.equals(definition.getUnboundFlag().id()))) {
+ throw new NotFoundException("There is no flag '" + flagId + "' (use ?force=true to override)");
+ }
+ }
+
+ return flagId;
+ }
+}
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..19363fbdadc
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java
@@ -0,0 +1,19 @@
+// 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;
+
+/**
+ * @author hakonhall
+ */
+public class OKResponse extends EmptyResponse {
+ public OKResponse() {
+ super(Response.Status.OK);
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/json";
+ }
+}
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..26eb96f1ef5
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java
@@ -0,0 +1,29 @@
+// 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 static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+public class V1Response extends StaticResponse {
+ public V1Response(String flagsV1Uri) {
+ super(Response.Status.OK, HttpConfigResponse.JSON_CONTENT_TYPE, generateBody(flagsV1Uri));
+ }
+
+ private static String generateBody(String flagsV1Uri) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor data = root.setObject("data");
+ data.setString("url", flagsV1Uri + "/data");
+ 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..3e0572a8c99 100644
--- a/configserver/src/main/resources/configserver-app/services.xml
+++ b/configserver/src/main/resources/configserver-app/services.xml
@@ -60,7 +60,7 @@
<!-- 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="flags"/>
<preprocess:include file='hosted-vespa/metrics-packets.xml' required='false' />
<preprocess:include file='controller/container.xml' required='false' />
@@ -103,6 +103,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..d88bd6ab4a5
--- /dev/null
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java
@@ -0,0 +1,161 @@
+// 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.UnboundFlag;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsHandlerTest {
+ private static final UnboundFlag<Boolean> FLAG1 =
+ Flags.defineBoolean("id1", false, "desc1", "mod1");
+ private static final UnboundFlag<Boolean> FLAG2 =
+ Flags.defineBoolean("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() {
+ verifySuccessfulRequest(Method.GET, "", "",
+ "{\"data\":{\"url\":\"https://foo.com:4443/flags/v1/data\"}}");
+ verifySuccessfulRequest(Method.GET, "/", "",
+ "{\"data\":{\"url\":\"https://foo.com:4443/flags/v1/data\"}}");
+ }
+
+ @Test
+ public void testData() {
+ // PUT flag with ID id1
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG1.id(),
+ "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ]\n" +
+ "}",
+ "{\"rules\":[{\"value\":true}]}");
+
+ // GET on ID id1 should return the same as the put (this will also issue a payload for the get,
+ // which we assume will be ignored).
+ verifySuccessfulRequest(Method.GET, "/data/" + FLAG1.id(),
+ "", "{\"rules\":[{\"value\":true}]}");
+
+ // List all flags should list only id1
+ verifySuccessfulRequest(Method.GET, "/data",
+ "", "{\"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/",
+ "", "{\"id1\":{\"url\":\"https://foo.com:4443/flags/v1/data/id1\"}}");
+
+ // PUT id2
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG2.id(),
+ "{\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",
+ "{\"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",
+ "",
+ "{\"id1\":{\"url\":\"https://foo.com:4443/flags/v1/data/id1\"},\"id2\":{\"url\":\"https://foo.com:4443/flags/v1/data/id2\"}}");
+
+ // Putting (overriding) id1 should work silently
+ verifySuccessfulRequest(Method.PUT, "/data/" + FLAG1.id(),
+ "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"value\": false\n" +
+ " }\n" +
+ " ]\n" +
+ "}\n",
+ "{\"rules\":[{\"value\":false}]}");
+
+ // Get all recursivelly displays all flag data
+ verifySuccessfulRequest(Method.GET, "/data?recursive=true", "",
+ "{\"id1\":{\"rules\":[{\"value\":false}]},\"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", "", "{}");
+ }
+
+ @Test
+ public void testForcing() {
+ FlagId undefinedFlagId = new FlagId("undef");
+ HttpResponse response = handle(Method.PUT, "/data/" + undefinedFlagId, "");
+
+ assertEquals(404, response.getStatus());
+ assertEquals("application/json", response.getContentType());
+
+ }
+
+ private void verifySuccessfulRequest(Method method, String pathSuffix, String requestBody, String expectedResponseBody) {
+ HttpResponse response = handle(method, pathSuffix, requestBody);
+
+ assertEquals(200, response.getStatus());
+ assertEquals("application/json", response.getContentType());
+ String actualResponse = uncheck(() -> SessionHandlerTest.getRenderedString(response));
+
+ assertThat(actualResponse, is(expectedResponseBody));
+ }
+
+ private HttpResponse handle(Method method, String pathSuffix, String requestBody) {
+ String uri = FLAGS_V1_URL + pathSuffix;
+ HttpRequest request = HttpRequest.createTestRequest(uri, method, makeInputStream(requestBody));
+ return handler.handle(request);
+ }
+
+ private String makeUrl(String component) {
+ return FLAGS_V1_URL + "/" + component;
+ }
+
+ private InputStream makeInputStream(String content) {
+ return new ByteArrayInputStream(Utf8.toBytes(content));
+ }
+} \ No newline at end of file
diff --git a/flags/pom.xml b/flags/pom.xml
index 5a535ad4de8..ade598556de 100644
--- a/flags/pom.xml
+++ b/flags/pom.xml
@@ -13,9 +13,10 @@
</parent>
<artifactId>flags</artifactId>
- <version>6-SNAPSHOT</version>
<packaging>container-plugin</packaging>
+ <version>6-SNAPSHOT</version>
<name>${project.artifactId}</name>
+ <description>Feature flags.</description>
<dependencies>
<dependency>
@@ -55,6 +56,14 @@
<artifactId>bundle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>8</source>
+ <target>8</target>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java b/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java
new file mode 100644
index 00000000000..7ececa5489a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Deserializer.java
@@ -0,0 +1,10 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+@FunctionalInterface
+public interface Deserializer<T> {
+ T deserialize(RawFlag rawFlag);
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java
deleted file mode 100644
index 7cc5529646e..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * A FeatureFlag is a boolean flag.
- *
- * @author hakonhall
- */
-public class FeatureFlag implements Flag {
- private final boolean defaultValue;
- private final FlagId id;
- private final FlagSource source;
-
- public static Function<FlagSource, FeatureFlag> createUnbound(String flagId, boolean defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, FeatureFlag> createUnbound(FlagId id, boolean defaultValue) {
- return source -> new FeatureFlag(id, defaultValue, source);
- }
-
- public FeatureFlag(String flagId, boolean defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public FeatureFlag(FlagId id, boolean defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public boolean value() {
- return source.getString(id).map(FeatureFlag::booleanFromString).orElse(defaultValue);
- }
-
- private static boolean booleanFromString(String string) {
- String canonicalString = string.trim().toLowerCase();
- if (canonicalString.equals("true")) {
- return true;
- } else if (canonicalString.equals("false")) {
- return false;
- }
-
- throw new IllegalArgumentException("Unable to convert to true or false: '" + string + "'");
- }
-
- @Override
- public String toString() {
- return "IntFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
new file mode 100644
index 00000000000..581ec599aab
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FetchVector.java
@@ -0,0 +1,82 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Denotes which RawFlag should be retrieved from {@link FlagSource} for a given {@link FlagId},
+ * as the raw flag may depend on the hostname, application, etc.
+ *
+ * @author hakonhall
+ */
+@Immutable
+public
+class FetchVector {
+ public enum Dimension {
+ /** Value from ZoneId::value */
+ ZONE_ID,
+ /** Value from ApplicationId::serializedForm */
+ APPLICATION_ID,
+ /** Fully qualified hostname */
+ HOSTNAME
+ }
+
+ private final Map<Dimension, String> map;
+
+ public FetchVector() {
+ this.map = Collections.emptyMap();
+ }
+
+ public static FetchVector fromMap(Map<Dimension, String> map) {
+ return new FetchVector(new HashMap<>(map));
+ }
+
+ private FetchVector(Map<Dimension, String> map) {
+ this.map = Collections.unmodifiableMap(map);
+ }
+
+ public Optional<String> getValue(Dimension dimension) {
+ return Optional.ofNullable(map.get(dimension));
+ }
+
+ public Map<Dimension, String> toMap() {
+ return map;
+ }
+
+ /** Returns a new FetchVector, identical to {@code this} except for its value in {@code dimension}. */
+ public FetchVector with(Dimension dimension, String value) {
+ return makeFetchVector(merged -> merged.put(dimension, value));
+ }
+
+ /** Returns a new FetchVector, identical to {@code this} except for its values in the override's dimensions. */
+ public FetchVector with(FetchVector override) {
+ return makeFetchVector(vector -> vector.putAll(override.map));
+ }
+
+ private FetchVector makeFetchVector(Consumer<EnumMap<Dimension, String>> mapModifier) {
+ EnumMap<Dimension, String> mergedMap = new EnumMap<>(Dimension.class);
+ mergedMap.putAll(map);
+ mapModifier.accept(mergedMap);
+ return new FetchVector(mergedMap);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ FetchVector that = (FetchVector) o;
+ return Objects.equals(map, that.map);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(map);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
index 1a856fee60a..3403a15d7ff 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
@@ -1,7 +1,10 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.flags;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
+import com.yahoo.vespa.flags.json.FlagData;
+import com.yahoo.vespa.flags.json.Rule;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -11,14 +14,15 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.Optional;
/**
- * A {@link FlagSource} backed by local files.
- *
* @author hakonhall
*/
public class FileFlagSource implements FlagSource {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
static final String FLAGS_DIRECTORY = "/etc/vespa/flags";
private final Path flagsDirectory;
@@ -37,13 +41,36 @@ public class FileFlagSource implements FlagSource {
}
@Override
- public Optional<String> getString(FlagId id) {
- return getBytes(id).map(bytes -> new String(bytes, StandardCharsets.UTF_8));
+ public Optional<RawFlag> fetch(FlagId flagId, FetchVector vector) {
+ return getResolver(flagId).resolve(vector);
+ }
+
+ private FlagData getResolver(FlagId flagId) {
+ Optional<String> v2String = getString(flagId, ".2");
+ if (v2String.isPresent()) {
+ return FlagData.deserialize(v2String.get());
+ }
+
+ Optional<String> v1String = getString(flagId, "");
+ if (v1String.isPresent()) {
+ // version 1: File contains value as a JSON
+ // version 2: File contains FileResolver as a JSON (which may contain many values, one for each rule)
+ // version 1 files should probably be discontinued
+ Rule rule = new Rule(Optional.of(JsonNodeRawFlag.fromJson(v1String.get())), Collections.emptyList());
+ return new FlagData(new FetchVector(), Collections.singletonList(rule));
+ }
+
+ // Will eventually resolve to empty RawFlag
+ return new FlagData();
+ }
+
+ private Optional<String> getString(FlagId id, String suffix) {
+ return getBytes(id, suffix).map(bytes -> new String(bytes, StandardCharsets.UTF_8));
}
- public Optional<byte[]> getBytes(FlagId id) {
+ private Optional<byte[]> getBytes(FlagId id, String suffix) {
try {
- return Optional.of(Files.readAllBytes(getPath(id)));
+ return Optional.of(Files.readAllBytes(getPath(id, suffix)));
} catch (NoSuchFileException e) {
return Optional.empty();
} catch (IOException e) {
@@ -51,8 +78,8 @@ public class FileFlagSource implements FlagSource {
}
}
- private Path getPath(FlagId id) {
- return flagsDirectory.resolve(id.toString());
+ private Path getPath(FlagId id, String suffix) {
+ return flagsDirectory.resolve(id.toString() + suffix);
}
@Override
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
index 831e0d0dab9..18c1db0d756 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
@@ -1,9 +1,44 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.flags;
+import javax.annotation.concurrent.Immutable;
+
/**
* @author hakonhall
*/
-public interface Flag {
- FlagId id();
+@Immutable
+public class Flag<T> {
+ private final FlagId id;
+ private final T defaultValue;
+ private final FlagSource source;
+ private final Deserializer<T> deserializer;
+ private final FetchVector fetchVector;
+
+ public Flag(String flagId, T defaultValue, FlagSource source, Deserializer<T> deserializer) {
+ this(new FlagId(flagId), defaultValue, source, deserializer);
+ }
+
+ public Flag(FlagId id, T defaultValue, FlagSource source, Deserializer<T> deserializer) {
+ this(id, defaultValue, deserializer, new FetchVector(), source);
+ }
+
+ public Flag(FlagId id, T defaultValue, Deserializer<T> deserializer, FetchVector fetchVector, FlagSource source) {
+ this.id = id;
+ this.defaultValue = defaultValue;
+ this.source = source;
+ this.deserializer = deserializer;
+ this.fetchVector = fetchVector;
+ }
+
+ public FlagId id() {
+ return id;
+ }
+
+ public Flag<T> with(FetchVector.Dimension dimension, String value) {
+ return new Flag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value), source);
+ }
+
+ public T value() {
+ return source.fetch(id, fetchVector).map(deserializer::deserialize).orElse(defaultValue);
+ }
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java
new file mode 100644
index 00000000000..a3f490e2f96
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagDefinition.java
@@ -0,0 +1,41 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class FlagDefinition<T> {
+ private final UnboundFlag<T> unboundFlag;
+ private final String description;
+ private final String modificationEffect;
+ private final List<FetchVector.Dimension> dimensions;
+
+ public FlagDefinition(UnboundFlag<T> unboundFlag, String description, String modificationEffect,
+ List<FetchVector.Dimension> dimensions) {
+ this.unboundFlag = unboundFlag;
+ this.description = description;
+ this.modificationEffect = modificationEffect;
+ this.dimensions = Collections.unmodifiableList(dimensions);
+ }
+
+ public UnboundFlag<T> getUnboundFlag() {
+ return unboundFlag;
+ }
+
+ public List<FetchVector.Dimension> getDimensions() {
+ return dimensions;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getModificationEffect() {
+ return modificationEffect;
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
index f004df063ed..ae38fbe7dc1 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
@@ -9,7 +9,7 @@ import java.util.regex.Pattern;
* @author hakonhall
*/
@Immutable
-public class FlagId {
+public class FlagId implements Comparable<FlagId> {
private static final Pattern ID_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$");
private final String id;
@@ -23,6 +23,11 @@ public class FlagId {
}
@Override
+ public int compareTo(FlagId that) {
+ return this.id.compareTo(that.id);
+ }
+
+ @Override
public String toString() {
return id;
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java
new file mode 100644
index 00000000000..697cbfcbb4a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagSerializer.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+public interface FlagSerializer<T> extends Serializer<T>, Deserializer<T> {
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
index 509db40c4d4..da8e6b29cab 100644
--- a/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
@@ -7,6 +7,5 @@ import java.util.Optional;
* @author hakonhall
*/
public interface FlagSource {
- /** The String value of a flag, or empty if not set by the source. */
- Optional<String> getString(FlagId id);
+ Optional<RawFlag> fetch(FlagId id, FetchVector vector);
}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
new file mode 100644
index 00000000000..b66ba9fc0c9
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java
@@ -0,0 +1,130 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Definitions of most/all flags.
+ *
+ * <p>The flags are centrally defined in this module to allow 1. all code to access flags that may be used in
+ * quite different modules and processes, and 2. in particular allow the config server to access all flags
+ * so operators have a nicer UI for setting, modifying, or removing flag values.
+ *
+ * <p>This class should have been an enum, but unfortunately enums cannot be generic, which will eventually be
+ * fixed with <a href="https://openjdk.java.net/jeps/301">JEP 301: Enhanced Enums</a>.
+ *
+ * @author hakonhall
+ */
+public class Flags {
+ public static final FlagSerializer<Boolean> BOOLEAN_SERIALIZER = new SimpleFlagSerializer<>(BooleanNode::valueOf, JsonNode::isBoolean, JsonNode::asBoolean);
+ public static final FlagSerializer<String> STRING_SERIALIZER = new SimpleFlagSerializer<>(TextNode::new, JsonNode::isTextual, JsonNode::asText);
+ public static final FlagSerializer<Integer> INT_SERIALIZER = new SimpleFlagSerializer<>(IntNode::new, JsonNode::isIntegralNumber, JsonNode::asInt);
+ public static final FlagSerializer<Long> LONG_SERIALIZER = new SimpleFlagSerializer<>(LongNode::new, JsonNode::isIntegralNumber, JsonNode::asLong);
+
+ private static final ConcurrentHashMap<FlagId, FlagDefinition<?>> flags = new ConcurrentHashMap<>();
+
+ public static final UnboundFlag<Boolean> HEALTHMONITOR_MONITOR_INFRA = defineBoolean(
+ "healthmonitor-monitorinfra", true,
+ "Whether the health monitor in service monitor monitors the health of infrastructure applications.",
+ "Affects all applications activated after the value is changed.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> DUPERMODEL_CONTAINS_INFRA = defineBoolean(
+ "dupermodel-contains-infra", true,
+ "Whether the DuperModel in config server/controller includes active infrastructure applications " +
+ "(except from controller/config apps).",
+ "Requires restart of config server/controller to take effect.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> DUPERMODEL_USE_CONFIGSERVERCONFIG = defineBoolean(
+ "dupermodel-use-configserverconfig", true,
+ "For historical reasons, the ApplicationInfo in the DuperModel for controllers and config servers " +
+ "is based on the ConfigserverConfig (this flag is true). We want to transition to use the " +
+ "infrastructure application activated by the InfrastructureProvisioner once that supports health.",
+ "Requires restart of config server/controller to take effect.",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static final UnboundFlag<Boolean> USE_CONFIG_SERVER_CACHE = defineBoolean(
+ "use-config-server-cache", true,
+ "Whether config server will use cache to answer config requests.",
+ "Takes effect immediately when changed.",
+ FetchVector.Dimension.HOSTNAME, FetchVector.Dimension.APPLICATION_ID);
+
+ public static final UnboundFlag<Boolean> CONFIG_SERVER_BOOTSTRAP_IN_SEPARATE_THREAD = defineBoolean(
+ "config-server-bootstrap-in-separate-thread", true,
+ "Whether to run config server/controller bootstrap in a separate thread.",
+ "Takes effect only at bootstrap of config server/controller",
+ FetchVector.Dimension.HOSTNAME);
+
+ public static UnboundFlag<Boolean> defineBoolean(String flagId, boolean defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, BOOLEAN_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<String> defineString(String flagId, String defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, STRING_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<Integer> defineInt(String flagId, Integer defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, INT_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static UnboundFlag<Long> defineLong(String flagId, Long defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, LONG_SERIALIZER, description, modificationEffect, dimensions);
+ }
+
+ public static <T> UnboundFlag<T> defineJackson(String flagId, Class<T> jacksonClass, T defaultValue, String description,
+ String modificationEffect, FetchVector.Dimension... dimensions) {
+ return define(flagId, defaultValue, new JacksonSerializer<>(jacksonClass), description, modificationEffect, dimensions);
+ }
+
+ /**
+ * Defines a Flag.
+ *
+ * @param flagId The globally unique FlagId.
+ * @param defaultValue The default value if none is present after resolution.
+ * @param deserializer Deserialize JSON to value type.
+ * @param description Description of how the flag is used.
+ * @param modificationEffect What is required for the flag to take effect? A restart of process? immediately? etc.
+ * @param dimensions What dimensions will be set in the {@link FetchVector} when fetching
+ * the flag value in
+ * {@link FlagSource#fetch(FlagId, FetchVector) FlagSource::fetch}.
+ * For instance, if APPLICATION is one of the dimensions here, you should make sure
+ * APPLICATION is set to the ApplicationId in the fetch vector when fetching the RawFlag
+ * from the FlagSource.
+ * @param <T> The type of the flag value, typically Boolean for flags guarding features.
+ * @return An unbound flag with {@link FetchVector.Dimension#HOSTNAME HOSTNAME} environment. The ZONE environment
+ * is typically implicit.
+ */
+ private static <T> UnboundFlag<T> define(String flagId, T defaultValue, Deserializer<T> deserializer,
+ String description, String modificationEffect,
+ FetchVector.Dimension... dimensions) {
+ UnboundFlag<T> flag = new UnboundFlag<>(flagId, defaultValue, deserializer)
+ .with(FetchVector.Dimension.HOSTNAME, Defaults.getDefaults().vespaHostname());
+ FlagDefinition<T> definition = new FlagDefinition<>(flag, description, modificationEffect, Arrays.asList(dimensions));
+ flags.put(flag.id(), definition);
+ return flag;
+ }
+
+ public static List<FlagDefinition<?>> getAllFlags() {
+ return new ArrayList<>(flags.values());
+ }
+
+ public static Optional<FlagDefinition<?>> getFlag(FlagId flagId) {
+ return Optional.ofNullable(flags.get(flagId));
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
deleted file mode 100644
index f7c9645c5db..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class IntFlag implements Flag {
- private final FlagId id;
- private final int defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, IntFlag> createUnbound(String flagId, int defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, IntFlag> createUnbound(FlagId id, int defaultValue) {
- return source -> new IntFlag(id, defaultValue, source);
- }
-
- public IntFlag(String flagId, int defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public IntFlag(FlagId id, int defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public int value() {
- return source.getString(id).map(String::trim).map(Integer::parseInt).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "IntFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java
deleted file mode 100644
index 99add358e75..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.util.function.Function;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author hakonhall
- */
-public class JacksonFlag<T> implements Flag {
- private final static ObjectMapper mapper = new ObjectMapper();
-
- private final FlagId id;
- private final Class<T> jacksonClass;
- private final T defaultValue;
- private final FlagSource source;
-
- public static <T> Function<FlagSource, JacksonFlag<T>> createUnbound(String flagId, Class<T> jacksonClass, T defaultValue) {
- return createUnbound(new FlagId(flagId), jacksonClass, defaultValue);
- }
-
- public static <T> Function<FlagSource, JacksonFlag<T>> createUnbound(FlagId id, Class<T> jacksonClass, T defaultValue) {
- return source -> new JacksonFlag<>(id, jacksonClass, defaultValue, source);
- }
-
- public JacksonFlag(String flagId, Class<T> jacksonClass, T defaultValue, FlagSource source) {
- this(new FlagId(flagId), jacksonClass, defaultValue, source);
- }
-
- public JacksonFlag(FlagId id, Class<T> jacksonClass, T defaultValue, FlagSource source) {
- this.id = id;
- this.jacksonClass = jacksonClass;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public T value() {
- return source.getString(id).map(string -> uncheck(() -> mapper.readValue(string, jacksonClass))).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "JacksonFlag{" +
- "id=" + id +
- ", jacksonClass=" + jacksonClass +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java
new file mode 100644
index 00000000000..b6a4c9b87c6
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/JacksonSerializer.java
@@ -0,0 +1,23 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+public class JacksonSerializer<T> implements FlagSerializer<T> {
+ private final Class<T> jacksonClass;
+
+ public JacksonSerializer(Class<T> jacksonClass) {
+ this.jacksonClass = jacksonClass;
+ }
+
+ @Override
+ public T deserialize(RawFlag rawFlag) {
+ return JsonNodeRawFlag.fromJsonNode(rawFlag.asJsonNode()).toJacksonClass(jacksonClass);
+ }
+
+ @Override
+ public RawFlag serialize(T value) {
+ return JsonNodeRawFlag.fromJacksonClass(value);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java
new file mode 100644
index 00000000000..f41dd9f9e5c
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/JsonNodeRawFlag.java
@@ -0,0 +1,48 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * {@link RawFlag} using Jackson's {@link JsonNode}.
+ *
+ * @author hakonhall
+ */
+public class JsonNodeRawFlag implements RawFlag {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private final JsonNode jsonNode;
+
+ private JsonNodeRawFlag(JsonNode jsonNode) {
+ this.jsonNode = jsonNode;
+ }
+
+ public static JsonNodeRawFlag fromJson(String json) {
+ return new JsonNodeRawFlag(uncheck(() -> mapper.readTree(json)));
+ }
+
+ public static JsonNodeRawFlag fromJsonNode(JsonNode jsonNode) {
+ return new JsonNodeRawFlag(jsonNode);
+ }
+
+ public static <T> JsonNodeRawFlag fromJacksonClass(T value) {
+ return new JsonNodeRawFlag(uncheck(() -> mapper.valueToTree(value)));
+ }
+
+ public <T> T toJacksonClass(Class<T> jacksonClass) {
+ return uncheck(() -> mapper.treeToValue(jsonNode, jacksonClass));
+ }
+
+ @Override
+ public JsonNode asJsonNode() {
+ return jsonNode;
+ }
+
+ @Override
+ public String asJson() {
+ return jsonNode.toString();
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
deleted file mode 100644
index d60dc7b5adc..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class LongFlag implements Flag {
- private final FlagId id;
- private final long defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, LongFlag> createUnbound(String flagId, int defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, LongFlag> createUnbound(FlagId id, int defaultValue) {
- return source -> new LongFlag(id, defaultValue, source);
- }
-
- public LongFlag(String flagId, long defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public LongFlag(FlagId id, long defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public long value() {
- return source.getString(id).map(String::trim).map(Long::parseLong).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "LongFlag{" +
- "id=" + id +
- ", defaultValue=" + defaultValue +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java
new file mode 100644
index 00000000000..6ca74715999
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/OrderedFlagSource.java
@@ -0,0 +1,33 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A {@link FlagSource#fetch(FlagId, FetchVector) fetch} on this flag source will return the {@link RawFlag}
+ * from the first (highest priority) flag source that returns a raw flag for the fetch vector.
+ *
+ * @author hakonhall
+ */
+public class OrderedFlagSource implements FlagSource {
+ private final List<FlagSource> sources;
+
+ /**
+ *
+ * @param sources Flag sources in descending priority order.
+ */
+ public OrderedFlagSource(FlagSource... sources) {
+ this.sources = Arrays.asList(sources);
+ }
+
+ @Override
+ public Optional<RawFlag> fetch(FlagId id, FetchVector vector) {
+ return sources.stream()
+ .map(source -> source.fetch(id, vector))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java
new file mode 100644
index 00000000000..2308659470e
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/RawFlag.java
@@ -0,0 +1,14 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A {@link RawFlag} represents the typeless flag value, possibly partially deserialized.
+ *
+ * @author hakonhall
+ */
+public interface RawFlag {
+ JsonNode asJsonNode();
+ String asJson();
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java b/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java
new file mode 100644
index 00000000000..3569b10c8f4
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Serializer.java
@@ -0,0 +1,10 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+/**
+ * @author hakonhall
+ */
+@FunctionalInterface
+public interface Serializer<T> {
+ RawFlag serialize(T value);
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java b/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java
new file mode 100644
index 00000000000..2340588f54a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/SimpleFlagSerializer.java
@@ -0,0 +1,39 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * @author hakonhall
+ */
+public class SimpleFlagSerializer<T> implements FlagSerializer<T> {
+ private final Function<T, JsonNode> serializer;
+ private final Predicate<JsonNode> isCorrectType;
+ private final Function<JsonNode, T> deserializer;
+
+ public SimpleFlagSerializer(Function<T, JsonNode> serializer,
+ Predicate<JsonNode> isCorrectType,
+ Function<JsonNode, T> deserializer) {
+ this.serializer = serializer;
+ this.isCorrectType = isCorrectType;
+ this.deserializer = deserializer;
+ }
+
+ @Override
+ public JsonNodeRawFlag serialize(T value) {
+ return JsonNodeRawFlag.fromJsonNode(serializer.apply(value));
+ }
+
+ @Override
+ public T deserialize(RawFlag rawFlag) {
+ JsonNode jsonNode = rawFlag.asJsonNode();
+ if (!isCorrectType.test(jsonNode)) {
+ throw new IllegalArgumentException("Wrong type of JsonNode: " + jsonNode.getNodeType());
+ }
+
+ return deserializer.apply(jsonNode);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
deleted file mode 100644
index 8226e999238..00000000000
--- a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import java.util.function.Function;
-
-/**
- * @author hakonhall
- */
-public class StringFlag implements Flag {
- private final FlagId id;
- private final String defaultValue;
- private final FlagSource source;
-
- public static Function<FlagSource, StringFlag> createUnbound(String flagId, String defaultValue) {
- return createUnbound(new FlagId(flagId), defaultValue);
- }
-
- public static Function<FlagSource, StringFlag> createUnbound(FlagId id, String defaultValue) {
- return source -> new StringFlag(id, defaultValue, source);
- }
-
- public StringFlag(String flagId, String defaultValue, FlagSource source) {
- this(new FlagId(flagId), defaultValue, source);
- }
-
- public StringFlag(FlagId id, String defaultValue, FlagSource source) {
- this.id = id;
- this.defaultValue = defaultValue;
- this.source = source;
- }
-
- @Override
- public FlagId id() {
- return id;
- }
-
- public String value() {
- return source.getString(id).orElse(defaultValue);
- }
-
- @Override
- public String toString() {
- return "StringFlag{" +
- "id=" + id +
- ", defaultValue='" + defaultValue + '\'' +
- ", source=" + source +
- '}';
- }
-}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java
new file mode 100644
index 00000000000..597e19080ab
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/UnboundFlag.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class UnboundFlag<T> {
+ private final FlagId id;
+ private final T defaultValue;
+ private final Deserializer<T> deserializer;
+ private final FetchVector fetchVector;
+
+ public UnboundFlag(String flagId, T defaultValue, Deserializer<T> deserializer) {
+ this(new FlagId(flagId), defaultValue, deserializer, new FetchVector());
+ }
+
+ public UnboundFlag(FlagId id, T defaultValue, Deserializer<T> deserializer, FetchVector fetchVector) {
+ this.id = id;
+ this.defaultValue = defaultValue;
+ this.deserializer = deserializer;
+ this.fetchVector = fetchVector;
+ }
+
+ public FlagId id() {
+ return id;
+ }
+
+ public UnboundFlag<T> with(FetchVector.Dimension dimension, String value) {
+ return new UnboundFlag<>(id, defaultValue, deserializer, fetchVector.with(dimension, value));
+ }
+
+ public Flag<T> bindTo(FlagSource source) {
+ return new Flag<>(id, defaultValue, deserializer, fetchVector, source);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java
new file mode 100644
index 00000000000..c07232bfe66
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Condition.java
@@ -0,0 +1,65 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.json.wire.DimensionHelper;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * @author hakonhall
+ */
+public class Condition implements Predicate<FetchVector> {
+ public enum Type { WHITELIST, BLACKLIST }
+
+ private final Type type;
+ private final FetchVector.Dimension dimension;
+ private final Set<String> values;
+
+ public Condition(Type type, FetchVector.Dimension dimension, String... values) {
+ this(type, dimension, new HashSet<>(Arrays.asList(values)));
+ }
+
+ public Condition(Type type, FetchVector.Dimension dimension, Set<String> values) {
+ this.type = type;
+ this.dimension = dimension;
+ this.values = values;
+ }
+
+ @Override
+ public boolean test(FetchVector vector) {
+ boolean isMember = vector.getValue(dimension).filter(values::contains).isPresent();
+
+ switch (type) {
+ case WHITELIST: return isMember;
+ case BLACKLIST: return !isMember;
+ default: throw new IllegalArgumentException("Unknown type " + type);
+ }
+ }
+
+ public static Condition fromWire(WireCondition wireCondition) {
+ Objects.requireNonNull(wireCondition.type);
+ Type type = Type.valueOf(wireCondition.type.toUpperCase());
+
+ Objects.requireNonNull(wireCondition.dimension);
+ FetchVector.Dimension dimension = DimensionHelper.fromWire(wireCondition.dimension);
+
+ Set<String> values = wireCondition.values == null ? Collections.emptySet() : wireCondition.values;
+
+ return new Condition(type, dimension, values);
+ }
+
+ public WireCondition toWire() {
+ WireCondition wire = new WireCondition();
+ wire.type = type.name().toLowerCase();
+ wire.dimension = DimensionHelper.toWire(dimension);
+ wire.values = values.isEmpty() ? null : values;
+ return wire;
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java
new file mode 100644
index 00000000000..9ee3b2dcd85
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FlagData.java
@@ -0,0 +1,104 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.RawFlag;
+import com.yahoo.vespa.flags.json.wire.FetchVectorHelper;
+import com.yahoo.vespa.flags.json.wire.WireFlagData;
+import com.yahoo.vespa.flags.json.wire.WireRule;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * A data structure containing all data for a single flag, that can be serialized to/from JSON,
+ * and that can be used to implement {@link FlagSource}.
+ *
+ * @author hakonhall
+ */
+@Immutable
+public class FlagData {
+ private final List<Rule> rules;
+ private final FetchVector defaultFetchVector;
+
+ public FlagData() {
+ this(new FetchVector(), Collections.emptyList());
+ }
+
+ public FlagData(FetchVector defaultFetchVector, Rule... rules) {
+ this(defaultFetchVector, Arrays.asList(rules));
+ }
+
+ public FlagData(FetchVector defaultFetchVector, List<Rule> rules) {
+ this.rules = Collections.unmodifiableList(new ArrayList<>(rules));
+ this.defaultFetchVector = defaultFetchVector;
+ }
+
+ public Optional<RawFlag> resolve(FetchVector fetchVector) {
+ return rules.stream()
+ .filter(rule -> rule.match(defaultFetchVector.with(fetchVector)))
+ .findFirst()
+ .flatMap(Rule::getValueToApply);
+ }
+
+ public String serializeToJson() {
+ return toWire().serializeToJson();
+ }
+
+ public byte[] serializeToUtf8Json() {
+ return toWire().serializeToBytes();
+ }
+
+ public void serializeToOutputStream(OutputStream outputStream) {
+ toWire().serializeToOutputStream(outputStream);
+ }
+
+ public JsonNode toJsonNode() {
+ return toWire().serializeToJsonNode();
+ }
+
+ private WireFlagData toWire() {
+ WireFlagData wireFlagData = new WireFlagData();
+
+ if (!rules.isEmpty()) {
+ wireFlagData.rules = rules.stream().map(Rule::toWire).collect(Collectors.toList());
+ }
+
+ wireFlagData.defaultFetchVector = FetchVectorHelper.toWire(defaultFetchVector);
+
+ return wireFlagData;
+ }
+
+ public static FlagData deserializeUtf8Json(byte[] bytes) {
+ return fromWire(WireFlagData.deserialize(bytes));
+ }
+
+ public static FlagData deserialize(InputStream inputStream) {
+ return fromWire(WireFlagData.deserialize(inputStream));
+ }
+
+ public static FlagData deserialize(String string) {
+ return fromWire(WireFlagData.deserialize(string));
+ }
+
+ private static FlagData fromWire(WireFlagData wireFlagData) {
+ return new FlagData(
+ FetchVectorHelper.fromWire(wireFlagData.defaultFetchVector), rulesFromWire(wireFlagData.rules)
+ );
+ }
+
+ private static List<Rule> rulesFromWire(List<WireRule> wireRules) {
+ if (wireRules == null) return Collections.emptyList();
+ return wireRules.stream().map(Rule::fromWire).collect(Collectors.toList());
+ }
+}
+
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java
new file mode 100644
index 00000000000..b7d60889419
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/Rule.java
@@ -0,0 +1,58 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.JsonNodeRawFlag;
+import com.yahoo.vespa.flags.RawFlag;
+import com.yahoo.vespa.flags.json.wire.WireRule;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class Rule {
+ private final List<Condition> andConditions;
+ private final Optional<RawFlag> valueToApply;
+
+ public Rule(Optional<RawFlag> valueToApply, Condition... andConditions) {
+ this(valueToApply, Arrays.asList(andConditions));
+ }
+
+ public Rule(Optional<RawFlag> valueToApply, List<Condition> andConditions) {
+ this.andConditions = andConditions;
+ this.valueToApply = valueToApply;
+ }
+
+ public boolean match(FetchVector fetchVector) {
+ return andConditions.stream().allMatch(condition -> condition.test(fetchVector));
+ }
+
+ public Optional<RawFlag> getValueToApply() {
+ return valueToApply;
+ }
+
+ public WireRule toWire() {
+ WireRule wireRule = new WireRule();
+
+ if (!andConditions.isEmpty()) {
+ wireRule.andConditions = andConditions.stream().map(Condition::toWire).collect(Collectors.toList());
+ }
+
+ wireRule.value = valueToApply.map(RawFlag::asJsonNode).orElse(null);
+
+ return wireRule;
+ }
+
+ public static Rule fromWire(WireRule wireRule) {
+ List<Condition> conditions = wireRule.andConditions == null ?
+ Collections.emptyList() :
+ wireRule.andConditions.stream().map(Condition::fromWire).collect(Collectors.toList());
+ Optional<RawFlag> value = wireRule.value == null ? Optional.empty() : Optional.of(JsonNodeRawFlag.fromJsonNode(wireRule.value));
+ return new Rule(value, conditions);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java b/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java
new file mode 100644
index 00000000000..6fbe3d587c5
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java
new file mode 100644
index 00000000000..e2cb6dd0932
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/DimensionHelper.java
@@ -0,0 +1,48 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.yahoo.vespa.flags.FetchVector;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class DimensionHelper {
+ private static Map<FetchVector.Dimension, String> serializedDimensions = new HashMap<>();
+ static {
+ serializedDimensions.put(FetchVector.Dimension.ZONE_ID, "zone");
+ serializedDimensions.put(FetchVector.Dimension.HOSTNAME, "hostname");
+ serializedDimensions.put(FetchVector.Dimension.APPLICATION_ID, "application");
+
+ if (serializedDimensions.size() != FetchVector.Dimension.values().length) {
+ throw new IllegalStateException(FetchVectorHelper.class.getName() + " is not in sync with " +
+ FetchVector.Dimension.class.getName());
+ }
+ }
+
+ private static Map<String, FetchVector.Dimension> deserializedDimensions = serializedDimensions.
+ entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
+
+ public static String toWire(FetchVector.Dimension dimension) {
+ String serializedDimension = serializedDimensions.get(dimension);
+ if (serializedDimension == null) {
+ throw new IllegalArgumentException("Unsupported dimension (please add it): '" + dimension + "'");
+ }
+
+ return serializedDimension;
+ }
+
+ public static FetchVector.Dimension fromWire(String serializedDimension) {
+ FetchVector.Dimension dimension = deserializedDimensions.get(serializedDimension);
+ if (dimension == null) {
+ throw new IllegalArgumentException("Unknown serialized dimension: '" + serializedDimension + "'");
+ }
+
+ return dimension;
+ }
+
+ private DimensionHelper() { }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java
new file mode 100644
index 00000000000..834e4024a6a
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/FetchVectorHelper.java
@@ -0,0 +1,27 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.yahoo.vespa.flags.FetchVector;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author hakonhall
+ */
+public class FetchVectorHelper {
+ public static Map<String, String> toWire(FetchVector vector) {
+ Map<FetchVector.Dimension, String> map = vector.toMap();
+ if (map.isEmpty()) return null;
+ return map.entrySet().stream().collect(Collectors.toMap(
+ entry -> DimensionHelper.toWire(entry.getKey()),
+ Map.Entry::getValue));
+ }
+
+ public static FetchVector fromWire(Map<String, String> wireMap) {
+ if (wireMap == null) return new FetchVector();
+ return FetchVector.fromMap(wireMap.entrySet().stream().collect(Collectors.toMap(
+ entry -> DimensionHelper.fromWire(entry.getKey()),
+ Map.Entry::getValue)));
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
new file mode 100644
index 00000000000..31aa6200fdb
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireCondition.java
@@ -0,0 +1,19 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Set;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireCondition {
+ @JsonProperty("type") public String type;
+ @JsonProperty("dimension") public String dimension;
+ @JsonProperty("values") public Set<String> values;
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java
new file mode 100644
index 00000000000..b4a000d7b70
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireFlagData.java
@@ -0,0 +1,55 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireFlagData {
+ @JsonProperty("rules") public List<WireRule> rules;
+ @JsonProperty("attributes") public Map<String, String> defaultFetchVector;
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ public byte[] serializeToBytes() {
+ return uncheck(() -> mapper.writeValueAsBytes(this));
+ }
+
+ public String serializeToJson() {
+ return uncheck(() -> mapper.writeValueAsString(this));
+ }
+
+ public JsonNode serializeToJsonNode() {
+ return uncheck(() -> mapper.valueToTree(this));
+ }
+
+ public void serializeToOutputStream(OutputStream outputStream) {
+ uncheck(() -> mapper.writeValue(outputStream, this));
+ }
+
+ public static WireFlagData deserialize(byte[] bytes) {
+ return uncheck(() -> mapper.readValue(bytes, WireFlagData.class));
+ }
+
+ public static WireFlagData deserialize(String string) {
+ return uncheck(() -> mapper.readValue(string, WireFlagData.class));
+ }
+
+ public static WireFlagData deserialize(InputStream inputStream) {
+ return uncheck(() -> mapper.readValue(inputStream, WireFlagData.class));
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java
new file mode 100644
index 00000000000..38619e87488
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/json/wire/WireRule.java
@@ -0,0 +1,19 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json.wire;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.List;
+
+/**
+ * @author hakonhall
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WireRule {
+ @JsonProperty("conditions") public List<WireCondition> andConditions;
+ @JsonProperty("value") public JsonNode value;
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
index 9e7508706ed..7d9d7868308 100644
--- a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
@@ -5,9 +5,11 @@ import com.yahoo.vespa.test.file.TestFileSystem;
import org.junit.Test;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Optional;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
@@ -22,18 +24,18 @@ public class FileFlagSourceTest {
@Test
public void testFeatureLikeFlags() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
- FeatureFlag byDefaultTrue = new FeatureFlag(id, true, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
+ Flag<Boolean> byDefaultTrue = new Flag<>(id, true, source, Flags.BOOLEAN_SERIALIZER);
assertFalse(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "True\n");
+ writeFlag(id.toString(), "true\n");
assertTrue(featureFlag.value());
assertTrue(byDefaultTrue.value());
- writeFlag(id.toString(), "False\n");
+ writeFlag(id.toString(), "false\n");
assertFalse(featureFlag.value());
assertFalse(byDefaultTrue.value());
@@ -41,35 +43,48 @@ public class FileFlagSourceTest {
@Test
public void testIntegerLikeFlags() throws IOException {
- StringFlag stringFlag = new StringFlag(id, "default", source);
- IntFlag intFlag = new IntFlag(id, -1, source);
- LongFlag longFlag = new LongFlag(id, -2L, source);
+ Flag<Integer> intFlag = new Flag<>(id, -1, source, Flags.INT_SERIALIZER);
+ Flag<Long> longFlag = new Flag<>(id, -2L, source, Flags.LONG_SERIALIZER);
- assertFalse(source.getString(id).isPresent());
- assertEquals("default", stringFlag.value());
- assertEquals(-1, intFlag.value());
- assertEquals(-2L, longFlag.value());
+ assertFalse(fetch().isPresent());
+ assertFalse(fetch().isPresent());
+ assertEquals(-1, (int) intFlag.value());
+ assertEquals(-2L, (long) longFlag.value());
writeFlag(id.toString(), "1\n");
- assertTrue(source.getString(id).isPresent());
+ assertTrue(fetch().isPresent());
+ assertTrue(fetch().isPresent());
+ assertEquals(1, (int) intFlag.value());
+ assertEquals(1L, (long) longFlag.value());
+ }
+
+ @Test
+ public void testStringFlag() throws IOException {
+ Flag<String> stringFlag = new Flag<>(id, "default", source, Flags.STRING_SERIALIZER);
+ assertFalse(fetch().isPresent());
+ assertEquals("default", stringFlag.value());
+
+ writeFlag(id.toString(), "\"1\\n\"\n");
assertEquals("1\n", stringFlag.value());
- assertEquals(1, intFlag.value());
- assertEquals(1L, longFlag.value());
}
@Test
public void parseFailure() throws IOException {
- FeatureFlag featureFlag = new FeatureFlag(id, false, source);
+ Flag<Boolean> featureFlag = new Flag<>(id, false, source, Flags.BOOLEAN_SERIALIZER);
writeFlag(featureFlag.id().toString(), "garbage");
try {
featureFlag.value();
- } catch (IllegalArgumentException e) {
+ } catch (UncheckedIOException e) {
assertThat(e.getMessage(), containsString("garbage"));
}
}
+ private Optional<RawFlag> fetch() {
+ return source.fetch(id, new FetchVector());
+ }
+
private void writeFlag(String flagId, String value) throws IOException {
Path featurePath = fileSystem.getPath(FileFlagSource.FLAGS_DIRECTORY).resolve(flagId);
Files.createDirectories(featurePath.getParent());
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
new file mode 100644
index 00000000000..4f7d797e07d
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FlagsTest.java
@@ -0,0 +1,131 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class FlagsTest {
+ @Test
+ public void testBoolean() {
+ final boolean defaultValue = false;
+ FlagSource source = mock(FlagSource.class);
+ Flag<Boolean> booleanFlag = Flags.defineBoolean("id", defaultValue, "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME)
+ .with(FetchVector.Dimension.ZONE_ID, "a-zone")
+ .bindTo(source);
+ assertThat(booleanFlag.id().toString(), equalTo("id"));
+
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.empty());
+ // default value without raw flag
+ assertThat(booleanFlag.value(), equalTo(defaultValue));
+
+ ArgumentCaptor<FetchVector> vector = ArgumentCaptor.forClass(FetchVector.class);
+ verify(source).fetch(any(), vector.capture());
+ // hostname is set by default
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).isPresent(), is(true));
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.HOSTNAME).get(), is(not(emptyOrNullString())));
+ // zone is set because it was set on the unbound flag above
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.ZONE_ID), is(Optional.of("a-zone")));
+ // application is not set
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.empty()));
+
+ RawFlag rawFlag = mock(RawFlag.class);
+ when(source.fetch(eq(new FlagId("id")), any())).thenReturn(Optional.of(rawFlag));
+ when(rawFlag.asJsonNode()).thenReturn(BooleanNode.getTrue());
+
+ // raw flag deserializes to true
+ assertThat(booleanFlag.with(FetchVector.Dimension.APPLICATION_ID, "an-app").value(), equalTo(true));
+
+ verify(source, times(2)).fetch(any(), vector.capture());
+ // application was set on the (bound) flag.
+ assertThat(vector.getValue().getValue(FetchVector.Dimension.APPLICATION_ID), is(Optional.of("an-app")));
+ }
+
+ @Test
+ public void testString() {
+ testGeneric(Flags.defineString("string-id", "default value", "description",
+ "modification effect", FetchVector.Dimension.ZONE_ID, FetchVector.Dimension.HOSTNAME),
+ "default value", "other value");
+ }
+
+ @Test
+ public void testInt() {
+ testGeneric(Flags.defineInt("int-id", 2, "desc", "mod"), 2, 3);
+ }
+
+ @Test
+ public void testLong() {
+ testGeneric(Flags.defineLong("long-id", 1L, "desc", "mod"), 1L, 2L);
+ }
+
+ @Test
+ public void testJacksonClass() {
+ ExampleJacksonClass defaultInstance = new ExampleJacksonClass();
+ ExampleJacksonClass instance = new ExampleJacksonClass();
+ instance.integer = -2;
+ instance.string = "foo";
+
+ testGeneric(Flags.defineJackson("jackson-id", ExampleJacksonClass.class, defaultInstance,
+ "description", "modification effect", FetchVector.Dimension.HOSTNAME),
+ defaultInstance, instance);
+ }
+
+ private <T> void testGeneric(UnboundFlag<T> unboundFlag, T defaultValue, T value) {
+ FlagSource source = mock(FlagSource.class);
+ Flag<T> flag = unboundFlag.bindTo(source);
+
+ when(source.fetch(any(), any())).thenReturn(Optional.empty());
+ assertThat(flag.value(), equalTo(defaultValue));
+
+ when(source.fetch(any(), any())).thenReturn(Optional.of(JsonNodeRawFlag.fromJacksonClass(value)));
+ assertThat(flag.value(), equalTo(value));
+
+ assertTrue(Flags.getFlag(unboundFlag.id()).isPresent());
+ }
+
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private static class ExampleJacksonClass {
+ @JsonProperty("integer")
+ public int integer = 1;
+
+ @JsonProperty("string")
+ public String string = "2";
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ExampleJacksonClass that = (ExampleJacksonClass) o;
+ return integer == that.integer &&
+ Objects.equals(string, that.string);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(integer, string);
+ }
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java b/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
deleted file mode 100644
index 8bd486f5b47..00000000000
--- a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.flags;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import org.junit.Test;
-
-import java.util.Objects;
-import java.util.Optional;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class JacksonFlagTest {
- private final FlagId id = new FlagId("id");
- private final ExampleJacksonClass defaultValue = new ExampleJacksonClass();
- private final FlagSource source = mock(FlagSource.class);
- private final JacksonFlag<ExampleJacksonClass> jacksonFlag = new JacksonFlag<>(id.toString(), ExampleJacksonClass.class, defaultValue, source);
-
- @Test
- public void unsetThenSet() {
- when(source.getString(id)).thenReturn(Optional.empty());
- ExampleJacksonClass value = jacksonFlag.value();
- assertEquals(1, value.integer);
- assertEquals("2", value.string);
- assertEquals("3", value.dummy);
-
- when(source.getString(id)).thenReturn(Optional.of("{\"integer\": 4, \"string\": \"foo\", \"stray\": 6}"));
- value = jacksonFlag.value();
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
-
- assertEquals(4, value.integer);
- assertEquals("foo", value.string);
- assertEquals("3", value.dummy);
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- private static class ExampleJacksonClass {
- @JsonProperty("integer")
- public int integer = 1;
-
- @JsonProperty("string")
- public String string = "2";
-
- @JsonProperty("dummy")
- public String dummy = "3";
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ExampleJacksonClass that = (ExampleJacksonClass) o;
- return integer == that.integer &&
- Objects.equals(string, that.string) &&
- Objects.equals(dummy, that.dummy);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(integer, string, dummy);
- }
- }
-} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.java
new file mode 100644
index 00000000000..5465f89e2eb
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/OrderedFlagSourceTest.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.flags;
+
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author hakonhall
+ */
+public class OrderedFlagSourceTest {
+ @Test
+ public void test() {
+ FlagSource source1 = mock(FlagSource.class);
+ FlagSource source2 = mock(FlagSource.class);
+ OrderedFlagSource orderedSource = new OrderedFlagSource(source1, source2);
+
+ FlagId id = new FlagId("id");
+ FetchVector vector = new FetchVector();
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertFalse(orderedSource.fetch(id, vector).isPresent());
+ verify(source1, times(1)).fetch(any(), any());
+ verify(source2, times(1)).fetch(any(), any());
+
+ RawFlag rawFlag = mock(RawFlag.class);
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.empty());
+ when(source2.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(2)).fetch(any(), any());
+ verify(source2, times(2)).fetch(any(), any());
+
+ when(source1.fetch(any(), any())).thenReturn(Optional.of(rawFlag));
+ when(source2.fetch(any(), any())).thenReturn(Optional.empty());
+ assertEquals(orderedSource.fetch(id, vector), Optional.of(rawFlag));
+ verify(source1, times(3)).fetch(any(), any());
+ // Not invoked as source1 provided raw flag
+ verify(source2, times(2)).fetch(any(), any());
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
new file mode 100644
index 00000000000..96cbce71fa8
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/ConditionTest.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import org.junit.Test;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class ConditionTest {
+ @Test
+ public void testWhitelist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.WHITELIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertFalse(condition.test(new FetchVector()));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+
+ @Test
+ public void testBlacklist() {
+ String hostname1 = "host1";
+ Condition condition = new Condition(Condition.Type.BLACKLIST, FetchVector.Dimension.HOSTNAME,
+ Stream.of(hostname1).collect(Collectors.toSet()));
+ assertTrue(condition.test(new FetchVector()));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.APPLICATION_ID, "foo")));
+ assertTrue(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, "bar")));
+ assertFalse(condition.test(new FetchVector().with(FetchVector.Dimension.HOSTNAME, hostname1)));
+ }
+}
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
new file mode 100644
index 00000000000..2eb12e53ddc
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/FlagDataTest.java
@@ -0,0 +1,82 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.yahoo.vespa.flags.FetchVector;
+import com.yahoo.vespa.flags.RawFlag;
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author hakonhall
+ */
+public class FlagDataTest {
+ private final String json = "{\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" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"values\": [ \"zone1\", \"zone2\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": false\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"zone1\"\n" +
+ " }\n" +
+ "}";
+
+ private final FetchVector vector = new FetchVector();
+
+ @Test
+ public void test() {
+ // Second rule matches with the default zone matching
+ verify(Optional.of("false"), vector);
+
+ // First rule matches only if both conditions match
+ verify(Optional.of("false"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app2"));
+ verify(Optional.of("true"), vector
+ .with(FetchVector.Dimension.HOSTNAME, "host1")
+ .with(FetchVector.Dimension.APPLICATION_ID, "app3"));
+
+ // No rules apply if zone is overridden to an unknown zone
+ verify(Optional.empty(), vector.with(FetchVector.Dimension.ZONE_ID, "unknown zone"));
+ }
+
+ private void verify(Optional<String> expectedValue, FetchVector vector) {
+ FlagData data = FlagData.deserialize(json);
+ Optional<RawFlag> rawFlag = data.resolve(vector);
+
+ if (expectedValue.isPresent()) {
+ assertTrue(rawFlag.isPresent());
+ assertEquals(expectedValue.get(), rawFlag.get().asJson());
+ } else {
+ assertFalse(rawFlag.isPresent());
+ }
+
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
new file mode 100644
index 00000000000..f3f8c147212
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/json/SerializationTest.java
@@ -0,0 +1,130 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.flags.json;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeType;
+import com.yahoo.vespa.flags.json.wire.WireCondition;
+import com.yahoo.vespa.flags.json.wire.WireFlagData;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+/**
+ * @author hakonhall
+ */
+public class SerializationTest {
+ @Test
+ public void emptyJson() throws IOException {
+ String json = "{}";
+ WireFlagData wireData = WireFlagData.deserialize(json);
+ assertThat(wireData.defaultFetchVector, nullValue());
+ assertThat(wireData.rules, nullValue());
+ assertThat(wireData.serializeToJson(), equalTo(json));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{}"));
+ }
+
+ @Test
+ public void deserialization() throws IOException {
+ String json = "{\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"application\",\n" +
+ " \"values\": [ \"a1\", \"a2\" ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"blacklist\",\n" +
+ " \"dimension\": \"hostname\",\n" +
+ " \"values\": [ \"h1\" ]\n" +
+ " }\n" +
+ " ],\n" +
+ " \"value\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " \"zone\": \"z1\",\n" +
+ " \"application\": \"a1\",\n" +
+ " \"hostname\": \"h1\"\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ // rule
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(2));
+ assertThat(wireData.rules.get(0).value.getNodeType(), equalTo(JsonNodeType.BOOLEAN));
+ assertThat(wireData.rules.get(0).value.asBoolean(), equalTo(true));
+ // first condition
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("application"));
+ assertThat(whitelistCondition.values, equalTo(new HashSet<>(Arrays.asList("a1", "a2"))));
+ // second condition
+ WireCondition blacklistCondition = wireData.rules.get(0).andConditions.get(1);
+ assertThat(blacklistCondition.type, equalTo("blacklist"));
+ assertThat(blacklistCondition.dimension, equalTo("hostname"));
+ assertThat(blacklistCondition.values, equalTo(new HashSet<>(Arrays.asList("h1"))));
+
+ // attributes
+ assertThat(wireData.defaultFetchVector, notNullValue());
+ assertThat(wireData.defaultFetchVector.get("zone"), equalTo("z1"));
+ assertThat(wireData.defaultFetchVector.get("application"), equalTo("a1"));
+ assertThat(wireData.defaultFetchVector.get("hostname"), equalTo("h1"));
+
+ // Verify serialization of RawFlag == serialization by ObjectMapper
+ ObjectMapper mapper = new ObjectMapper();
+ String serializedWithObjectMapper = mapper.writeValueAsString(mapper.readTree(json));
+ assertThat(wireData.serializeToJson(), equalTo(serializedWithObjectMapper));
+
+ // Unfortunately the order of attributes members are different...
+ // assertThat(FlagData.deserialize(json).serializeToJson(), equalTo(serializedWithObjectMapper));
+ }
+
+ @Test
+ public void jsonWithStrayFields() {
+ String json = "{\n" +
+ " \"foo\": true,\n" +
+ " \"rules\": [\n" +
+ " {\n" +
+ " \"conditions\": [\n" +
+ " {\n" +
+ " \"type\": \"whitelist\",\n" +
+ " \"dimension\": \"zone\",\n" +
+ " \"bar\": \"zoo\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"other\": true\n" +
+ " }\n" +
+ " ],\n" +
+ " \"attributes\": {\n" +
+ " }\n" +
+ "}";
+
+ WireFlagData wireData = WireFlagData.deserialize(json);
+
+ assertThat(wireData.rules.size(), equalTo(1));
+ assertThat(wireData.rules.get(0).andConditions.size(), equalTo(1));
+ WireCondition whitelistCondition = wireData.rules.get(0).andConditions.get(0);
+ assertThat(whitelistCondition.type, equalTo("whitelist"));
+ assertThat(whitelistCondition.dimension, equalTo("zone"));
+ assertThat(whitelistCondition.values, nullValue());
+ assertThat(wireData.rules.get(0).value, nullValue());
+ assertThat(wireData.defaultFetchVector, anEmptyMap());
+
+ assertThat(wireData.serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}],\"attributes\":{}}"));
+
+ assertThat(FlagData.deserialize(json).serializeToJson(), equalTo("{\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"zone\"}]}]}"));
+ }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index bb74e6a5f24..168e5ec8d7e 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -104,15 +104,15 @@
<showWarnings>true</showWarnings>
<optimize>true</optimize>
<showDeprecation>false</showDeprecation>
- <compilerArgs>
- <arg>-Xlint:all</arg>
- <arg>-Xlint:-serial</arg>
- <arg>-Xlint:-try</arg>
- <arg>-Xlint:-processing</arg>
- <arg>-Xlint:-varargs</arg>
+ <compilerArgs>
+ <arg>-Xlint:all</arg>
+ <arg>-Xlint:-serial</arg>
+ <arg>-Xlint:-try</arg>
+ <arg>-Xlint:-processing</arg>
+ <arg>-Xlint:-varargs</arg>
<arg>-Xlint:-options</arg>
- <arg>-Werror</arg>
- </compilerArgs>
+ <arg>-Werror</arg>
+ </compilerArgs>
</configuration>
</plugin>
<plugin>
@@ -404,11 +404,11 @@
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
- <dependency>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity</artifactId>
- <version>1.7</version>
- </dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ <version>1.7</version>
+ </dependency>
<dependency>
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
@@ -723,7 +723,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<test.hide>true</test.hide>
- <doclint>all</doclint>
+ <doclint>all</doclint>
<surefire.version>2.21.0</surefire.version>
</properties>
diff --git a/pom.xml b/pom.xml
index 68b0f1e2f9a..c3ad8fe9320 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,7 @@
<module>config-provisioning</module>
<module>config-proxy</module>
<module>configserver</module>
+ <module>configserver-flags</module>
<module>config_test</module>
<module>container</module>
<module>container-core</module>
diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/duper/DuperModelManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/duper/DuperModelManager.java
index e0e2b64bdae..57c206ee570 100644
--- a/service-monitor/src/main/java/com/yahoo/vespa/service/duper/DuperModelManager.java
+++ b/service-monitor/src/main/java/com/yahoo/vespa/service/duper/DuperModelManager.java
@@ -10,8 +10,8 @@ import com.yahoo.config.model.api.SuperModelProvider;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.flags.FeatureFlag;
import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.service.monitor.DuperModelInfraApi;
import com.yahoo.vespa.service.monitor.InfraApplicationApi;
@@ -59,12 +59,8 @@ public class DuperModelManager implements DuperModelInfraApi {
@Inject
public DuperModelManager(ConfigserverConfig configServerConfig, FlagSource flagSource, SuperModelProvider superModelProvider) {
this(
- // Whether to include activate infrastructure applications (except from controller/config apps - see below).
- new FeatureFlag("dupermodel-contains-infra", true, flagSource).value(),
- // For historical reasons, the ApplicationInfo in the DuperModel for controllers and config servers
- // is based on the ConfigserverConfig (this flag is true). We want to transition to use the
- // infrastructure application activated by the InfrastructureProvisioner once that supports health.
- new FeatureFlag("dupermodel-use-configserverconfig", true, flagSource).value(),
+ Flags.DUPERMODEL_CONTAINS_INFRA.bindTo(flagSource).value(),
+ Flags.DUPERMODEL_USE_CONFIGSERVERCONFIG.bindTo(flagSource).value(),
configServerConfig.multitenant(),
configServerApplication.makeApplicationInfoFromConfig(configServerConfig),
superModelProvider,
diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/health/HealthMonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/health/HealthMonitorManager.java
index 2ad37faf593..340d30e9f41 100644
--- a/service-monitor/src/main/java/com/yahoo/vespa/service/health/HealthMonitorManager.java
+++ b/service-monitor/src/main/java/com/yahoo/vespa/service/health/HealthMonitorManager.java
@@ -8,8 +8,9 @@ import com.yahoo.vespa.applicationmodel.ClusterId;
import com.yahoo.vespa.applicationmodel.ConfigId;
import com.yahoo.vespa.applicationmodel.ServiceStatus;
import com.yahoo.vespa.applicationmodel.ServiceType;
-import com.yahoo.vespa.flags.FeatureFlag;
-import com.yahoo.vespa.flags.FileFlagSource;
+import com.yahoo.vespa.flags.Flag;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.service.duper.DuperModelManager;
import com.yahoo.vespa.service.duper.ZoneApplication;
import com.yahoo.vespa.service.executor.RunletExecutorImpl;
@@ -49,23 +50,23 @@ public class HealthMonitorManager implements MonitorManager {
private final ConcurrentHashMap<ApplicationId, ApplicationHealthMonitor> healthMonitors = new ConcurrentHashMap<>();
private final DuperModelManager duperModel;
private final ApplicationHealthMonitorFactory applicationHealthMonitorFactory;
- private final FeatureFlag monitorInfra;
+ private final Flag<Boolean> monitorInfra;
@Inject
- public HealthMonitorManager(DuperModelManager duperModel, FileFlagSource flagSource) {
+ public HealthMonitorManager(DuperModelManager duperModel, FlagSource flagSource) {
this(duperModel,
- new FeatureFlag("healthmonitor-monitorinfra", true, flagSource),
+ Flags.HEALTHMONITOR_MONITOR_INFRA.bindTo(flagSource),
new StateV1HealthModel(TARGET_HEALTH_STALENESS, HEALTH_REQUEST_TIMEOUT, KEEP_ALIVE, new RunletExecutorImpl(THREAD_POOL_SIZE)));
}
private HealthMonitorManager(DuperModelManager duperModel,
- FeatureFlag monitorInfra,
+ Flag<Boolean> monitorInfra,
StateV1HealthModel healthModel) {
this(duperModel, monitorInfra, id -> new ApplicationHealthMonitor(id, healthModel));
}
HealthMonitorManager(DuperModelManager duperModel,
- FeatureFlag monitorInfra,
+ Flag<Boolean> monitorInfra,
ApplicationHealthMonitorFactory applicationHealthMonitorFactory) {
this.duperModel = duperModel;
this.monitorInfra = monitorInfra;
diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/health/HealthMonitorManagerTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/health/HealthMonitorManagerTest.java
index 86b0ee4a8f3..39147955d4c 100644
--- a/service-monitor/src/test/java/com/yahoo/vespa/service/health/HealthMonitorManagerTest.java
+++ b/service-monitor/src/test/java/com/yahoo/vespa/service/health/HealthMonitorManagerTest.java
@@ -7,7 +7,7 @@ import com.yahoo.vespa.applicationmodel.ClusterId;
import com.yahoo.vespa.applicationmodel.ConfigId;
import com.yahoo.vespa.applicationmodel.ServiceStatus;
import com.yahoo.vespa.applicationmodel.ServiceType;
-import com.yahoo.vespa.flags.FeatureFlag;
+import com.yahoo.vespa.flags.Flag;
import com.yahoo.vespa.service.duper.ConfigServerApplication;
import com.yahoo.vespa.service.duper.ControllerHostApplication;
import com.yahoo.vespa.service.duper.DuperModelManager;
@@ -33,7 +33,8 @@ import static org.mockito.Mockito.when;
public class HealthMonitorManagerTest {
private final ConfigServerApplication configServerApplication = new ConfigServerApplication();
private final DuperModelManager duperModel = mock(DuperModelManager.class);
- private final FeatureFlag monitorInfra = mock(FeatureFlag.class);
+ @SuppressWarnings("unchecked")
+ private final Flag<Boolean> monitorInfra = (Flag<Boolean>) mock(Flag.class);
private final ApplicationHealthMonitor monitor = mock(ApplicationHealthMonitor.class);
private final ApplicationHealthMonitorFactory monitorFactory = mock(ApplicationHealthMonitorFactory.class);
private final HealthMonitorManager manager = new HealthMonitorManager(duperModel, monitorInfra, monitorFactory);