diff options
39 files changed, 540 insertions, 655 deletions
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java index c94ebc1ab93..f0dc26844e7 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/Timer.java @@ -4,6 +4,8 @@ package com.yahoo.jdisc; import com.google.inject.ImplementedBy; import com.yahoo.jdisc.core.SystemTimer; +import java.time.Instant; + /** * <p>This class provides access to the current time in milliseconds, as viewed by the {@link Container}. Inject an * instance of this class into any component that needs to access time, instead of using @@ -27,4 +29,10 @@ public interface Timer { */ long currentTimeMillis(); + /** + * Convenience method for getting an java.util.Instance from currentTimeMillis(). + */ + default Instant currentTime() { + return Instant.ofEpochMilli(currentTimeMillis()); + } } diff --git a/node-admin/pom.xml b/node-admin/pom.xml index 983e4d3a832..7b3b787b503 100644 --- a/node-admin/pom.xml +++ b/node-admin/pom.xml @@ -115,6 +115,11 @@ <artifactId>mockito-core</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>com.google.jimfs</groupId> + <artifactId>jimfs</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> 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 index 9bed492bd76..9888cca9c7e 100644 --- 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 @@ -1,6 +1,8 @@ // 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; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; + /** * An AdminComponent cannot assume anything about the environment until enable() * is called: Required YUM packages may not have been installed, services @@ -14,6 +16,11 @@ public interface AdminComponent { void enable(); /** + * @return NodeAdminStateUpdater used by the REST API + */ + NodeAdminStateUpdater getNodeAdminStateUpdater(); + + /** * Disable component. May be called more than once. * Must be compatible with component deconstruct(). */ diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/IdempotentTask.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/IdempotentTask.java new file mode 100644 index 00000000000..b6b64dbf5dd --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/IdempotentTask.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; + +/** + * This class is thread unsafe: All method calls MUST be exclusive and serialized. + */ +public interface IdempotentTask { + String name(); + + /** + * Execute an administrative task to converge the system towards some ideal state. + * + * converge() must be idempotent: it may be called any number of times, or + * interrupted at any time e.g. by `kill -9`. The caller must ensure there is at + * most one invocation of converge() on this instance at any given time. + * + * @return false if the system was already converged, i.e. converge() was a no-op. + * @throws RuntimeException (or a subclass) if the task is unable to converge. + */ + boolean converge(TaskContext context); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskComponent.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskComponent.java new file mode 100644 index 00000000000..c54f9ee00c8 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskComponent.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.component; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.chain.ChainedComponent; + +public abstract class TaskComponent extends ChainedComponent implements IdempotentTask { + protected TaskComponent(ComponentId id) { + super(id); + } + + public String name() { + return getIdString(); + } + + public abstract boolean converge(TaskContext context); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskContext.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskContext.java new file mode 100644 index 00000000000..9def627e87f --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/TaskContext.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; + +import java.nio.file.FileSystem; +import java.util.EnumSet; +import java.util.logging.Logger; + +public interface TaskContext { + enum Cloud { YAHOO, AWS } + Cloud cloud(); + + enum Role { TENANT_DOCKER_HOST, CONFIG_SERVER_DOCKER_HOST } + EnumSet<Role> roles(); + default boolean hasRole(Role role) { + return roles().contains(role); + } + + FileSystem fileSystem(); + + void logSystemModification(Logger logger, String actionDescription); +} 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 deleted file mode 100644 index bfec341e05c..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemPath.java +++ /dev/null @@ -1,68 +0,0 @@ -// 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 index 8ba93e769e5..db0313583db 100644 --- 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 @@ -18,6 +18,7 @@ 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.provider.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; import com.yahoo.vespa.hosted.node.admin.util.Environment; @@ -41,7 +42,7 @@ public class DockerAdminComponent implements AdminComponent { private ConfigServerHttpRequestExecutor requestExecutor; - private Optional<NodeAdminStateUpdater> nodeAdminStateUpdater = Optional.empty(); + private Optional<NodeAdminStateUpdaterImpl> nodeAdminStateUpdater = Optional.empty(); public DockerAdminComponent(ConfigServerConfig configServerConfig, NodeAdminConfig config, @@ -108,7 +109,7 @@ public class DockerAdminComponent implements AdminComponent { metricReceiver, clock); - nodeAdminStateUpdater = Optional.of(new NodeAdminStateUpdater( + nodeAdminStateUpdater = Optional.of(new NodeAdminStateUpdaterImpl( nodeRepository, orchestrator, storageMaintainer, @@ -133,6 +134,7 @@ public class DockerAdminComponent implements AdminComponent { // TODO: Also stop docker } + @Override public NodeAdminStateUpdater getNodeAdminStateUpdater() { return nodeAdminStateUpdater.get(); } 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 0be31ffdbae..df26bc2dee5 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 @@ -7,18 +7,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class NodeAdminConfig { private static final ObjectMapper mapper = new ObjectMapper(); /** - * A list of components to enable instead of the default. + * If null, the default admin component will be used. */ - @JsonProperty("components") - public List<String> components = new ArrayList<>(); + @JsonProperty("main-component") + public String mainComponent = null; public enum Mode { aws_tenant, 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 4ee84d12b1d..3e9cb239890 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 @@ -10,11 +10,9 @@ import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.ConfigServerConfig; import com.yahoo.vespa.hosted.node.admin.component.AdminComponent; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -24,8 +22,8 @@ import java.util.stream.Collectors; * - It will "start" (only) the necessary components. * - Other components MUST NOT try to start (typically in constructor) since the features * they provide is NOT WANTED and possibly destructive, and/or the environment may be - * incompatible. For instance, trying to contact the Docker daemon too early will - * be fatal: the node admin may not have installed and started the docker daemon. + * incompatible. For instance, trying to contact the Docker daemon too early will be + * fatal: the node admin may not have installed and started the docker daemon. */ public class NodeAdminMain implements AutoCloseable { private static final Logger logger = Logger.getLogger(NodeAdminMain.class.getName()); @@ -36,9 +34,7 @@ public class NodeAdminMain implements AutoCloseable { private final MetricReceiverWrapper metricReceiver; private final ClassLocking classLocking; - private List<AdminComponent> enabledComponents = new ArrayList<>(); - - private Optional<DockerAdminComponent> dockerAdmin = Optional.empty(); + private AdminComponent mainAdminComponent = null; public NodeAdminMain(ComponentRegistry<AdminComponent> adminRegistry, ConfigServerConfig configServerConfig, @@ -59,53 +55,46 @@ public class NodeAdminMain implements AutoCloseable { public void start() { NodeAdminConfig config = getConfig(); + mainAdminComponent = selectAdminComponent(config); + mainAdminComponent.enable(); + } - if (config.components.isEmpty()) { - dockerAdmin = Optional.of(new DockerAdminComponent( - configServerConfig, config, docker, metricReceiver, classLocking)); - enable(dockerAdmin.get()); - } else { - logger.log(LogLevel.INFO, () -> { - String registeredComponentsList = adminRegistry - .allComponentsById().keySet().stream() - .map(ComponentId::stringValue) - .collect(Collectors.joining(", ")); - - String requestedComponentsList = config.components.stream() - .collect(Collectors.joining(", ")); - - return String.format( - "Components registered = '%s', enabled = '%s'", - registeredComponentsList, - requestedComponentsList); - }); + private AdminComponent selectAdminComponent(NodeAdminConfig config) { + if (config.mainComponent == null) { + return new DockerAdminComponent(configServerConfig, config, docker, metricReceiver, classLocking); + } - for (String componentSpecificationString : config.components) { - AdminComponent component = - adminRegistry.getComponent(componentSpecificationString); - if (component == null) { - throw new IllegalArgumentException("There is no component named '" + - componentSpecificationString + "'"); - } - enable(component); - } + logger.log(LogLevel.INFO, () -> { + String registeredComponentsList = adminRegistry + .allComponentsById().keySet().stream() + .map(ComponentId::stringValue) + .collect(Collectors.joining(", ")); + + return String.format( + "Components registered = '%s', enabled = '%s'", + registeredComponentsList, + config.mainComponent); + }); + + AdminComponent component = adminRegistry.getComponent(config.mainComponent); + if (component == null) { + throw new IllegalArgumentException("There is no component named '" + + config.mainComponent + "'"); } - } - private void enable(AdminComponent component) { - component.enable(); - enabledComponents.add(component); + return component; } @Override public void close() { - int i = enabledComponents.size(); - while (i --> 0) { - enabledComponents.remove(i).disable(); + if (mainAdminComponent != null) { + mainAdminComponent.disable(); + mainAdminComponent = null; } } public NodeAdminStateUpdater getNodeAdminStateUpdater() { - return dockerAdmin.get().getNodeAdminStateUpdater(); + assert mainAdminComponent != null : "start() hasn't been called yet"; + return mainAdminComponent.getNodeAdminStateUpdater(); } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdater.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java index 5ec041298fb..e4e66b57186 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdater.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java @@ -12,6 +12,7 @@ import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; 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; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.util.HttpException; import com.yahoo.vespa.hosted.provision.Node; @@ -32,16 +33,16 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import static com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater.State.RESUMED; -import static com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater.State.SUSPENDED_NODE_ADMIN; -import static com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater.State.TRANSITIONING; +import static com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater.State.RESUMED; +import static com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater.State.SUSPENDED_NODE_ADMIN; +import static com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater.State.TRANSITIONING; /** * Pulls information from node repository and forwards containers to run to node admin. * * @author dybis, stiankri */ -public class NodeAdminStateUpdater { +public class NodeAdminStateUpdaterImpl implements NodeAdminStateUpdater { static final Duration FREEZE_CONVERGENCE_TIMEOUT = Duration.ofMinutes(5); private final AtomicBoolean terminated = new AtomicBoolean(false); @@ -67,7 +68,7 @@ public class NodeAdminStateUpdater { private Optional<ClassLock> classLock; private Instant lastTick; - public NodeAdminStateUpdater( + public NodeAdminStateUpdaterImpl( NodeRepository nodeRepository, Orchestrator orchestrator, StorageMaintainer storageMaintainer, @@ -111,8 +112,7 @@ public class NodeAdminStateUpdater { return this.getClass().getSimpleName() + "@" + Integer.toString(System.identityHashCode(this)); } - public enum State { TRANSITIONING, RESUMED, SUSPENDED_NODE_ADMIN, SUSPENDED} - + @Override public Map<String, Object> getDebugPage() { Map<String, Object> debug = new LinkedHashMap<>(); synchronized (monitor) { @@ -142,6 +142,7 @@ public class NodeAdminStateUpdater { } } + @Override public boolean setResumeStateAndCheckIfResumed(State wantedState) { synchronized (monitor) { if (this.wantedState != wantedState) { @@ -309,7 +310,7 @@ public class NodeAdminStateUpdater { classLocking.interrupt(); - // First we need to stop NodeAdminStateUpdater thread to make sure no new NodeAgents are spawned + // First we need to stop NodeAdminStateUpdaterImpl thread to make sure no new NodeAgents are spawned signalWorkToBeDone(); specVerifierScheduler.shutdown(); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java index 9a667c29fb3..a5146fcae09 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminProvider.java @@ -7,10 +7,9 @@ import com.yahoo.concurrent.classlock.ClassLocking; import com.yahoo.container.di.componentgraph.Provider; import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; +import com.yahoo.vespa.hosted.node.admin.ConfigServerConfig; import com.yahoo.vespa.hosted.node.admin.component.AdminComponent; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminMain; -import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; -import com.yahoo.vespa.hosted.node.admin.ConfigServerConfig; public class NodeAdminProvider implements Provider<NodeAdminStateUpdater> { private final NodeAdminMain nodeAdminMain; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminStateUpdater.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminStateUpdater.java new file mode 100644 index 00000000000..755e1301c12 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/NodeAdminStateUpdater.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.provider; + +import java.util.Map; + +public interface NodeAdminStateUpdater { + enum State { TRANSITIONING, RESUMED, SUSPENDED_NODE_ADMIN, SUSPENDED} + + /** + * Set the wanted state, and return whether the current state equals it. + * Typically, this method should be called repeatedly until current state + * has converged. + */ + boolean setResumeStateAndCheckIfResumed(State wantedState); + + Map<String, Object> getDebugPage(); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/restapi/RestApiHandler.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/restapi/RestApiHandler.java index 03217c85329..a8dcde02ca6 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/restapi/RestApiHandler.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/restapi/RestApiHandler.java @@ -6,10 +6,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; -import com.yahoo.container.logging.AccessLog; import com.yahoo.vespa.hosted.dockerapi.metrics.DimensionMetrics; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; -import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import javax.inject.Inject; import javax.ws.rs.core.MediaType; @@ -17,7 +16,6 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.util.concurrent.Executor; import static com.yahoo.jdisc.http.HttpRequest.Method.GET; import static com.yahoo.jdisc.http.HttpRequest.Method.PUT; 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 deleted file mode 100644 index 522abb81248..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTask.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 4d40ac02440..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/Task.java +++ /dev/null @@ -1,17 +0,0 @@ -// 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 deleted file mode 100644 index 308a7470d24..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTask.java +++ /dev/null @@ -1,80 +0,0 @@ -// 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/util/file/FileWriter.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.java new file mode 100644 index 00000000000..60a7b3482b2 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriter.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.util.file; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; +import org.glassfish.jersey.internal.util.Producer; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +public class FileWriter { + private static final Logger logger = Logger.getLogger(FileWriter.class.getName()); + + private final Path path; + private final Producer<String> contentProducer; + + private Optional<String> owner = Optional.empty(); + private Optional<String> group = Optional.empty(); + private Optional<String> permissions = Optional.empty(); + + public FileWriter(Path path, Producer<String> contentProducer) { + this.path = path; + this.contentProducer = contentProducer; + } + + public FileWriter withOwner(String owner) { + this.owner = Optional.of(owner); + return this; + } + + public FileWriter withGroup(String group) { + this.group = Optional.of(group); + return this; + } + + public FileWriter withPermissions(String permissions) { + this.permissions = Optional.of(permissions); + return this; + } + + public boolean converge(TaskContext context) { + // TODO: Only return false if content, permission, etc would be unchanged. + if (Files.isRegularFile(path)) { + return false; + } + + context.logSystemModification(logger,"Writing file " + path); + + String content = contentProducer.call(); + + UnixPath unixPath = new UnixPath(path); + unixPath.createParents(); + unixPath.writeUtf8File(content); + permissions.ifPresent(unixPath::setPermissions); + owner.ifPresent(unixPath::setOwner); + group.ifPresent(unixPath::setGroup); + + return true; + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java new file mode 100644 index 00000000000..dee5525d42a --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/IOExceptionUtil.java @@ -0,0 +1,34 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import java.io.IOException; +import java.io.UncheckedIOException; + +public class IOExceptionUtil { + public static <T> void uncheck(RunnableThrowingIOException<T> runnable) { + try { + runnable.run(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static <T> T uncheck(SupplierThrowingIOException<T> supplier) { + try { + return supplier.get(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @FunctionalInterface + public interface SupplierThrowingIOException<T> { + T get() throws IOException; + } + + + @FunctionalInterface + public interface RunnableThrowingIOException<T> { + void run() throws IOException; + } +} 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/task/util/file/UnixPath.java index c5c2df9e38e..606f8cfb06e 100644 --- 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/task/util/file/UnixPath.java @@ -1,5 +1,5 @@ // 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; +package com.yahoo.vespa.hosted.node.admin.task.util.file; import java.io.IOException; import java.io.UncheckedIOException; @@ -7,7 +7,7 @@ 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.Paths; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; @@ -15,52 +15,48 @@ 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.time.Instant; 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); +// @Immutable +public class UnixPath { + private final Path path; + + public UnixPath(Path path) { + this.path = path; } - public boolean isDirectory(Path path) { - return path.toFile().isDirectory(); + public UnixPath(String path) { + this(Paths.get(path)); } - public boolean isRegularFile(Path path) { - return path.toFile().isFile(); + public Path toPath() { + return path; } - public void createDirectory(Path path, FileAttribute<?>... attributes) { - uncheck(() -> Files.createDirectory(path, attributes)); + public void createParents() { + uncheck(() -> Files.createDirectories(path.getParent())); } - public String readUtf8File(Path path) { + public String readUtf8File() { byte[] byteContent = uncheck(() -> Files.readAllBytes(path)); return new String(byteContent, StandardCharsets.UTF_8); } - public void writeUtf8File(Path path, String content, OpenOption... options) { + public void writeUtf8File(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()); + public String getPermissions() { + return PosixFilePermissions.toString(getAttributes().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) { + public void setPermissions(String permissions) { Set<PosixFilePermission> permissionSet; try { permissionSet = PosixFilePermissions.fromString(permissions); @@ -72,26 +68,35 @@ public class FileSystem { uncheck(() -> Files.setPosixFilePermissions(path, permissionSet)); } - public String getOwner(Path path) { - return getAttributes(path).owner().getName(); + public String getOwner() { + return getAttributes().owner().getName(); } - public void setOwner(Path path, String owner) { + public void setOwner(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 String getGroup() { + return getAttributes().group().getName(); } - public void setGroup(Path path, String group) { + public void setGroup(String group) { UserPrincipalLookupService service = path.getFileSystem().getUserPrincipalLookupService(); GroupPrincipal principal = uncheck(() -> service.lookupPrincipalByGroupName(group)); uncheck(() -> Files.getFileAttributeView(path, PosixFileAttributeView.class).setGroup(principal)); } + public Instant getLastModifiedTime() { + return uncheck(() -> Files.getLastModifiedTime(path)).toInstant(); + } + + private PosixFileAttributes getAttributes() { + return uncheck(() -> + Files.getFileAttributeView(path, PosixFileAttributeView.class).readAttributes()); + } + @FunctionalInterface private interface SupplierThrowingIOException<T> { T get() throws IOException; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/package-info.java new file mode 100644 index 00000000000..076912c073d --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/file/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.util.file; + +import com.yahoo.osgi.annotation.ExportPackage; 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/util/yum/AddYumRepo.java index a2ed6a80084..9ca1c0286f9 100644 --- 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/util/yum/AddYumRepo.java @@ -1,11 +1,13 @@ // 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; +package com.yahoo.vespa.hosted.node.admin.task.util.yum; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; +import com.yahoo.vespa.hosted.node.admin.task.util.file.FileWriter; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.regex.Pattern; -public class AddYumRepoTask implements Task { +public class AddYumRepo { private static final Pattern REPOSITORY_ID_PATTERN = Pattern.compile("^[a-zA-Z_-]+$"); private final String repositoryId; // e.g. "platform_rpms-latest" @@ -13,10 +15,10 @@ public class AddYumRepoTask implements Task { private final String baseurl; private final boolean enabled; - public AddYumRepoTask(String repositoryId, - String name, - String baseurl, - boolean enabled) { + public AddYumRepo(String repositoryId, + String name, + String baseurl, + boolean enabled) { this.repositoryId = repositoryId; this.name = name; this.baseurl = baseurl; @@ -24,23 +26,18 @@ public class AddYumRepoTask implements Task { 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; - } + public boolean converge(TaskContext context) { + Path path = context.fileSystem().getPath("/etc/yum.repos.d",repositoryId + ".repo"); - WriteFileTask writeFileTask = new WriteFileTask(path, this::getRepoFileContent) + FileWriter fileWriter = new FileWriter(path, this::getRepoFileContent) .withOwner("root") .withGroup("root") .withPermissions("rw-r--r--"); - return context.executeSubtask(writeFileTask); + return fileWriter.converge(context); } - String getRepoFileContent() { + private String getRepoFileContent() { return String.join("\n", "# This file was generated by node admin", "# Do NOT modify this file by hand", @@ -53,7 +50,7 @@ public class AddYumRepoTask implements Task { ) + "\n"; } - static void validateRepositoryId(String repositoryId) { + private 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/package-info.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/package-info.java index 433884139f6..3c5292982d0 100644 --- 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/util/yum/package-info.java @@ -1,5 +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; +package com.yahoo.vespa.hosted.node.admin.task.util.yum; import com.yahoo.osgi.annotation.ExportPackage; diff --git a/node-admin/src/main/sh/node-admin.sh b/node-admin/src/main/sh/node-admin.sh index ff0ea492318..3196ff9fa32 100755 --- a/node-admin/src/main/sh/node-admin.sh +++ b/node-admin/src/main/sh/node-admin.sh @@ -67,16 +67,19 @@ EOF exit 1 } -if (( $# != 1 )); then +if (( $# == 0 )); then Usage fi -case "$1" in +command="$1" +shift + +case "$command" in start) - "$VESPA_HOME"/libexec/vespa/standalone-container.sh start -s node-admin -u root + "$VESPA_HOME"/libexec/vespa/standalone-container.sh start -s node-admin -u root "$@" ;; stop) - "$VESPA_HOME"/libexec/vespa/standalone-container.sh stop -s node-admin -u root + "$VESPA_HOME"/libexec/vespa/standalone-container.sh stop -s node-admin -u root "$@" ;; *) Usage ;; esac diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java index 7a314ff0614..3cfc67824ed 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java @@ -11,7 +11,7 @@ import com.yahoo.vespa.hosted.node.admin.docker.DockerOperationsImpl; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; -import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdaterImpl; 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.util.Environment; @@ -40,7 +40,7 @@ public class DockerTester implements AutoCloseable { final CallOrderVerifier callOrderVerifier = new CallOrderVerifier(); final Docker dockerMock = new DockerMock(callOrderVerifier); final NodeRepoMock nodeRepositoryMock = new NodeRepoMock(callOrderVerifier); - final NodeAdminStateUpdater nodeAdminStateUpdater; + final NodeAdminStateUpdaterImpl nodeAdminStateUpdater; final NodeAdmin nodeAdmin; private final OrchestratorMock orchestratorMock = new OrchestratorMock(callOrderVerifier); @@ -66,7 +66,7 @@ public class DockerTester implements AutoCloseable { Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock, orchestratorMock, dockerOperations, storageMaintainer, aclMaintainer, environment, clock, NODE_AGENT_SCAN_INTERVAL); nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, storageMaintainer, aclMaintainer, mr, Clock.systemUTC()); - nodeAdminStateUpdater = new NodeAdminStateUpdater(nodeRepositoryMock, orchestratorMock, storageMaintainer, + nodeAdminStateUpdater = new NodeAdminStateUpdaterImpl(nodeRepositoryMock, orchestratorMock, storageMaintainer, nodeAdmin, "basehostname", clock, NODE_ADMIN_CONVERGE_STATE_INTERVAL, new ClassLocking()); nodeAdminStateUpdater.start(); } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RebootTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RebootTest.java index 0b5d67dd13c..2db93ba4203 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RebootTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RebootTest.java @@ -5,7 +5,8 @@ import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperationsImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; -import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdaterImpl; import com.yahoo.vespa.hosted.provision.Node; import org.junit.Ignore; import org.junit.Test; @@ -41,7 +42,7 @@ public class RebootTest { "createContainerCommand with DockerImage { imageId=dockerImage }, HostName: host1.test.yahoo.com, ContainerName { name=host1 }", "updateNodeAttributes with HostName: host1.test.yahoo.com, NodeAttributes{restartGeneration=1, rebootGeneration=null, dockerImage=dockerImage, vespaVersion='null'}"); - NodeAdminStateUpdater updater = dockerTester.nodeAdminStateUpdater; + NodeAdminStateUpdaterImpl updater = dockerTester.nodeAdminStateUpdater; assertThat(updater.setResumeStateAndCheckIfResumed(NodeAdminStateUpdater.State.SUSPENDED), is(Optional.of("Not all node agents are frozen."))); diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java index 23d55bd947c..a0e122d99fc 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java @@ -15,7 +15,8 @@ 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.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; -import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; +import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdaterImpl; 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; @@ -77,7 +78,7 @@ public class RunInContainerTest { @Before public void startContainer() throws Exception { - // To test the initial NodeAdminStateUpdater convergence towards RESUME, orchestrator should + // To test the initial NodeAdminStateUpdaterImpl convergence towards RESUME, orchestrator should // deny permission to resume for parent host, otherwise it'll converge to RESUME before REST // handler comes up doThrow(new RuntimeException()).when(orchestratorMock).resume(parentHostname); @@ -241,7 +242,7 @@ public class RunInContainerTest { (hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock, orchestratorMock, dockerOperationsMock, storageMaintainer, aclMaintainer, environment, Clock.systemUTC(), NODE_AGENT_SCAN_INTERVAL); private final NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperationsMock, nodeAgentFactory, storageMaintainer, aclMaintainer, mr, Clock.systemUTC()); - private final NodeAdminStateUpdater nodeAdminStateUpdater = new NodeAdminStateUpdater(nodeRepositoryMock, + private final NodeAdminStateUpdaterImpl nodeAdminStateUpdater = new NodeAdminStateUpdaterImpl(nodeRepositoryMock, orchestratorMock, storageMaintainer, nodeAdmin, "localhost.test.yahoo.com", Clock.systemUTC(), NODE_ADMIN_CONVERGE_STATE_INTERVAL, new ClassLocking()); public NodeAdminProviderWithMocks() { @@ -249,7 +250,7 @@ public class RunInContainerTest { } @Override - public NodeAdminStateUpdater get() { + public NodeAdminStateUpdaterImpl get() { return nodeAdminStateUpdater; } 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 deleted file mode 100644 index 6961efc159c..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/io/FileSystemTest.java +++ /dev/null @@ -1,81 +0,0 @@ -// 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/NodeAdminStateUpdaterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java index 1058278a02c..7920d0cad29 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java @@ -7,6 +7,7 @@ import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; 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; +import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.provision.Node; import org.junit.Test; @@ -32,11 +33,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; /** - * Basic test of NodeAdminStateUpdater + * Basic test of NodeAdminStateUpdaterImpl * * @author freva */ -public class NodeAdminStateUpdaterTest { +public class NodeAdminStateUpdaterImplTest { private final NodeRepository nodeRepository = mock(NodeRepository.class); private final Orchestrator orchestrator = mock(Orchestrator.class); private final StorageMaintainer storageMaintainer = mock(StorageMaintainer.class); @@ -45,7 +46,7 @@ public class NodeAdminStateUpdaterTest { private final ManualClock clock = new ManualClock(); private final Duration convergeStateInterval = Duration.ofSeconds(30); - private final NodeAdminStateUpdater refresher = spy(new NodeAdminStateUpdater( + private final NodeAdminStateUpdaterImpl refresher = spy(new NodeAdminStateUpdaterImpl( nodeRepository, orchestrator, storageMaintainer, nodeAdmin, parentHostname, clock, convergeStateInterval, null)); @@ -102,7 +103,7 @@ public class NodeAdminStateUpdaterTest { // The second orchestration failure happens after the freeze convergence timeout, // and so SHOULD call setFrozen(false) when(nodeAdmin.setFrozen(eq(true))).thenReturn(true); - when(nodeAdmin.subsystemFreezeDuration()).thenReturn(NodeAdminStateUpdater.FREEZE_CONVERGENCE_TIMEOUT.plusMinutes(1)); + when(nodeAdmin.subsystemFreezeDuration()).thenReturn(NodeAdminStateUpdaterImpl.FREEZE_CONVERGENCE_TIMEOUT.plusMinutes(1)); doThrow(new RuntimeException("Cannot allow to suspend because some reason")).doNothing() .when(orchestrator).suspend(eq(parentHostname)); tickAfter(35); 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 deleted file mode 100644 index 3e444f08508..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/AddYumRepoTaskTest.java +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index cb4d8ab677c..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/MakeDirectoryTaskTest.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index ac24ac6524e..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/TaskTestBase.java +++ /dev/null @@ -1,26 +0,0 @@ -// 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 deleted file mode 100644 index 9c998cc6fdb..00000000000 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/WriteFileTaskTest.java +++ /dev/null @@ -1,80 +0,0 @@ -// 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/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java new file mode 100644 index 00000000000..ca4eabf855b --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/FileWriterTest.java @@ -0,0 +1,51 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; +import org.junit.Test; + +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.time.Instant; + +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.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class FileWriterTest { + private final FileSystem fileSystem = TestFileSystem.create(); + + @Test + public void testWrite() { + final String content = "content"; + final String permissions = "rwxr-xr-x"; + final String owner = "owner"; + final String group = "group"; + + Path path = fileSystem.getPath("/opt/vespa/tmp/file.txt"); + FileWriter writer = new FileWriter(path, () -> content) + .withPermissions(permissions) + .withOwner(owner) + .withGroup(group); + TaskContext context = mock(TaskContext.class); + assertTrue(writer.converge(context)); + verify(context, times(1)).logSystemModification(any(), eq("Writing file " + path)); + + UnixPath unixPath = new UnixPath(path); + assertEquals(content, unixPath.readUtf8File()); + assertEquals(permissions, unixPath.getPermissions()); + assertEquals(owner, unixPath.getOwner()); + assertEquals(group, unixPath.getGroup()); + Instant fileTime = unixPath.getLastModifiedTime(); + + // Second time is a no-op. + assertFalse(writer.converge(context)); + assertEquals(fileTime, unixPath.getLastModifiedTime()); + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestFileSystem.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestFileSystem.java new file mode 100644 index 00000000000..465cb671a97 --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/TestFileSystem.java @@ -0,0 +1,24 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Feature; +import com.google.common.jimfs.Jimfs; +import com.google.common.jimfs.PathType; + +import java.nio.file.FileSystem; + +public class TestFileSystem { + public static FileSystem create() { + // This configuration is based on Configuration.unix(), except: + // - Use "posix" attribute view which is necessary for permissions, owner, and group. + Configuration configuration = Configuration.builder(PathType.unix()) + .setRoots("/") + .setWorkingDirectory("/work") + .setAttributeViews("posix") + .setSupportedFeatures(Feature.LINKS, Feature.SYMBOLIC_LINKS, Feature.SECURE_DIRECTORY_STREAM, Feature.FILE_CHANNEL) + .build(); + return Jimfs.newFileSystem(configuration); + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java new file mode 100644 index 00000000000..821c6397ee7 --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/file/UnixPathTest.java @@ -0,0 +1,65 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.task.util.file; + +import org.junit.Test; + +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class UnixPathTest { + final FileSystem fileSystem = TestFileSystem.create(); + + @Test + public void createParents() throws Exception { + Path parentDirectory = fileSystem.getPath("/a/b/c"); + Path filePath = parentDirectory.resolve("bar"); + UnixPath path = new UnixPath(filePath); + + assertFalse(Files.exists(fileSystem.getPath("/a"))); + path.createParents(); + assertTrue(Files.exists(parentDirectory)); + } + + @Test + public void utf8File() throws Exception { + String original = "foo\nbar\n"; + UnixPath path = new UnixPath(fileSystem.getPath("example.txt")); + path.writeUtf8File(original); + String fromFile = path.readUtf8File(); + assertEquals(original, fromFile); + } + + @Test + public void permissions() throws Exception { + String expectedPermissions = "rwxr-x---"; + UnixPath path = new UnixPath(fileSystem.getPath("file.txt")); + path.writeUtf8File("foo"); + path.setPermissions(expectedPermissions); + assertEquals(expectedPermissions, path.getPermissions()); + } + + @Test(expected = IllegalArgumentException.class) + public void badPermissionsString() { + new UnixPath(fileSystem.getPath("file.txt")).setPermissions("abcdefghi"); + } + + @Test + public void owner() throws Exception { + FileSystem fs = TestFileSystem.create(); + Path path = fs.getPath("file.txt"); + UnixPath unixPath = new UnixPath(path); + unixPath.writeUtf8File("foo"); + + unixPath.setOwner("owner"); + assertEquals("owner", unixPath.getOwner()); + + unixPath.setGroup("group"); + assertEquals("group", unixPath.getGroup()); + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepoTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepoTest.java new file mode 100644 index 00000000000..7b6ab91345b --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/yum/AddYumRepoTest.java @@ -0,0 +1,57 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.node.admin.task.util.yum; + +import com.yahoo.vespa.hosted.node.admin.component.TaskContext; +import com.yahoo.vespa.hosted.node.admin.task.util.file.TestFileSystem; +import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath; +import org.junit.Test; + +import java.nio.file.FileSystem; +import java.time.Instant; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AddYumRepoTest { + @Test + public void converge() throws Exception { + String repositoryId = "repoid"; + String name = "name"; + String baseurl = "http://foo.com/bar"; + boolean enabled = true; + + AddYumRepo addYumRepo = new AddYumRepo( + repositoryId, + name, + baseurl, + enabled); + + TaskContext context = mock(TaskContext.class); + + FileSystem fileSystem = TestFileSystem.create(); + when(context.fileSystem()).thenReturn(fileSystem); + + assertTrue(addYumRepo.converge(context)); + + UnixPath unixPath = new UnixPath(fileSystem.getPath("/etc/yum.repos.d/" + repositoryId + ".repo")); + String content = unixPath.readUtf8File(); + assertEquals("# This file was generated by node admin\n" + + "# Do NOT modify this file by hand\n" + + "\n" + + "[repoid]\n" + + "name=name\n" + + "baseurl=http://foo.com/bar\n" + + "enabled=1\n" + + "gpgcheck=0\n", content); + Instant lastModifiedTime = unixPath.getLastModifiedTime(); + + // Second time is a no-op + assertFalse(addYumRepo.converge(context)); + assertEquals(lastModifiedTime, unixPath.getLastModifiedTime()); + } + +}
\ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index de02f5085ea..46b8c90baef 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -607,6 +607,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>com.google.jimfs</groupId> + <artifactId>jimfs</artifactId> + <version>1.1</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.osgi</groupId> <artifactId>org.osgi.compendium</artifactId> <version>4.3.0</version> diff --git a/standalone-container/src/main/sh/standalone-container.sh b/standalone-container/src/main/sh/standalone-container.sh index dd0693f6f85..02c71beaac5 100755 --- a/standalone-container/src/main/sh/standalone-container.sh +++ b/standalone-container/src/main/sh/standalone-container.sh @@ -67,6 +67,7 @@ Manage Vespa standalone jdisc container service. Options: -u USER Run as USER. Overrides any VESPA_USER environment variable. -s SERVICE The service name. + -- ARGS... Pass the rest of the arguments (ARGS) to the Java invocation EOF exit 1 @@ -89,16 +90,19 @@ FixDataDirectory() { StartCommand() { local service="$1" shift - - if (( $# > 0 )); then - Fail "Too many arguments" - fi + local -a jvm_arguments=("$@") local service_regex='^[0-9a-zA-Z_-]+$' if ! [[ "$service" =~ $service_regex ]]; then Fail "Service must match regex '$service_regex'" fi + local pidfile="$VESPA_HOME/var/run/$service.pid" + if test -r "$pidfile"; then + echo "$service is already running as PID $(< "$pidfile") according to $pidfile" + return + fi + # common setup export VESPA_SERVICE_NAME="$service" @@ -135,6 +139,7 @@ StartCommand() { FixDataDirectory "$bundlecachedir" java \ + "${jvm_arguments[@]}" \ -Xms128m -Xmx2048m \ -XX:+PreserveFramePointer \ -XX:+HeapDumpOnOutOfMemoryError \ @@ -267,6 +272,7 @@ Main() { local service="standalone/container" local user="$VESPA_USER" + local -a jvm_arguments=() while (( $# > 0 )); do case "$1" in @@ -279,6 +285,11 @@ Main() { user="$2" shift 2 ;; + --) + shift + jvm_arguments=("$@") + break + ;; *) break ;; esac done @@ -300,7 +311,7 @@ Main() { case "$command" in help) Usage ;; - start) StartCommand "$service" "$@" ;; + start) StartCommand "$service" "${jvm_arguments[@]}" ;; stop) StopCommand "$user" "$service" "$@" ;; *) Fail "Unknown command '$command'" ;; esac |