diff options
author | Håkon Hallingstad <hakon@oath.com> | 2018-11-23 17:30:13 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@oath.com> | 2018-11-23 17:30:13 +0100 |
commit | cadcac9a8c0501f86372eb05d107d7b089643d0a (patch) | |
tree | 9593cc78763cba8e790e49c21f28a19182002765 | |
parent | f67aa7bfa1553d8cc19ce4eef96f42ff8c31a320 (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).
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 @@ -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> |