diff options
author | HÃ¥kon Hallingstad <hakon@oath.com> | 2018-01-09 00:25:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-09 00:25:51 +0100 |
commit | 6db81d28a8aca631e10c0f539f7727a9b9b2e6b7 (patch) | |
tree | 27a1976b105ca303e563d499bf56cb02c69ce1bf | |
parent | 2710414fabfa7f17d88a48fd01c775e1b2b2ddeb (diff) | |
parent | af2833b52d307b6536ad646a42be6c4ce4ac7ca0 (diff) |
Merge pull request #4563 from vespa-engine/hakonhall/introduce-task-and-admincomponent
Introduce Task and AdminComponent
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> |