summaryrefslogtreecommitdiffstats
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
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).
-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
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleter.java33
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java19
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java9
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtl.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java2
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleterTest.java27
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtlTest.java28
-rw-r--r--node-admin/vespa-node-admin.spec3
-rw-r--r--pom.xml1
24 files changed, 818 insertions, 12 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
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleter.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleter.java
new file mode 100644
index 00000000000..efb56be56c5
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleter.java
@@ -0,0 +1,33 @@
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+import static com.yahoo.yolean.Exceptions.uncheck;
+
+/**
+ * Deletes a file or empty directory.
+ *
+ * @author hakonhall
+ */
+public class FileDeleter {
+ private static final Logger logger = Logger.getLogger(FileDeleter.class.getName());
+
+ private final Path path;
+
+ public FileDeleter(Path path) {
+ this.path = path;
+ }
+
+ public boolean converge(TaskContext context) {
+ boolean deleted = uncheck(() -> Files.deleteIfExists(path));
+ if (deleted) {
+ context.recordSystemModification(logger, "Deleted file or directory " + path);
+ }
+
+ return deleted;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
index 4bd3aad3f52..1b927cfc682 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java
@@ -49,16 +49,6 @@ public class UnixPath {
return path;
}
- public boolean createParents() {
- Path parent = path.getParent();
- if (Files.isDirectory(parent)) {
- return false;
- }
-
- uncheck(() -> Files.createDirectories(parent));
- return true;
- }
-
public String readUtf8File() {
return new String(readBytes(), StandardCharsets.UTF_8);
}
@@ -141,6 +131,15 @@ public class UnixPath {
return this;
}
+ public UnixPath createParents() {
+ Path parent = path.getParent();
+ if (!Files.isDirectory(parent)) {
+ uncheck(() -> Files.createDirectories(parent));
+ }
+
+ return this;
+ }
+
public UnixPath createDirectory(String permissions) {
Set<PosixFilePermission> set = getPosixFilePermissionsFromString(permissions);
FileAttribute<Set<PosixFilePermission>> attribute = PosixFilePermissions.asFileAttribute(set);
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
index 44b7da9367b..940b3255766 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
@@ -180,6 +180,15 @@ public class CommandLine {
}
/**
+ * By default, a non-zero exit code causes the command execution to fail. This method
+ * will override that predicate.
+ */
+ public CommandLine setSuccessfulExitCodePredicate(Predicate<Integer> successPredicate) {
+ successfulExitCodePredicate = successPredicate;
+ return this;
+ }
+
+ /**
* By default, the output of the command is parsed as UTF-8. This method will set a
* different encoding.
*/
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtl.java
index b61ebb610af..77510f7b6ef 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtl.java
@@ -20,6 +20,9 @@ public class SystemCtl {
// Valid systemd property names from looking at a couple of services.
private static final Pattern PROPERTY_NAME_PATTERN = Pattern.compile("^[a-zA-Z]+$");
+ // Last line of `systemctl list-unit-files <unit>` prints '0 unit files listed.'
+ private static final Pattern UNIT_FILES_LISTED_PATTERN = Pattern.compile("([0-9]+) unit files listed\\.");
+
// Patterns matching properties output by the 'systemctl show' command.
private static final Pattern UNIT_FILE_STATE_PROPERTY_PATTERN = createPropertyPattern("UnitFileState");
private static final Pattern ACTIVE_STATE_PROPERTY_PATTERN = createPropertyPattern("ActiveState");
@@ -53,6 +56,20 @@ public class SystemCtl {
public SystemCtlStop stop(String unit) { return new SystemCtlStop(unit); }
public SystemCtlRestart restart(String unit) { return new SystemCtlRestart(unit); }
+ public boolean serviceExists(TaskContext context, String unit) {
+ return terminal.newCommandLine(context)
+ .add("systemctl").add("list-unit-files").add(unit + ".service").executeSilently()
+ .mapOutput(output -> {
+ // Last line of the form: "1 unit files listed."
+ Matcher matcher = UNIT_FILES_LISTED_PATTERN.matcher(output);
+ if (!matcher.find()) {
+ throw new IllegalArgumentException();
+ }
+
+ return !matcher.group(1).equals("0");
+ });
+ }
+
public class SystemCtlEnable extends SystemCtlCommand {
private SystemCtlEnable(String unit) {
super("enable", unit);
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java
index a3a154e2175..fb85815c70f 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/YumPackageName.java
@@ -43,7 +43,7 @@ public class YumPackageName {
private static final Pattern NAME_VER_REL_PATTERN = Pattern.compile("^((.+)-)?" +
"([a-z0-9._]*[0-9][a-z0-9._]*)-" + // ver contains at least one digit
"([a-z0-9._]*[0-9][a-z0-9._]*)$"); // rel contains at least one digit
- private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z0-9._-]+$");
+ private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9._-]+$");
private final Optional<String> epoch;
private final String name;
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleterTest.java
new file mode 100644
index 00000000000..3a60061344f
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileDeleterTest.java
@@ -0,0 +1,27 @@
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+import com.yahoo.vespa.test.file.TestFileSystem;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+public class FileDeleterTest {
+ private final FileSystem fileSystem = TestFileSystem.create();
+ private final UnixPath path = new UnixPath(fileSystem.getPath("/tmp/foo"));
+ private final FileDeleter deleter = new FileDeleter(path.toPath());
+ private final TaskContext context = mock(TaskContext.class);
+
+ @Test
+ public void deleteExisting() throws IOException {
+ assertFalse(deleter.converge(context));
+ path.createParents().writeUtf8File("bar");
+ assertTrue(deleter.converge(context));
+ assertFalse(deleter.converge(context));
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtlTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtlTest.java
index 315138e897f..fc794c3ab68 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtlTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/systemd/SystemCtlTest.java
@@ -6,7 +6,9 @@ import com.yahoo.vespa.hosted.node.admin.task.util.process.ChildProcessFailureEx
import com.yahoo.vespa.hosted.node.admin.task.util.process.TestTerminal;
import org.junit.Test;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
@@ -133,4 +135,30 @@ public class SystemCtlTest {
terminal.expectCommand("systemctl restart docker 2>&1", 0, "");
assertTrue(new SystemCtl(terminal).restart("docker").converge(taskContext));
}
+
+ @Test
+ public void testUnitExists() {
+ SystemCtl systemCtl = new SystemCtl(terminal);
+
+ terminal.expectCommand("systemctl list-unit-files foo.service 2>&1", 0,
+ "UNIT FILE STATE\n" +
+ "\n" +
+ "0 unit files listed.\n");
+ assertFalse(systemCtl.serviceExists(taskContext, "foo"));
+
+ terminal.expectCommand("systemctl list-unit-files foo.service 2>&1", 0,
+ "UNIT FILE STATE \n" +
+ "foo.service enabled\n" +
+ "\n" +
+ "1 unit files listed.\n");
+ assertTrue(systemCtl.serviceExists(taskContext, "foo"));
+
+ terminal.expectCommand("systemctl list-unit-files foo.service 2>&1", 0, "garbage");
+ try {
+ systemCtl.serviceExists(taskContext, "foo");
+ fail();
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("garbage"));
+ }
+ }
} \ No newline at end of file
diff --git a/node-admin/vespa-node-admin.spec b/node-admin/vespa-node-admin.spec
index 82e92fae52c..ff426e8bfa5 100644
--- a/node-admin/vespa-node-admin.spec
+++ b/node-admin/vespa-node-admin.spec
@@ -33,8 +33,9 @@ mkdir -p "$app_dir"/components
cp node-admin/src/main/application/services.xml "$app_dir"
declare -a jar_components=(
- node-admin/target/node-admin-jar-with-dependencies.jar
docker-api/target/docker-api-jar-with-dependencies.jar
+ flags/target/flags-jar-with-dependencies.jar
+ node-admin/target/node-admin-jar-with-dependencies.jar
)
for path in "${jar_components[@]}"; do
cp "$path" "$app_dir"/components
diff --git a/pom.xml b/pom.xml
index f749adbdabf..cfbf1eb1967 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,7 @@
<module>fat-model-dependencies</module>
<module>fileacquirer</module>
<module>filedistribution</module>
+ <module>flags</module>
<module>fsa</module>
<module>indexinglanguage</module>
<module>jaxrs_client_utils</module>