summaryrefslogtreecommitdiffstats
path: root/flags
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-11-23 17:30:13 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-11-23 17:30:13 +0100
commitcadcac9a8c0501f86372eb05d107d7b089643d0a (patch)
tree9593cc78763cba8e790e49c21f28a19182002765 /flags
parentf67aa7bfa1553d8cc19ce4eef96f42ff8c31a320 (diff)
Add flags module
FileFlagSource reads flags from files in /etc/vespa/flags and is a component that can be injected in host admin, config server, etc. A flag named foo corresponds to filename foo. In general a FlagSource manages: - Feature flags: A feature is either set (true/enabled) or otherwise false. Touching a file foo means the feature flag foo is set (true). - Value flags: Either a String or empty if not set. The String corresponds to the file content. The plan is to make the config server another source of flags. A unified FlagSource can merge the two sources with some priority and used in e.g. parts of node-admin. In other parts one would only have access to the file source. Defines various flag facades: - FeatureFlag: Used to test whether a feature has been enabled or not. - IntFlag - JacksonFlag: Deserializes JSON to Jackson class, or return default if unset. - LongFlag - OptionalJacksonFlag: Deserializes JSON to Jackson class, or empty if unset. - OptionalStringFlag - StringFlag This is part of removing some of the last Chef recipes. Some minor tweaks have been necessary as part of this and are included in this PR (test whether a systemd service exists, task-friendly file deletion, allow capitalized letters in YUM package name).
Diffstat (limited to 'flags')
-rw-r--r--flags/pom.xml66
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java48
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java67
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/Flag.java9
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagId.java42
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java15
-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/LongFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/OptionalJacksonFlag.java60
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/OptionalStringFlag.java53
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java49
-rw-r--r--flags/src/main/java/com/yahoo/vespa/flags/package-info.java5
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java49
-rw-r--r--flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java72
15 files changed, 691 insertions, 0 deletions
diff --git a/flags/pom.xml b/flags/pom.xml
new file mode 100644
index 00000000000..57c57f907d3
--- /dev/null
+++ b/flags/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!-- Copyright 2018 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>flags</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>container-plugin</packaging>
+ <name>${project.artifactId}</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>defaults</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-junit</artifactId>
+ <version>2.0.0.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>testutil</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.java
new file mode 100644
index 00000000000..957faee41cc
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FeatureFlag.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 java.util.function.Function;
+
+/**
+ * A FeatureFlag is true only if set (enabled).
+ *
+ * @author hakonhall
+ */
+public class FeatureFlag implements Flag {
+ private final FlagId id;
+ private final FlagSource source;
+
+ public static Function<FlagSource, FeatureFlag> createUnbound(String flagId) {
+ return createUnbound(new FlagId(flagId));
+ }
+
+ public static Function<FlagSource, FeatureFlag> createUnbound(FlagId flagId) {
+ return source -> new FeatureFlag(flagId, source);
+ }
+
+ public FeatureFlag(String flagId, FlagSource source) {
+ this(new FlagId(flagId), source);
+ }
+
+ public FeatureFlag(FlagId id, FlagSource source) {
+ this.id = id;
+ this.source = source;
+ }
+
+ @Override
+ public FlagId id() {
+ return id;
+ }
+
+ public boolean isSet() {
+ return source.hasFeature(id);
+ }
+
+ @Override
+ public String toString() {
+ return "FeatureFlag{" +
+ "id=" + id +
+ ", source=" + source +
+ '}';
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
new file mode 100644
index 00000000000..f13a122b156
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FileFlagSource.java
@@ -0,0 +1,67 @@
+// 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.google.common.util.concurrent.UncheckedTimeoutException;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+/**
+ * A {@link FlagSource} backed by local files.
+ *
+ * @author hakonhall
+ */
+public class FileFlagSource implements FlagSource {
+ static final String FLAGS_DIRECTORY = "/etc/vespa/flags";
+
+ private final Path flagsDirectory;
+
+ @Inject
+ public FileFlagSource() {
+ this(FileSystems.getDefault());
+ }
+
+ public FileFlagSource(FileSystem fileSystem) {
+ this(fileSystem.getPath(FLAGS_DIRECTORY));
+ }
+
+ public FileFlagSource(Path flagsDirectory) {
+ this.flagsDirectory = flagsDirectory;
+ }
+
+ @Override
+ public boolean hasFeature(FlagId id) {
+ return Files.exists(getPath(id));
+ }
+
+ @Override
+ public Optional<String> getString(FlagId id) {
+ return getBytes(id).map(bytes -> new String(bytes, StandardCharsets.UTF_8));
+ }
+
+ public Optional<byte[]> getBytes(FlagId id) {
+ try {
+ return Optional.of(Files.readAllBytes(getPath(id)));
+ } catch (NoSuchFileException e) {
+ return Optional.empty();
+ } catch (IOException e) {
+ throw new UncheckedTimeoutException(e);
+ }
+ }
+
+ private Path getPath(FlagId id) {
+ return flagsDirectory.resolve(id.toString());
+ }
+
+ @Override
+ public String toString() {
+ return "FileFlagSource{" + flagsDirectory + '}';
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flag.java b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
new file mode 100644
index 00000000000..831e0d0dab9
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/Flag.java
@@ -0,0 +1,9 @@
+// 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 Flag {
+ FlagId id();
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
new file mode 100644
index 00000000000..f004df063ed
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagId.java
@@ -0,0 +1,42 @@
+// 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.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * @author hakonhall
+ */
+@Immutable
+public class FlagId {
+ private static final Pattern ID_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$");
+
+ private final String id;
+
+ public FlagId(String id) {
+ if (!ID_PATTERN.matcher(id).find()) {
+ throw new IllegalArgumentException("Not a valid FlagId: '" + id + "'");
+ }
+
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ FlagId flagId = (FlagId) o;
+ return Objects.equals(id, flagId.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
new file mode 100644
index 00000000000..56f4b5ee0ae
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/FlagSource.java
@@ -0,0 +1,15 @@
+// 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.Optional;
+
+/**
+ * @author hakonhall
+ */
+public interface FlagSource {
+ /** Whether the source has the feature flag with the given id. */
+ boolean hasFeature(FlagId id);
+
+ /** The String value of a flag. */
+ Optional<String> getString(FlagId id);
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
new file mode 100644
index 00000000000..f7c9645c5db
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/IntFlag.java
@@ -0,0 +1,49 @@
+// 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
new file mode 100644
index 00000000000..99add358e75
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/JacksonFlag.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;
+
+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/LongFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
new file mode 100644
index 00000000000..d60dc7b5adc
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/LongFlag.java
@@ -0,0 +1,49 @@
+// 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/OptionalJacksonFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/OptionalJacksonFlag.java
new file mode 100644
index 00000000000..9b25a5d6786
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/OptionalJacksonFlag.java
@@ -0,0 +1,60 @@
+// 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.Optional;
+import java.util.function.Function;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * @author hakonhall
+ */
+public class OptionalJacksonFlag<T> implements Flag {
+ private final static ObjectMapper mapper = new ObjectMapper();
+
+ private final FlagId id;
+ private final Class<T> jacksonClass;
+ private final FlagSource source;
+
+ public static <T> Function<FlagSource, OptionalJacksonFlag<T>> createUnbound(String flagId, Class<T> jacksonClass) {
+ return createUnbound(new FlagId(flagId), jacksonClass);
+ }
+
+ public static <T> Function<FlagSource, OptionalJacksonFlag<T>> createUnbound(FlagId id, Class<T> jacksonClass) {
+ return source -> new OptionalJacksonFlag<>(id, jacksonClass, source);
+ }
+
+ public OptionalJacksonFlag(String flagId, Class<T> jacksonClass, FlagSource source) {
+ this(new FlagId(flagId), jacksonClass, source);
+ }
+
+ public OptionalJacksonFlag(FlagId id, Class<T> jacksonClass, FlagSource source) {
+ this.id = id;
+ this.jacksonClass = jacksonClass;
+ this.source = source;
+ }
+
+ @Override
+ public FlagId id() {
+ return id;
+ }
+
+ public Optional<T> value() {
+ return source.getString(id).map(string -> uncheck(() -> mapper.readValue(string, jacksonClass)));
+ }
+
+ public JacksonFlag<T> withDefault(T defaultValue) {
+ return new JacksonFlag<T>(id, jacksonClass, defaultValue, source);
+ }
+
+ @Override
+ public String toString() {
+ return "OptionalJacksonFlag{" +
+ "id=" + id +
+ ", jacksonClass=" + jacksonClass +
+ ", source=" + source +
+ '}';
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/OptionalStringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/OptionalStringFlag.java
new file mode 100644
index 00000000000..3e25b08cf9d
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/OptionalStringFlag.java
@@ -0,0 +1,53 @@
+// 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.Optional;
+import java.util.function.Function;
+
+/**
+ * An OptionalStringFlag is a flag which is either not set (empty), or set (String present).
+ *
+ * @author hakonhall
+ */
+public class OptionalStringFlag implements Flag {
+ private final FlagId id;
+ private final FlagSource source;
+
+ public static Function<FlagSource, OptionalStringFlag> createUnbound(String flagId) {
+ return createUnbound(new FlagId(flagId));
+ }
+
+ public static Function<FlagSource, OptionalStringFlag> createUnbound(FlagId id) {
+ return source -> new OptionalStringFlag(id, source);
+ }
+
+ public OptionalStringFlag(String flagId, FlagSource source) {
+ this(new FlagId(flagId), source);
+ }
+
+ public OptionalStringFlag(FlagId id, FlagSource source) {
+ this.id = id;
+ this.source = source;
+ }
+
+ @Override
+ public FlagId id() {
+ return id;
+ }
+
+ public StringFlag bindDefault(String defaultValue) {
+ return new StringFlag(id, defaultValue, source);
+ }
+
+ public Optional<String> value() {
+ return source.getString(id);
+ }
+
+ @Override
+ public String toString() {
+ return "OptionalStringFlag{" +
+ "id=" + id +
+ ", source=" + source +
+ '}';
+ }
+}
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
new file mode 100644
index 00000000000..8226e999238
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/StringFlag.java
@@ -0,0 +1,49 @@
+// 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/package-info.java b/flags/src/main/java/com/yahoo/vespa/flags/package-info.java
new file mode 100644
index 00000000000..42b9b057b29
--- /dev/null
+++ b/flags/src/main/java/com/yahoo/vespa/flags/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;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
new file mode 100644
index 00000000000..e422057f5fe
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/FileFlagSourceTest.java
@@ -0,0 +1,49 @@
+// 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.yahoo.vespa.test.file.TestFileSystem;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FileFlagSourceTest {
+ private final FileSystem fileSystem = TestFileSystem.create();
+ private final FileFlagSource source = new FileFlagSource(fileSystem);
+
+ @Test
+ public void absentThenSet() throws IOException {
+ FlagId id = new FlagId("foo");
+ FeatureFlag featureFlag = new FeatureFlag(id, source);
+ StringFlag stringFlag = new StringFlag(id, "default", source);
+ OptionalStringFlag optionalStringFlag = new OptionalStringFlag(id, source);
+ IntFlag intFlag = new IntFlag(id, -1, source);
+ LongFlag longFlag = new LongFlag(id, -2L, source);
+
+ assertFalse(source.hasFeature(id));
+ assertFalse(source.getString(id).isPresent());
+ assertFalse(featureFlag.isSet());
+ assertEquals("default", stringFlag.value());
+ assertFalse(optionalStringFlag.value().isPresent());
+ assertEquals(-1, intFlag.value());
+ assertEquals(-2L, longFlag.value());
+
+ Path featurePath = fileSystem.getPath(FileFlagSource.FLAGS_DIRECTORY).resolve(id.toString());
+ Files.createDirectories(featurePath.getParent());
+ Files.write(featurePath, "1\n".getBytes());
+
+ assertTrue(source.hasFeature(id));
+ assertTrue(source.getString(id).isPresent());
+ assertTrue(featureFlag.isSet());
+ assertEquals("1\n", stringFlag.value());
+ assertEquals("1\n", optionalStringFlag.value().get());
+ assertEquals(1, intFlag.value());
+ assertEquals(1L, longFlag.value());
+ }
+} \ No newline at end of file
diff --git a/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java b/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
new file mode 100644
index 00000000000..e4424d9886a
--- /dev/null
+++ b/flags/src/test/java/com/yahoo/vespa/flags/JacksonFlagTest.java
@@ -0,0 +1,72 @@
+// 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.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+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);
+ private final OptionalJacksonFlag<ExampleJacksonClass> optionalJacksonFlag = new OptionalJacksonFlag<>(id, ExampleJacksonClass.class, 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);
+ assertFalse(optionalJacksonFlag.value().isPresent());
+
+ 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);
+
+ assertTrue(optionalJacksonFlag.value().isPresent());
+ value = optionalJacksonFlag.value().get();
+ 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