summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHÃ¥kon Hallingstad <hakon@oath.com>2018-01-09 00:25:51 +0100
committerGitHub <noreply@github.com>2018-01-09 00:25:51 +0100
commit6db81d28a8aca631e10c0f539f7727a9b9b2e6b7 (patch)
tree27a1976b105ca303e563d499bf56cb02c69ce1bf
parent2710414fabfa7f17d88a48fd01c775e1b2b2ddeb (diff)
parentaf2833b52d307b6536ad646a42be6c4ce4ac7ca0 (diff)
Merge pull request #4563 from vespa-engine/hakonhall/introduce-task-and-admincomponent
Introduce Task and AdminComponent
-rw-r--r--node-admin/src/main/application/services.xml44
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/AdminComponent.java21
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/package-info.java5
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystem.java120
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemPath.java68
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminComponent.java133
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminConfig.java9
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminConfig.java8
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java5
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminMain.java90
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java5
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTask.java61
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTask.java49
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/Task.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTask.java80
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/package-info.java5
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemTest.java81
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java3
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTaskTest.java70
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTaskTest.java49
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/TaskTestBase.java26
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTaskTest.java80
-rw-r--r--yolean/pom.xml2
23 files changed, 932 insertions, 99 deletions
diff --git a/node-admin/src/main/application/services.xml b/node-admin/src/main/application/services.xml
index 030c42ac8c5..27de4c856e8 100644
--- a/node-admin/src/main/application/services.xml
+++ b/node-admin/src/main/application/services.xml
@@ -1,23 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<jdisc id="node-admin" jetty="true" version="1.0">
- <!-- Please update container test when changing this file -->
- <accesslog type="vespa" fileNamePattern="logs/vespa/node-admin/access.log.%Y%m%d%H%M%S" rotationScheme="date" symlinkName="access.log" />
- <handler id="com.yahoo.vespa.hosted.node.admin.restapi.RestApiHandler" bundle="node-admin">
- <binding>http://*/rest/*</binding>
- </handler>
- <component id="node-admin" class="com.yahoo.vespa.hosted.node.admin.provider.NodeAdminProvider" bundle="node-admin"/>
- <component id="docker-api" class="com.yahoo.vespa.hosted.dockerapi.DockerImpl" bundle="docker-api"/>
- <component id="metrics-wrapper" class="com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper" bundle="docker-api"/>
+<service version="1.0" xmlns:preprocess="properties">
+ <jdisc id="node-admin" jetty="true" version="1.0">
+ <!-- Please update container test when changing this file -->
+ <accesslog type="vespa" fileNamePattern="logs/vespa/node-admin/access.log.%Y%m%d%H%M%S" rotationScheme="date" symlinkName="access.log" />
+ <handler id="com.yahoo.vespa.hosted.node.admin.restapi.RestApiHandler" bundle="node-admin">
+ <binding>http://*/rest/*</binding>
+ </handler>
+ <component id="node-admin" class="com.yahoo.vespa.hosted.node.admin.provider.NodeAdminProvider" bundle="node-admin"/>
+ <component id="docker-api" class="com.yahoo.vespa.hosted.dockerapi.DockerImpl" bundle="docker-api"/>
+ <component id="metrics-wrapper" class="com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper" bundle="docker-api"/>
- <config name='vespa.hosted.dockerapi.docker'>
- <isRunningLocally>false</isRunningLocally>
- </config>
-
- <config name='vespa.hosted.node.admin.node-admin'>
- <isRunningLocally>false</isRunningLocally>
- <restartOnDeploy>true</restartOnDeploy>
- </config>
-
- <nodes type="host"/>
-</jdisc>
+ <config name="vespa.hosted.dockerapi.docker">
+ <isRunningLocally>false</isRunningLocally>
+ </config>
+
+ <config name="vespa.hosted.node.admin.node-admin">
+ <isRunningLocally>false</isRunningLocally>
+ <restartOnDeploy>true</restartOnDeploy>
+ </config>
+
+ <nodes type="host"/>
+
+ <preprocess:include file="variant.xml" required="false"/>
+ </jdisc>
+</service>
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/AdminComponent.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/AdminComponent.java
new file mode 100644
index 00000000000..9bed492bd76
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/AdminComponent.java
@@ -0,0 +1,21 @@
+// 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.component;
+
+/**
+ * An AdminComponent cannot assume anything about the environment until enable()
+ * is called: Required YUM packages may not have been installed, services
+ * not started, etc. An enabled AdminComponent can be disabled to disengage from
+ * the environment.
+ */
+public interface AdminComponent {
+ /**
+ * Enable component. May be called more than once.
+ */
+ void enable();
+
+ /**
+ * Disable component. May be called more than once.
+ * Must be compatible with component deconstruct().
+ */
+ void disable();
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/package-info.java
new file mode 100644
index 00000000000..6863a0050cd
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/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.hosted.node.admin.component;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystem.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystem.java
new file mode 100644
index 00000000000..c5c2df9e38e
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystem.java
@@ -0,0 +1,120 @@
+// 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.io;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.Set;
+
+/**
+ * File system operations to be mocked in unit tests.
+ */
+public class FileSystem {
+ public FileSystemPath withPath(Path path) {
+ return new FileSystemPath(this, path);
+ }
+
+ public boolean isDirectory(Path path) {
+ return path.toFile().isDirectory();
+ }
+
+ public boolean isRegularFile(Path path) {
+ return path.toFile().isFile();
+ }
+
+ public void createDirectory(Path path, FileAttribute<?>... attributes) {
+ uncheck(() -> Files.createDirectory(path, attributes));
+ }
+
+ public String readUtf8File(Path path) {
+ byte[] byteContent = uncheck(() -> Files.readAllBytes(path));
+ return new String(byteContent, StandardCharsets.UTF_8);
+ }
+
+ public void writeUtf8File(Path path, String content, OpenOption... options) {
+ byte[] contentInUtf8 = content.getBytes(StandardCharsets.UTF_8);
+ uncheck(() -> Files.write(path, contentInUtf8, options));
+ }
+
+ private PosixFileAttributes getAttributes(Path path) {
+ return uncheck(() ->
+ Files.getFileAttributeView(path, PosixFileAttributeView.class).readAttributes());
+ }
+
+ public String getPermissions(Path path) {
+ return PosixFilePermissions.toString(getAttributes(path).permissions());
+ }
+
+ /**
+ * @param permissions Example: "rwxr-x---" means rwx for owner, rx for group,
+ * and no permissions for others.
+ */
+ public void setPermissions(Path path, String permissions) {
+ Set<PosixFilePermission> permissionSet;
+ try {
+ permissionSet = PosixFilePermissions.fromString(permissions);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Failed to set permissions '" +
+ permissions + "' on path " + path, e);
+ }
+
+ uncheck(() -> Files.setPosixFilePermissions(path, permissionSet));
+ }
+
+ public String getOwner(Path path) {
+ return getAttributes(path).owner().getName();
+ }
+
+ public void setOwner(Path path, String owner) {
+ UserPrincipalLookupService service = path.getFileSystem().getUserPrincipalLookupService();
+ UserPrincipal principal = uncheck(() -> service.lookupPrincipalByName(owner));
+ uncheck(() -> Files.setOwner(path, principal));
+ }
+
+ public String getGroup(Path path) {
+ return getAttributes(path).group().getName();
+ }
+
+ public void setGroup(Path path, String group) {
+ UserPrincipalLookupService service = path.getFileSystem().getUserPrincipalLookupService();
+ GroupPrincipal principal = uncheck(() -> service.lookupPrincipalByGroupName(group));
+ uncheck(() -> Files.getFileAttributeView(path, PosixFileAttributeView.class).setGroup(principal));
+ }
+
+ @FunctionalInterface
+ private interface SupplierThrowingIOException<T> {
+ T get() throws IOException;
+ }
+
+ private static <T> T uncheck(SupplierThrowingIOException<T> supplier) {
+ try {
+ return supplier.get();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @FunctionalInterface
+ private interface RunnableThrowingIOException<T> {
+ void run() throws IOException;
+ }
+
+ private static <T> void uncheck(RunnableThrowingIOException<T> runnable) {
+ try {
+ runnable.run();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemPath.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemPath.java
new file mode 100644
index 00000000000..bfec341e05c
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemPath.java
@@ -0,0 +1,68 @@
+// 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.io;
+
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
+
+/**
+ * Convenience class for calling FileSystem methods on a fixed Path.
+ */
+public class FileSystemPath {
+ private final FileSystem fileSystem;
+ private final Path path;
+
+ FileSystemPath(FileSystem fileSystem, Path path) {
+ this.fileSystem = fileSystem;
+ this.path = path;
+ }
+
+ public boolean isDirectory() {
+ return fileSystem.isDirectory(path);
+ }
+
+ public boolean isRegularFile() {
+ return fileSystem.isRegularFile(path);
+ }
+
+ public FileSystemPath createDirectory(FileAttribute<?>... attributes) {
+ fileSystem.createDirectory(path, attributes);
+ return this;
+ }
+
+ public String readUtf8File() {
+ return fileSystem.readUtf8File(path);
+ }
+
+ public FileSystemPath writeUtf8File(String content, OpenOption... options) {
+ fileSystem.writeUtf8File(path, content, options);
+ return this;
+ }
+
+ public String getPermissions() {
+ return fileSystem.getPermissions(path);
+ }
+
+ public FileSystemPath setPermissions(String permissions) {
+ fileSystem.setPermissions(path, permissions);
+ return this;
+ }
+
+ public String getOwner() {
+ return fileSystem.getOwner(path);
+ }
+
+ public FileSystemPath setOwner(String owner) {
+ fileSystem.setOwner(path, owner);
+ return this;
+ }
+
+ public String getGroup() {
+ return fileSystem.getGroup(path);
+ }
+
+ public FileSystemPath setGroup(String group) {
+ fileSystem.setGroup(path, group);
+ return this;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminComponent.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminComponent.java
new file mode 100644
index 00000000000..5d8fed67682
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminComponent.java
@@ -0,0 +1,133 @@
+// 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.nodeadmin;
+
+import com.yahoo.concurrent.classlock.ClassLocking;
+import com.yahoo.net.HostName;
+import com.yahoo.system.ProcessExecuter;
+import com.yahoo.vespa.hosted.dockerapi.Docker;
+import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
+import com.yahoo.vespa.hosted.node.admin.component.AdminComponent;
+import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
+import com.yahoo.vespa.hosted.node.admin.docker.DockerOperationsImpl;
+import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
+import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer;
+import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent;
+import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl;
+import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
+import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepositoryImpl;
+import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator;
+import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorImpl;
+import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor;
+import com.yahoo.vespa.hosted.node.admin.util.Environment;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Component that manages Docker containers based on some node repository.
+ */
+public class DockerAdminComponent implements AdminComponent {
+ private static final Duration NODE_AGENT_SCAN_INTERVAL = Duration.ofSeconds(30);
+ private static final Duration NODE_ADMIN_CONVERGE_STATE_INTERVAL = Duration.ofSeconds(30);
+
+ private final NodeAdminConfig config;
+ private final Docker docker;
+ private final MetricReceiverWrapper metricReceiver;
+ private final ClassLocking classLocking;
+
+ private Optional<NodeAdminStateUpdater> nodeAdminStateUpdater = Optional.empty();
+
+ public DockerAdminComponent(NodeAdminConfig config,
+ Docker docker,
+ MetricReceiverWrapper metricReceiver,
+ ClassLocking classLocking) {
+ this.config = config;
+ this.docker = docker;
+ this.metricReceiver = metricReceiver;
+ this.classLocking = classLocking;
+ }
+
+ @Override
+ public void enable() {
+ if (nodeAdminStateUpdater.isPresent()) {
+ return;
+ }
+
+
+ Environment environment = new Environment();
+ ConfigServerHttpRequestExecutor requestExecutor =
+ ConfigServerHttpRequestExecutor.create(environment.getConfigServerUris());
+ NodeRepository nodeRepository = new NodeRepositoryImpl(requestExecutor);
+ Orchestrator orchestrator = new OrchestratorImpl(requestExecutor);
+
+ Clock clock = Clock.systemUTC();
+ String dockerHostHostName = HostName.getLocalhost();
+ ProcessExecuter processExecuter = new ProcessExecuter();
+
+ docker.start();
+ DockerOperations dockerOperations = new DockerOperationsImpl(
+ docker,
+ environment,
+ processExecuter);
+
+ StorageMaintainer storageMaintainer = new StorageMaintainer(
+ dockerOperations,
+ processExecuter,
+ metricReceiver,
+ environment,
+ clock);
+
+ AclMaintainer aclMaintainer = new AclMaintainer(
+ dockerOperations,
+ nodeRepository,
+ dockerHostHostName);
+
+ Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(
+ hostName,
+ nodeRepository,
+ orchestrator,
+ dockerOperations,
+ storageMaintainer,
+ aclMaintainer,
+ environment,
+ clock,
+ NODE_AGENT_SCAN_INTERVAL);
+
+ NodeAdmin nodeAdmin = new NodeAdminImpl(
+ dockerOperations,
+ nodeAgentFactory,
+ storageMaintainer,
+ aclMaintainer,
+ metricReceiver,
+ clock);
+
+ nodeAdminStateUpdater = Optional.of(new NodeAdminStateUpdater(
+ nodeRepository,
+ orchestrator,
+ storageMaintainer,
+ nodeAdmin,
+ dockerHostHostName,
+ clock,
+ NODE_ADMIN_CONVERGE_STATE_INTERVAL,
+ classLocking));
+
+ nodeAdminStateUpdater.get().start();
+ }
+
+ @Override
+ public void disable() {
+ if (!nodeAdminStateUpdater.isPresent()) {
+ return;
+ }
+
+ nodeAdminStateUpdater.get().stop();
+ nodeAdminStateUpdater = Optional.empty();
+ // TODO: Also stop docker
+ }
+
+ public NodeAdminStateUpdater getNodeAdminStateUpdater() {
+ return nodeAdminStateUpdater.get();
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminConfig.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminConfig.java
new file mode 100644
index 00000000000..d1aaefab3dd
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/DockerAdminConfig.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.hosted.node.admin.nodeadmin;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DockerAdminConfig {
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminConfig.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminConfig.java
index 9caf1307aa4..d9726c88e4c 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminConfig.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminConfig.java
@@ -14,14 +14,18 @@ public class NodeAdminConfig {
private static final Logger logger = Logger.getLogger(NodeAdminConfig.class.getName());
private static final ObjectMapper mapper = new ObjectMapper();
- enum Mode {
+ public enum Mode {
+ aws_tenant,
+ config_server_host,
tenant,
- config_server_host
}
@JsonProperty("mode")
public Mode mode = Mode.tenant;
+ @JsonProperty("docker")
+ public DockerAdminConfig docker = new DockerAdminConfig();
+
public static NodeAdminConfig fromFile(File file) {
if (!file.exists()) {
return new NodeAdminConfig();
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
index 868ebf39f70..722b1f5a40d 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java
@@ -11,7 +11,6 @@ import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
-import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent;
import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger;
@@ -47,7 +46,7 @@ public class NodeAdminImpl implements NodeAdmin {
private final DockerOperations dockerOperations;
private final Function<String, NodeAgent> nodeAgentFactory;
private final StorageMaintainer storageMaintainer;
- private final AclMaintainer aclMaintainer;
+ private final Runnable aclMaintainer;
private final Clock clock;
private boolean previousWantFrozen;
@@ -62,7 +61,7 @@ public class NodeAdminImpl implements NodeAdmin {
public NodeAdminImpl(final DockerOperations dockerOperations,
final Function<String, NodeAgent> nodeAgentFactory,
final StorageMaintainer storageMaintainer,
- final AclMaintainer aclMaintainer,
+ final Runnable aclMaintainer,
final MetricReceiverWrapper metricReceiver,
final Clock clock) {
this.dockerOperations = dockerOperations;
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminMain.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminMain.java
index 1ce6c9a14e1..f5f2deb8dcb 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminMain.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminMain.java
@@ -2,29 +2,12 @@
package com.yahoo.vespa.hosted.node.admin.nodeadmin;
import com.yahoo.concurrent.classlock.ClassLocking;
-import com.yahoo.net.HostName;
-import com.yahoo.system.ProcessExecuter;
import com.yahoo.vespa.defaults.Defaults;
import com.yahoo.vespa.hosted.dockerapi.Docker;
import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
-import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
-import com.yahoo.vespa.hosted.node.admin.docker.DockerOperationsImpl;
-import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
-import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer;
-import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent;
-import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl;
-import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
-import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepositoryImpl;
-import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator;
-import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorImpl;
-import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor;
-import com.yahoo.vespa.hosted.node.admin.util.Environment;
import java.io.File;
-import java.time.Clock;
-import java.time.Duration;
import java.util.Optional;
-import java.util.function.Function;
/**
* NodeAdminMain is the main component of the node admin JDisc application:
@@ -36,16 +19,15 @@ import java.util.function.Function;
* be fatal: the node admin may not have installed and started the docker daemon.
*/
public class NodeAdminMain implements AutoCloseable {
- private static final Duration NODE_AGENT_SCAN_INTERVAL = Duration.ofSeconds(30);
- private static final Duration NODE_ADMIN_CONVERGE_STATE_INTERVAL = Duration.ofSeconds(30);
-
private final Docker docker;
private final MetricReceiverWrapper metricReceiver;
private final ClassLocking classLocking;
- private Optional<NodeAdminStateUpdater> nodeAdminStateUpdater = Optional.empty();
+ private Optional<DockerAdminComponent> dockerAdmin = Optional.empty();
- public NodeAdminMain(Docker docker, MetricReceiverWrapper metricReceiver, ClassLocking classLocking) {
+ public NodeAdminMain(Docker docker,
+ MetricReceiverWrapper metricReceiver,
+ ClassLocking classLocking) {
this.docker = docker;
this.metricReceiver = metricReceiver;
this.classLocking = classLocking;
@@ -53,11 +35,11 @@ public class NodeAdminMain implements AutoCloseable {
@Override
public void close() {
- nodeAdminStateUpdater.ifPresent(NodeAdminStateUpdater::stop);
+ dockerAdmin.ifPresent(DockerAdminComponent::disable);
}
public NodeAdminStateUpdater getNodeAdminStateUpdater() {
- return nodeAdminStateUpdater.get();
+ return dockerAdmin.get().getNodeAdminStateUpdater();
}
public void start() {
@@ -65,54 +47,24 @@ public class NodeAdminMain implements AutoCloseable {
NodeAdminConfig config = NodeAdminConfig.fromFile(new File(staticConfigPath));
switch (config.mode) {
+ case aws_tenant:
case tenant:
- setupTenantHostNodeAdmin();
- break;
+ dockerAdmin = Optional.of(new DockerAdminComponent(
+ config,
+ docker,
+ metricReceiver,
+ classLocking));
+ dockerAdmin.get().enable();
+ return;
case config_server_host:
- setupConfigServerHostNodeAdmin();
- break;
- default:
- throw new IllegalStateException(
- "Unknown bootstrap mode: " + config.mode.name());
+ // TODO:
+ // - install and start docker daemon
+ // - Read config that specifies which containers to start how
+ // - use thin static backends for node repo and orchestrator
+ // - Start node admin state updater.
+ return;
}
- }
-
- private void setupTenantHostNodeAdmin() {
- nodeAdminStateUpdater = Optional.of(createNodeAdminStateUpdater());
- nodeAdminStateUpdater.get().start();
- }
-
- private NodeAdminStateUpdater createNodeAdminStateUpdater() {
- Clock clock = Clock.systemUTC();
- String dockerHostHostName = HostName.getLocalhost();
- ProcessExecuter processExecuter = new ProcessExecuter();
- Environment environment = new Environment();
-
- ConfigServerHttpRequestExecutor requestExecutor = ConfigServerHttpRequestExecutor.create(environment.getConfigServerUris());
- NodeRepository nodeRepository = new NodeRepositoryImpl(requestExecutor);
- Orchestrator orchestrator = new OrchestratorImpl(requestExecutor);
-
- docker.start();
- DockerOperations dockerOperations = new DockerOperationsImpl(docker, environment, processExecuter);
-
- StorageMaintainer storageMaintainer = new StorageMaintainer(dockerOperations, processExecuter, metricReceiver, environment, clock);
- AclMaintainer aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository, dockerHostHostName);
-
- Function<String, NodeAgent> nodeAgentFactory =
- (hostName) -> new NodeAgentImpl(hostName, nodeRepository, orchestrator, dockerOperations,
- storageMaintainer, aclMaintainer, environment, clock, NODE_AGENT_SCAN_INTERVAL);
- NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, storageMaintainer, aclMaintainer,
- metricReceiver, clock);
-
- return new NodeAdminStateUpdater(nodeRepository, orchestrator, storageMaintainer, nodeAdmin,
- dockerHostHostName, clock, NODE_ADMIN_CONVERGE_STATE_INTERVAL, classLocking);
- }
- private void setupConfigServerHostNodeAdmin() {
- // TODO:
- // - install and start docker daemon
- // - Read config that specifies which containers to start how
- // - use thin static backends for node repo, orchestrator, and others
- // - Start core node admin.
+ throw new IllegalStateException("Unknown bootstrap mode: " + config.mode.name());
}
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java
index b8a8bbfd6ad..8ca3ace92e5 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java
@@ -17,7 +17,6 @@ import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec;
import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
-import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer;
import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator;
import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException;
@@ -71,7 +70,7 @@ public class NodeAgentImpl implements NodeAgent {
private final Orchestrator orchestrator;
private final DockerOperations dockerOperations;
private final StorageMaintainer storageMaintainer;
- private final AclMaintainer aclMaintainer;
+ private final Runnable aclMaintainer;
private final Environment environment;
private final Clock clock;
private final Duration timeBetweenEachConverge;
@@ -116,7 +115,7 @@ public class NodeAgentImpl implements NodeAgent {
final Orchestrator orchestrator,
final DockerOperations dockerOperations,
final StorageMaintainer storageMaintainer,
- final AclMaintainer aclMaintainer,
+ final Runnable aclMaintainer,
final Environment environment,
final Clock clock,
final Duration timeBetweenEachConverge) {
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTask.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTask.java
new file mode 100644
index 00000000000..a2ed6a80084
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTask.java
@@ -0,0 +1,61 @@
+// 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;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.regex.Pattern;
+
+public class AddYumRepoTask implements Task {
+ private static final Pattern REPOSITORY_ID_PATTERN = Pattern.compile("^[a-zA-Z_-]+$");
+
+ private final String repositoryId; // e.g. "platform_rpms-latest"
+ private final String name; // e.g. "Platform RPM Latest Repo"
+ private final String baseurl;
+ private final boolean enabled;
+
+ public AddYumRepoTask(String repositoryId,
+ String name,
+ String baseurl,
+ boolean enabled) {
+ this.repositoryId = repositoryId;
+ this.name = name;
+ this.baseurl = baseurl;
+ this.enabled = enabled;
+ validateRepositoryId(repositoryId);
+ }
+
+ @Override
+ public boolean execute(TaskContext context) {
+ Path path = Paths.get("/etc/yum.repos.d",repositoryId + ".repo");
+
+ if (context.getFileSystem().isRegularFile(path)) {
+ return false;
+ }
+
+ WriteFileTask writeFileTask = new WriteFileTask(path, this::getRepoFileContent)
+ .withOwner("root")
+ .withGroup("root")
+ .withPermissions("rw-r--r--");
+
+ return context.executeSubtask(writeFileTask);
+ }
+
+ String getRepoFileContent() {
+ return String.join("\n",
+ "# This file was generated by node admin",
+ "# Do NOT modify this file by hand",
+ "",
+ "[" + repositoryId + "]",
+ "name=" + name,
+ "baseurl=" + baseurl,
+ "enabled=" + (enabled ? 1 : 0),
+ "gpgcheck=0"
+ ) + "\n";
+ }
+
+ static void validateRepositoryId(String repositoryId) {
+ if (!REPOSITORY_ID_PATTERN.matcher(repositoryId).matches()) {
+ throw new IllegalArgumentException("Invalid repository ID '" + repositoryId + "'");
+ }
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTask.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTask.java
new file mode 100644
index 00000000000..522abb81248
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTask.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;
+
+import com.yahoo.vespa.hosted.node.admin.io.FileSystem;
+
+import java.nio.file.Path;
+
+public class MakeDirectoryTask implements Task {
+ private final Path path;
+ private boolean withParents = false;
+
+ public MakeDirectoryTask(Path path) {
+ this.path = path;
+ }
+
+ public MakeDirectoryTask withParents() {
+ this.withParents = true;
+ return this;
+ }
+
+ Path getPath() {
+ return path;
+ }
+
+ boolean getWithParents() {
+ return withParents;
+ }
+
+ private boolean makeDirectory(FileSystem fileSystem,
+ Path directory,
+ boolean withParents) {
+ if (fileSystem.isDirectory(directory)) {
+ return false;
+ }
+
+ if (withParents) {
+ makeDirectory(fileSystem, directory.getParent(), withParents);
+ }
+
+ fileSystem.createDirectory(directory);
+
+ return true;
+ }
+
+ @Override
+ public boolean execute(TaskContext context) {
+ return makeDirectory(context.getFileSystem(), path, withParents);
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/Task.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/Task.java
new file mode 100644
index 00000000000..4d40ac02440
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/Task.java
@@ -0,0 +1,17 @@
+// 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;
+
+import com.yahoo.vespa.hosted.node.admin.io.FileSystem;
+
+public interface Task {
+ interface TaskContext {
+ FileSystem getFileSystem();
+ boolean executeSubtask(Task task);
+ }
+
+ /**
+ * @return Returns false if task was a no-op. Used for informational purposes only.
+ * @throws RuntimeException if task could not be completed.
+ */
+ boolean execute(TaskContext context);
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTask.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTask.java
new file mode 100644
index 00000000000..308a7470d24
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTask.java
@@ -0,0 +1,80 @@
+// 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;
+
+import com.yahoo.vespa.hosted.node.admin.io.FileSystemPath;
+import org.glassfish.jersey.internal.util.Producer;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+public class WriteFileTask implements Task {
+ private final Path path;
+ private final Producer<String> contentProducer;
+
+ private Optional<String> owner = Optional.empty();
+ private Optional<String> group = Optional.empty();
+ private Optional<String> permissions = Optional.empty();
+
+ public WriteFileTask(Path path, Producer<String> contentProducer) {
+ this.path = path;
+ this.contentProducer = contentProducer;
+ }
+
+ public WriteFileTask withOwner(String owner) {
+ this.owner = Optional.of(owner);
+ return this;
+ }
+
+ public WriteFileTask withGroup(String group) {
+ this.group = Optional.of(group);
+ return this;
+ }
+
+ /**
+ * @param permissions of the form "rwxr-x---".
+ */
+ public WriteFileTask withPermissions(String permissions) {
+ this.permissions = Optional.of(permissions);
+ return this;
+ }
+
+ @Override
+ public boolean execute(TaskContext context) {
+ final FileSystemPath fileSystemPath = context.getFileSystem().withPath(path);
+
+ // TODO: Only return false if content, permission, etc would be unchanged.
+ if (fileSystemPath.isRegularFile()) {
+ return false;
+ }
+
+ context.executeSubtask(new MakeDirectoryTask(path.getParent()).withParents());
+
+ String content = contentProducer.call();
+ fileSystemPath.writeUtf8File(content);
+ permissions.ifPresent(fileSystemPath::setPermissions);
+ owner.ifPresent(fileSystemPath::setOwner);
+ group.ifPresent(fileSystemPath::setGroup);
+
+ return true;
+ }
+
+ public Path getPath() {
+ return path;
+ }
+
+ public Producer<String> getContentProducer() {
+ return contentProducer;
+ }
+
+ public Optional<String> getOwner() {
+ return owner;
+ }
+
+ public Optional<String> getGroup() {
+ return group;
+ }
+
+ public Optional<String> getPermissions() {
+ return permissions;
+ }
+}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/package-info.java
new file mode 100644
index 00000000000..433884139f6
--- /dev/null
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/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.hosted.node.admin.task;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemTest.java
new file mode 100644
index 00000000000..6961efc159c
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemTest.java
@@ -0,0 +1,81 @@
+// 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.io;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+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 FileSystemTest {
+ @Rule
+ public final TemporaryFolder folder = new TemporaryFolder();
+
+ private Path root;
+ private Path path;
+
+ private final FileSystem fileSystem = new FileSystem();
+
+ @Before
+ public void setUp() throws Exception {
+ root = folder.getRoot().toPath();
+ path = folder.newFile().toPath();
+ }
+
+ @Test
+ public void isDirectory() throws Exception {
+ assertTrue(fileSystem.isDirectory(root));
+ assertFalse(fileSystem.isDirectory(path));
+ }
+
+ @Test
+ public void isRegularFile() throws Exception {
+ assertTrue(fileSystem.isRegularFile(path));
+ assertFalse(fileSystem.isRegularFile(root));
+ }
+
+ @Test
+ public void createDirectory() throws Exception {
+ Path dir = root.resolve("subdir");
+ fileSystem.createDirectory(dir);
+ assertTrue(fileSystem.isDirectory(dir));
+ }
+
+ @Test
+ public void utf8FileIO() throws Exception {
+ String original = "foo\nbar\n";
+ Path path = root.resolve("example.txt");
+ fileSystem.writeUtf8File(path, original);
+ String fromFile = fileSystem.readUtf8File(path);
+ assertEquals(original, fromFile);
+ }
+
+ @Test
+ public void permissions() throws Exception {
+ String expectedPermissions = "rwxr-x---";
+ fileSystem.setPermissions(path, expectedPermissions);
+ assertEquals(expectedPermissions, fileSystem.getPermissions(path));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void badPermissionsString() {
+ fileSystem.setPermissions(path, "abcdefghi");
+ }
+
+ @Test
+ public void owner() throws Exception {
+ String owner = fileSystem.getOwner(path);
+ fileSystem.setOwner(path, owner);
+ }
+
+ @Test
+ public void group() throws Exception {
+ String group = fileSystem.getGroup(path);
+ fileSystem.setGroup(path, group);
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
index 582992869aa..2d351141103 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java
@@ -8,7 +8,6 @@ import com.yahoo.vespa.hosted.dockerapi.ContainerName;
import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper;
import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations;
import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer;
-import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent;
import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl;
import org.junit.Test;
@@ -48,7 +47,7 @@ public class NodeAdminImplTest {
private final DockerOperations dockerOperations = mock(DockerOperations.class);
private final Function<String, NodeAgent> nodeAgentFactory = mock(NodeAgentFactory.class);
private final StorageMaintainer storageMaintainer = mock(StorageMaintainer.class);
- private final AclMaintainer aclMaintainer = mock(AclMaintainer.class);
+ private final Runnable aclMaintainer = mock(Runnable.class);
private final ManualClock clock = new ManualClock();
private final NodeAdminImpl nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, storageMaintainer, aclMaintainer,
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTaskTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTaskTest.java
new file mode 100644
index 00000000000..3e444f08508
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTaskTest.java
@@ -0,0 +1,70 @@
+// 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;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AddYumRepoTaskTest extends TaskTestBase {
+ private Path expectedPath;
+ private String expectedContent;
+ private AddYumRepoTask task;
+
+ public void setUp() {
+ String repository = "repo-id";
+ String name = "name";
+ String baseUrl = "base-url";
+ boolean enabled = true;
+ expectedContent = "# This file was generated by node admin\n" +
+ "# Do NOT modify this file by hand\n" +
+ "\n" +
+ "[repo-id]\n" +
+ "name=name\n" +
+ "baseurl=base-url\n" +
+ "enabled=1\n" +
+ "gpgcheck=0\n";
+
+ task = new AddYumRepoTask(repository, name, baseUrl, enabled);
+ expectedPath = Paths.get("/etc/yum.repos.d/" + repository + ".repo");
+ }
+
+ @Test
+ public void alreadyExistsIsNoOp() {
+ when(fileSystemMock.isRegularFile(expectedPath)).thenReturn(true);
+ assertFalse(task.execute(contextMock));
+ }
+
+ @Test
+ public void fileContent() {
+ assertEquals(expectedContent, task.getRepoFileContent());
+ }
+
+ @Test
+ public void createsFile() {
+ when(fileSystemMock.isRegularFile(expectedPath)).thenReturn(false);
+ when(contextMock.executeSubtask(any())).thenReturn(true);
+ assertTrue(task.execute(contextMock));
+
+ // Writing a file with the expected content
+ ArgumentCaptor<WriteFileTask> writeFileTaskArgumentCaptor =
+ ArgumentCaptor.forClass(WriteFileTask.class);
+ verify(contextMock, times(1))
+ .executeSubtask(writeFileTaskArgumentCaptor.capture());
+ WriteFileTask writeFileTask = writeFileTaskArgumentCaptor.getValue();
+ assertEquals(expectedPath, writeFileTask.getPath());
+ assertEquals(expectedContent, writeFileTask.getContentProducer().call());
+ assertEquals("rw-r--r--", writeFileTask.getPermissions().get());
+ assertEquals("root", writeFileTask.getOwner().get());
+ assertEquals("root", writeFileTask.getGroup().get());
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTaskTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTaskTest.java
new file mode 100644
index 00000000000..cb4d8ab677c
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTaskTest.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;
+
+import com.yahoo.vespa.hosted.node.admin.io.FileSystem;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MakeDirectoryTaskTest {
+ private final FileSystem fileSystem = mock(FileSystem.class);
+ private final Task.TaskContext context = mock(Task.TaskContext.class);
+ private final Path root = Paths.get("/");
+ private final Path fooDir = root.resolve("foo");
+ private final Path barDir = fooDir.resolve("bar");
+ private final MakeDirectoryTask task = new MakeDirectoryTask(barDir);
+
+ @Before
+ public void setUp() {
+ when(context.getFileSystem()).thenReturn(fileSystem);
+ }
+
+ @Test
+ public void directoryExists() {
+ when(fileSystem.isDirectory(barDir)).thenReturn(true);
+ assertFalse(task.execute(context));
+ }
+
+ @Test
+ public void withParents() {
+ when(fileSystem.isDirectory(barDir)).thenReturn(false);
+ when(fileSystem.isDirectory(fooDir)).thenReturn(false);
+ when(fileSystem.isDirectory(root)).thenReturn(true);
+ assertTrue(task.withParents().execute(context));
+
+ InOrder inOrder = inOrder(fileSystem);
+ inOrder.verify(fileSystem).createDirectory(fooDir);
+ inOrder.verify(fileSystem).createDirectory(barDir);
+ inOrder.verifyNoMoreInteractions();
+ }
+} \ No newline at end of file
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/TaskTestBase.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/TaskTestBase.java
new file mode 100644
index 00000000000..ac24ac6524e
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/TaskTestBase.java
@@ -0,0 +1,26 @@
+// 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;
+
+import com.yahoo.vespa.hosted.node.admin.io.FileSystem;
+import org.junit.Before;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+abstract class TaskTestBase {
+ protected final FileSystem fileSystemMock = mock(FileSystem.class);
+ protected final Task.TaskContext contextMock = mock(Task.TaskContext.class);
+
+ @Before
+ public void baseSetup() {
+ when(contextMock.getFileSystem()).thenReturn(fileSystemMock);
+ when(fileSystemMock.withPath(any())).thenCallRealMethod();
+ setUp();
+ }
+
+ /**
+ * Override this to set up before each test.
+ */
+ void setUp() {}
+}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTaskTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTaskTest.java
new file mode 100644
index 00000000000..9c998cc6fdb
--- /dev/null
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTaskTest.java
@@ -0,0 +1,80 @@
+// 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;
+
+import org.glassfish.jersey.internal.util.Producer;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class WriteFileTaskTest extends TaskTestBase {
+ private final String content = "line1\nline2\n";
+
+ @Test
+ public void testWrite() {
+ Path parentDirectory = Paths.get("/foo");
+ Path path = parentDirectory.resolve("bar");
+
+ @SuppressWarnings("unchecked")
+ Producer<String> contentProducer = (Producer<String>) mock(Producer.class);
+ when(contentProducer.call()).thenReturn(content);
+
+ final String permissions = "rwxr-x---";
+ final String owner = "owner";
+ final String group = "group";
+
+ WriteFileTask task = new WriteFileTask(path, contentProducer)
+ .withPermissions(permissions)
+ .withOwner(owner)
+ .withGroup(group);
+
+ when(fileSystemMock.isRegularFile(path)).thenReturn(false);
+ when(contextMock.executeSubtask(any(MakeDirectoryTask.class))).thenReturn(false);
+
+ assertTrue(task.execute(contextMock));
+
+ verify(contentProducer, times(1)).call();
+ verify(fileSystemMock).writeUtf8File(path, content);
+ verify(fileSystemMock).setPermissions(path, permissions);
+ verify(fileSystemMock).setOwner(path, owner);
+ verify(fileSystemMock).setGroup(path, group);
+
+ // Writing a file with the expected content
+ ArgumentCaptor<MakeDirectoryTask> makeDirectoryTaskCaptor =
+ ArgumentCaptor.forClass(MakeDirectoryTask.class);
+ verify(contextMock, times(1))
+ .executeSubtask(makeDirectoryTaskCaptor.capture());
+
+ MakeDirectoryTask makeDirectoryTask = makeDirectoryTaskCaptor.getValue();
+ assertEquals(parentDirectory, makeDirectoryTask.getPath());
+ assertTrue(makeDirectoryTask.getWithParents());
+ }
+
+ @Test
+ public void fileAlreadyExists() {
+ Path path = Paths.get("foo");
+
+ final String permissions = "rwxr-x---";
+ final String owner = "owner";
+ final String group = "group";
+
+ WriteFileTask task = new WriteFileTask(path, () -> content)
+ .withPermissions(permissions)
+ .withOwner(owner)
+ .withGroup(group);
+
+ when(fileSystemMock.isRegularFile(path)).thenReturn(true);
+
+ assertFalse(task.execute(contextMock));
+ }
+} \ No newline at end of file
diff --git a/yolean/pom.xml b/yolean/pom.xml
index 850daae6fea..a59121cb1f6 100644
--- a/yolean/pom.xml
+++ b/yolean/pom.xml
@@ -51,6 +51,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
+ <source>1.8</source>
+ <target>1.8</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Xlint:-serial</arg>