summaryrefslogtreecommitdiffstats
path: root/node-admin
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-01-27 02:54:18 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-01-27 02:54:18 +0100
commitc3168ca58af1396e7a7562fdeb15fa604425020d (patch)
tree36b93fffd494aec216ec42e9c2621c4db89357ce /node-admin
parent9a9cdc344eb6de42c2c94db65a4d88a951a3b0a7 (diff)
Support file templates
Diffstat (limited to 'node-admin')
-rw-r--r--node-admin/pom.xml6
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributes.java22
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCache.java44
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCache.java34
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java116
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java40
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java18
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/PartialFileData.java62
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFile.java42
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPath.java59
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepo.java3
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCacheTest.java38
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCacheTest.java58
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java83
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java5
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFileTest.java49
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestTaskContext.java45
17 files changed, 667 insertions, 57 deletions
diff --git a/node-admin/pom.xml b/node-admin/pom.xml
index 983e4d3a832..161769a4edf 100644
--- a/node-admin/pom.xml
+++ b/node-admin/pom.xml
@@ -115,8 +115,12 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.velocity</groupId>
+ <artifactId>velocity</artifactId>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
-
<build>
<plugins>
<plugin>
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributes.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributes.java
new file mode 100644
index 00000000000..c99d5850909
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributes.java
@@ -0,0 +1,22 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.time.Instant;
+
+public class FileAttributes {
+ private final PosixFileAttributes attributes;
+
+ FileAttributes(PosixFileAttributes attributes) {
+ this.attributes = attributes;
+ }
+
+ public Instant lastModifiedTime() { return attributes.lastModifiedTime().toInstant(); }
+ public String owner() { return attributes.owner().getName(); }
+ public String group() { return attributes.group().getName(); }
+ public String permissions() { return PosixFilePermissions.toString(attributes.permissions()); }
+ public boolean isRegularFile() { return attributes.isRegularFile(); }
+ public boolean isDirectory() { return attributes.isDirectory(); }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCache.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCache.java
new file mode 100644
index 00000000000..12a9609f89c
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCache.java
@@ -0,0 +1,44 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.util.Optional;
+
+// @ThreadUnsafe
+public class FileAttributesCache {
+ private final UnixPath path;
+
+ private Optional<FileAttributes> attributes = Optional.empty();
+
+ public FileAttributesCache(UnixPath path) {
+ this.path = path;
+ }
+
+ public FileAttributes get() {
+ if (!attributes.isPresent()) {
+ attributes = Optional.of(path.getAttributes());
+ }
+
+ return attributes.get();
+ }
+
+ public FileAttributes forceGet() {
+ attributes = Optional.empty();
+ return get();
+ }
+
+ public boolean exists() {
+ if (attributes.isPresent()) {
+ return true;
+ }
+
+ Optional<FileAttributes> attributes = path.getAttributesIfExists();
+ if (attributes.isPresent()) {
+ // Might as well update this.attributes
+ this.attributes = attributes;
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCache.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCache.java
new file mode 100644
index 00000000000..ac50ff0cbab
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCache.java
@@ -0,0 +1,34 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Class to avoid repeated reads of file content when the file seldom changes.
+ */
+class FileContentCache {
+ private final UnixPath path;
+
+ private Optional<String> value = Optional.empty();
+ private Optional<Instant> modifiedTime = Optional.empty();
+
+ FileContentCache(UnixPath path) {
+ this.path = path;
+ }
+
+ String get(Instant lastModifiedTime) {
+ if (!value.isPresent() || lastModifiedTime.compareTo(modifiedTime.get()) > 0) {
+ value = Optional.of(path.readUtf8File());
+ modifiedTime = Optional.of(lastModifiedTime);
+ }
+
+ return value.get();
+ }
+
+ void updateWith(String content, Instant modifiedTime) {
+ this.value = Optional.of(content);
+ this.modifiedTime = Optional.of(modifiedTime);
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java
new file mode 100644
index 00000000000..f103ab394ef
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSync.java
@@ -0,0 +1,116 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+
+/**
+ * Class to minimize resource usage with repetitive and mostly identical, idempotent, and
+ * mutating file operations, e.g. setting file content, setting owner, etc.
+ *
+ * Only changes to the file is logged.
+ */
+// @ThreadUnsafe
+public class FileSync {
+ private static final Logger logger = Logger.getLogger(FileSync.class.getName());
+
+ private final UnixPath path;
+ private final FileContentCache contentCache;
+
+ public FileSync(Path path) {
+ this.path = new UnixPath(path);
+ this.contentCache = new FileContentCache(this.path);
+ }
+
+ /**
+ * CPU, I/O, and memory usage is optimized for repeated calls with the same arguments.
+ * @return true if the system was modified: content was written, or owner was set, etc.
+ * system is only modified if necessary (different).
+ */
+ public boolean convergeTo(TaskContext taskContext, PartialFileData partialFileData) {
+ FileAttributesCache currentAttributes = new FileAttributesCache(path);
+
+ boolean modifiedSystem = false;
+
+ modifiedSystem |= maybeUpdateContent(taskContext, partialFileData.getContent(), currentAttributes);
+
+ modifiedSystem |= convergeAttribute(
+ taskContext,
+ "owner",
+ partialFileData.getOwner(),
+ () -> currentAttributes.get().owner(),
+ path::setOwner);
+
+ modifiedSystem |= convergeAttribute(
+ taskContext,
+ "group",
+ partialFileData.getGroup(),
+ () -> currentAttributes.get().group(),
+ path::setGroup);
+
+ modifiedSystem |= convergeAttribute(
+ taskContext,
+ "permissions",
+ partialFileData.getPermissions(),
+ () -> currentAttributes.get().permissions(),
+ path::setPermissions);
+
+ return modifiedSystem;
+ }
+
+ private boolean convergeAttribute(TaskContext taskContext,
+ String attributeName,
+ Optional<String> wantedValue,
+ Supplier<String> currentValueSupplier,
+ Consumer<String> valueSetter) {
+ if (!wantedValue.isPresent()) {
+ return false;
+ }
+
+ String currentValue = currentValueSupplier.get();
+ if (Objects.equals(wantedValue.get(), currentValue)) {
+ return false;
+ } else {
+ String actionDescription = String.format("Changing %s of %s from %s to %s",
+ attributeName,
+ path,
+ currentValue,
+ wantedValue.get());
+ taskContext.logSystemModification(logger, actionDescription);
+ valueSetter.accept(wantedValue.get());
+ return true;
+ }
+ }
+
+ private boolean maybeUpdateContent(TaskContext taskContext,
+ Optional<String> content,
+ FileAttributesCache currentAttributes) {
+ if (!content.isPresent()) {
+ return false;
+ }
+
+ if (!currentAttributes.exists()) {
+ taskContext.logSystemModification(logger, "Creating file " + path);
+ path.createParents();
+ path.writeUtf8File(content.get());
+ contentCache.updateWith(content.get(), currentAttributes.forceGet().lastModifiedTime());
+ return true;
+ }
+
+ if (Objects.equals(content.get(), contentCache.get(currentAttributes.get().lastModifiedTime()))) {
+ return false;
+ } else {
+ taskContext.logSystemModification(logger, "Patching file " + path);
+ path.writeUtf8File(content.get());
+ contentCache.updateWith(content.get(), currentAttributes.forceGet().lastModifiedTime());
+ return true;
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java
index 60a7b3482b2..2f22c94781f 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java
@@ -6,56 +6,48 @@ import org.glassfish.jersey.internal.util.Producer;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Optional;
-import java.util.logging.Logger;
public class FileWriter {
- private static final Logger logger = Logger.getLogger(FileWriter.class.getName());
-
private final Path path;
+ private final FileSync fileSync;
+ private final PartialFileData.Builder fileDataBuilder = PartialFileData.builder();
private final Producer<String> contentProducer;
- private Optional<String> owner = Optional.empty();
- private Optional<String> group = Optional.empty();
- private Optional<String> permissions = Optional.empty();
+ private boolean overwriteExistingFile = true;
public FileWriter(Path path, Producer<String> contentProducer) {
this.path = path;
+ this.fileSync = new FileSync(path);
this.contentProducer = contentProducer;
}
public FileWriter withOwner(String owner) {
- this.owner = Optional.of(owner);
+ fileDataBuilder.withOwner(owner);
return this;
}
public FileWriter withGroup(String group) {
- this.group = Optional.of(group);
+ fileDataBuilder.withGroup(group);
return this;
}
public FileWriter withPermissions(String permissions) {
- this.permissions = Optional.of(permissions);
+ fileDataBuilder.withPermissions(permissions);
+ return this;
+ }
+
+ public FileWriter onlyIfFileDoesNotAlreadyExist() {
+ overwriteExistingFile = false;
return this;
}
public boolean converge(TaskContext context) {
- // TODO: Only return false if content, permission, etc would be unchanged.
- if (Files.isRegularFile(path)) {
+ if (!overwriteExistingFile && Files.isRegularFile(path)) {
return false;
}
- context.logSystemModification(logger,"Writing file " + path);
-
- String content = contentProducer.call();
-
- UnixPath unixPath = new UnixPath(path);
- unixPath.createParents();
- unixPath.writeUtf8File(content);
- permissions.ifPresent(unixPath::setPermissions);
- owner.ifPresent(unixPath::setOwner);
- group.ifPresent(unixPath::setGroup);
-
- return true;
+ fileDataBuilder.withContent(contentProducer.call());
+ PartialFileData fileData = fileDataBuilder.create();
+ return fileSync.convergeTo(context, fileData);
}
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java
index dee5525d42a..ef553a131e8 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java
@@ -1,8 +1,10 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.task.util.file;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.util.Optional;
public class IOExceptionUtil {
public static <T> void uncheck(RunnableThrowingIOException<T> runnable) {
@@ -31,4 +33,20 @@ public class IOExceptionUtil {
public interface RunnableThrowingIOException<T> {
void run() throws IOException;
}
+
+ /**
+ * Useful if it's not known whether a file or directory exists, in case e.g.
+ * FileNotFoundException is thrown and the caller wants an Optional.empty() in that case.
+ */
+ public static <T> Optional<T> ifExists(SupplierThrowingIOException<T> supplier) {
+ try {
+ return Optional.ofNullable(uncheck(supplier));
+ } catch (UncheckedIOException e) {
+ if (e.getCause() instanceof FileNotFoundException) {
+ return Optional.empty();
+ }
+
+ throw e;
+ }
+ }
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/PartialFileData.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/PartialFileData.java
new file mode 100644
index 00000000000..d29c01179f2
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/PartialFileData.java
@@ -0,0 +1,62 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import java.util.Optional;
+
+/**
+ * Represents a subset of a file's content, owner, group, and permissions.
+ */
+// @Immutable
+public class PartialFileData {
+ private final Optional<String> content;
+ private final Optional<String> owner;
+ private final Optional<String> group;
+ private final Optional<String> permissions;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public PartialFileData(Optional<String> content,
+ Optional<String> owner,
+ Optional<String> group,
+ Optional<String> permissions) {
+ this.content = content;
+ this.owner = owner;
+ this.group = group;
+ this.permissions = permissions;
+ }
+
+ public Optional<String> getContent() {
+ return content;
+ }
+
+ public Optional<String> getOwner() {
+ return owner;
+ }
+
+ public Optional<String> getGroup() {
+ return group;
+ }
+
+ public Optional<String> getPermissions() {
+ return permissions;
+ }
+
+ public static class Builder {
+ private Optional<String> content = Optional.empty();
+ private Optional<String> owner = Optional.empty();
+ private Optional<String> group = Optional.empty();
+ private Optional<String> permissions = Optional.empty();
+
+ public Builder withContent(String content) { this.content = Optional.of(content); return this; }
+ public Builder withOwner(String owner) { this.owner = Optional.of(owner); return this; }
+ public Builder withGroup(String group) { this.group = Optional.of(group); return this; }
+ public Builder withPermissions(String permissions) { this.permissions = Optional.of(permissions); return this; }
+
+ public PartialFileData create() {
+ return new PartialFileData(content, owner, group, permissions);
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFile.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFile.java
new file mode 100644
index 00000000000..1d5b5f42cf3
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFile.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.hosted.node.admin.task.util.file;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+import org.apache.velocity.app.VelocityEngine;
+
+import java.io.StringWriter;
+import java.nio.file.Path;
+
+public class TemplateFile {
+ private final Path templatePath;
+ private final VelocityEngine velocityEngine;
+ private final VelocityContext velocityContext = new VelocityContext();
+
+ public TemplateFile(Path templatePath) {
+ this.templatePath = templatePath;
+ velocityEngine = new VelocityEngine();
+ velocityEngine.addProperty(
+ Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
+ "org.apache.velocity.runtime.log.NullLogSystem");
+ velocityEngine.addProperty(Velocity.FILE_RESOURCE_LOADER_PATH, templatePath.getParent().toString());
+ velocityEngine.init();
+ }
+
+ public TemplateFile set(String name, String value) {
+ velocityContext.put(name, value);
+ return this;
+ }
+
+ public FileWriter getFileWriterTo(Path destinationPath) {
+ return new FileWriter(destinationPath, this::render);
+ }
+
+ private String render() {
+ Template template = velocityEngine.getTemplate(templatePath.getFileName().toString(), "UTF-8");
+ StringWriter writer = new StringWriter();
+ template.merge(velocityContext, writer);
+ return writer.toString();
+ }
+}
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 606f8cfb06e..a83d79a97a9 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
@@ -1,10 +1,10 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.task.util.file;
-import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -16,8 +16,11 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.time.Instant;
+import java.util.Optional;
import java.util.Set;
+import static com.yahoo.vespa.hosted.node.admin.task.util.file.IOExceptionUtil.uncheck;
+
// @Immutable
public class UnixPath {
private final Path path;
@@ -34,8 +37,14 @@ public class UnixPath {
return path;
}
- public void createParents() {
- uncheck(() -> Files.createDirectories(path.getParent()));
+ public boolean createParents() {
+ Path parent = path.getParent();
+ if (Files.isDirectory(parent)) {
+ return false;
+ }
+
+ uncheck(() -> Files.createDirectories(parent));
+ return true;
}
public String readUtf8File() {
@@ -49,7 +58,7 @@ public class UnixPath {
}
public String getPermissions() {
- return PosixFilePermissions.toString(getAttributes().permissions());
+ return getAttributes().permissions();
}
/**
@@ -69,7 +78,7 @@ public class UnixPath {
}
public String getOwner() {
- return getAttributes().owner().getName();
+ return getAttributes().owner();
}
public void setOwner(String owner) {
@@ -79,7 +88,7 @@ public class UnixPath {
}
public String getGroup() {
- return getAttributes().group().getName();
+ return getAttributes().group();
}
public void setGroup(String group) {
@@ -89,37 +98,29 @@ public class UnixPath {
}
public Instant getLastModifiedTime() {
- return uncheck(() -> Files.getLastModifiedTime(path)).toInstant();
+ return getAttributes().lastModifiedTime();
}
- private PosixFileAttributes getAttributes() {
- return uncheck(() ->
+ public FileAttributes getAttributes() {
+ PosixFileAttributes attributes = uncheck(() ->
Files.getFileAttributeView(path, PosixFileAttributeView.class).readAttributes());
+ return new FileAttributes(attributes);
}
- @FunctionalInterface
- private interface SupplierThrowingIOException<T> {
- T get() throws IOException;
- }
-
- private static <T> T uncheck(SupplierThrowingIOException<T> supplier) {
+ public Optional<FileAttributes> getAttributesIfExists() {
try {
- return supplier.get();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
+ return Optional.of(getAttributes());
+ } catch (UncheckedIOException e) {
+ if (e.getCause() instanceof NoSuchFileException) {
+ return Optional.empty();
+ }
- @FunctionalInterface
- private interface RunnableThrowingIOException<T> {
- void run() throws IOException;
+ throw e;
+ }
}
- private static <T> void uncheck(RunnableThrowingIOException<T> runnable) {
- try {
- runnable.run();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
+ @Override
+ public String toString() {
+ return path.toString();
}
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepo.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepo.java
index 9ca1c0286f9..d6dbc3cd12a 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepo.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepo.java
@@ -32,7 +32,8 @@ public class AddYumRepo {
FileWriter fileWriter = new FileWriter(path, this::getRepoFileContent)
.withOwner("root")
.withGroup("root")
- .withPermissions("rw-r--r--");
+ .withPermissions("rw-r--r--")
+ .onlyIfFileDoesNotAlreadyExist();
return fileWriter.converge(context);
}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCacheTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCacheTest.java
new file mode 100644
index 00000000000..9224faf1c6f
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileAttributesCacheTest.java
@@ -0,0 +1,38 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import org.junit.Test;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class FileAttributesCacheTest {
+ @Test
+ public void exists() throws Exception {
+ UnixPath unixPath = mock(UnixPath.class);
+ FileAttributesCache cache = new FileAttributesCache(unixPath);
+
+ when(unixPath.getAttributesIfExists()).thenReturn(Optional.empty());
+ assertFalse(cache.exists());
+ verify(unixPath, times(1)).getAttributesIfExists();
+ verifyNoMoreInteractions(unixPath);
+
+ FileAttributes attributes = mock(FileAttributes.class);
+ when(unixPath.getAttributesIfExists()).thenReturn(Optional.of(attributes));
+ assertTrue(cache.exists());
+ verify(unixPath, times(1 + 1)).getAttributesIfExists();
+ verifyNoMoreInteractions(unixPath);
+
+ assertEquals(attributes, cache.get());
+ verifyNoMoreInteractions(unixPath);
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCacheTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCacheTest.java
new file mode 100644
index 00000000000..677dd048445
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileContentCacheTest.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.hosted.node.admin.task.util.file;
+
+import org.junit.Test;
+
+import java.time.Instant;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class FileContentCacheTest {
+ private final UnixPath unixPath = mock(UnixPath.class);
+ private final FileContentCache cache = new FileContentCache(unixPath);
+
+ @Test
+ public void get() throws Exception {
+ when(unixPath.readUtf8File()).thenReturn("content");
+ assertEquals("content", cache.get(Instant.ofEpochMilli(0)));
+ verify(unixPath, times(1)).readUtf8File();
+ verifyNoMoreInteractions(unixPath);
+
+ // cache hit
+ assertEquals("content", cache.get(Instant.ofEpochMilli(0)));
+ verify(unixPath, times(1)).readUtf8File();
+ verifyNoMoreInteractions(unixPath);
+
+ // cache miss
+ when(unixPath.readUtf8File()).thenReturn("new-content");
+ assertEquals("new-content", cache.get(Instant.ofEpochMilli(1)));
+ verify(unixPath, times(1 + 1)).readUtf8File();
+ verifyNoMoreInteractions(unixPath);
+
+ // cache hit both at times 0 and 1
+ assertEquals("new-content", cache.get(Instant.ofEpochMilli(0)));
+ verify(unixPath, times(1 + 1)).readUtf8File();
+ verifyNoMoreInteractions(unixPath);
+ assertEquals("new-content", cache.get(Instant.ofEpochMilli(1)));
+ verify(unixPath, times(1 + 1)).readUtf8File();
+ verifyNoMoreInteractions(unixPath);
+ }
+
+ @Test
+ public void updateWith() throws Exception {
+ cache.updateWith("content", Instant.ofEpochMilli(2));
+ assertEquals("content", cache.get(Instant.ofEpochMilli(2)));
+ verifyNoMoreInteractions(unixPath);
+
+ cache.updateWith("new-content", Instant.ofEpochMilli(4));
+ assertEquals("new-content", cache.get(Instant.ofEpochMilli(4)));
+ verifyNoMoreInteractions(unixPath);
+ }
+
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java
new file mode 100644
index 00000000000..71cd312d5d6
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileSyncTest.java
@@ -0,0 +1,83 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.node.admin.task.util.file;
+
+import org.junit.Test;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FileSyncTest {
+ private final TestTaskContext taskContext = new TestTaskContext();
+ private final FileSystem fileSystem = taskContext.fileSystem();
+
+ private final Path path = fileSystem.getPath("/dir/file.txt");
+ private final UnixPath unixPath = new UnixPath(path);
+ private final FileSync fileSync = new FileSync(path);
+
+ private String content = "content";
+ private String owner = "owner"; // default is user
+ private String group = "group1"; // default is group
+ private String permissions = "rw-r-xr--";
+
+ @Test
+ public void trivial() {
+ assertConvergence("Creating file /dir/file.txt",
+ "Changing owner of /dir/file.txt from user to owner",
+ "Changing group of /dir/file.txt from group to group1",
+ "Changing permissions of /dir/file.txt from rw-r--r-- to rw-r-xr--");
+
+ content = "new-content";
+ assertConvergence("Patching file /dir/file.txt");
+
+ owner = "new-owner";
+ assertConvergence("Changing owner of /dir/file.txt from owner to " +
+ owner);
+
+ group = "new-group1";
+ assertConvergence("Changing group of /dir/file.txt from group1 to new-group1");
+
+ permissions = "rwxr--rwx";
+ assertConvergence("Changing permissions of /dir/file.txt from rw-r-xr-- to " +
+ permissions);
+ }
+
+ private void assertConvergence(String... systemModificationMessages) {
+ PartialFileData fileData = PartialFileData.builder()
+ .withContent(content)
+ .withOwner(owner)
+ .withGroup(group)
+ .withPermissions(permissions)
+ .create();
+ taskContext.clearSystemModificationLog();
+ assertTrue(fileSync.convergeTo(taskContext, fileData));
+
+ assertTrue(Files.isRegularFile(path));
+ fileData.getContent().ifPresent(content -> assertEquals(content, unixPath.readUtf8File()));
+ fileData.getOwner().ifPresent(owner -> assertEquals(owner, unixPath.getOwner()));
+ fileData.getGroup().ifPresent(group -> assertEquals(group, unixPath.getGroup()));
+ fileData.getPermissions().ifPresent(permissions -> assertEquals(permissions, unixPath.getPermissions()));
+
+ List<String> actualMods = taskContext.getSystemModificationLog();
+ List<String> expectedMods = Arrays.asList(systemModificationMessages);
+ assertEquals(expectedMods, actualMods);
+
+ UnixPath unixPath = new UnixPath(path);
+ Instant lastModifiedTime = unixPath.getLastModifiedTime();
+ taskContext.clearSystemModificationLog();
+ assertFalse(fileSync.convergeTo(taskContext, fileData));
+ assertEquals(lastModifiedTime, unixPath.getLastModifiedTime());
+
+ actualMods = taskContext.getSystemModificationLog();
+ assertEquals(new ArrayList<>(), actualMods);
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java
index 878b1e4cbdb..bb8ca2586c8 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java
@@ -33,10 +33,11 @@ public class FileWriterTest {
FileWriter writer = new FileWriter(path, () -> content)
.withPermissions(permissions)
.withOwner(owner)
- .withGroup(group);
+ .withGroup(group)
+ .onlyIfFileDoesNotAlreadyExist();
TaskContext context = mock(TaskContext.class);
assertTrue(writer.converge(context));
- verify(context, times(1)).logSystemModification(any(), eq("Writing file " + path));
+ verify(context, times(1)).logSystemModification(any(), eq("Creating file " + path));
UnixPath unixPath = new UnixPath(path);
assertEquals(content, unixPath.readUtf8File());
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFileTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFileTest.java
new file mode 100644
index 00000000000..b1d88fdaaee
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TemplateFileTest.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.hosted.node.admin.task.util.file;
+
+import com.yahoo.vespa.hosted.node.admin.component.TaskContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * WARNING: Velocity does not honor an alternative FileSystem like JimFS.
+ */
+public class TemplateFileTest {
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ private void writeFile(Path path, String content) {
+ UnixPath unixPath = new UnixPath(path);
+ unixPath.createParents();
+ unixPath.writeUtf8File(content);
+ }
+
+ @Test
+ public void basic() throws IOException {
+ String templateContent = "a $x, $y b";
+ Path templatePath = folder.newFile("example.vm").toPath();
+ writeFile(templatePath, templateContent);
+
+ Path toPath = folder.newFile().toPath();
+ TaskContext taskContext = mock(TaskContext.class);
+ boolean converged = new TemplateFile(templatePath)
+ .set("x", "foo")
+ .set("y", "bar")
+ .getFileWriterTo(toPath)
+ .converge(taskContext);
+
+ assertTrue(converged);
+
+ String actualContent = new UnixPath(toPath).readUtf8File();
+ assertEquals("a foo, bar b", actualContent);
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestTaskContext.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestTaskContext.java
new file mode 100644
index 00000000000..d023a11671e
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestTaskContext.java
@@ -0,0 +1,45 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+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 java.nio.file.FileSystem;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.logging.Logger;
+
+public class TestTaskContext implements TaskContext {
+ private final FileSystem fileSystem = TestFileSystem.create();
+ private final List<String> logs = new ArrayList<>();
+
+ @Override
+ public Cloud cloud() {
+ return Cloud.YAHOO;
+ }
+
+ @Override
+ public EnumSet<Role> roles() {
+ return EnumSet.of(Role.CONFIG_SERVER_DOCKER_HOST);
+ }
+
+ @Override
+ public FileSystem fileSystem() {
+ return fileSystem;
+ }
+
+ @Override
+ public void logSystemModification(Logger logger, String actionDescription) {
+ logs.add(actionDescription);
+ }
+
+ public List<String> getSystemModificationLog() {
+ return logs;
+ }
+
+ public void clearSystemModificationLog() {
+ logs.clear();
+ }
+}