From 19e0de0f04b17776f068a193e31e4abac6e67deb Mon Sep 17 00:00:00 2001 From: Karl DeBisschop Date: Mon, 20 Apr 2020 15:34:48 -0400 Subject: [PATCH] Improve logging and handling of service-based nodes * Replace static calls to RancherWebSocketListener in RancherNodeExecutorPlugin * Run node executor on services with multiple containers * Increase support for handling services in file copier * Better logging of errors in http client class Co-authored-by: Karl DeBisschop --- build.gradle | 5 +- .../bioraft/rundeck/rancher/HttpClient.java | 38 ++++- .../rundeck/rancher/RancherAddService.java | 17 +- .../rundeck/rancher/RancherCredentials.java | 28 +++ .../rundeck/rancher/RancherFileCopier.java | 161 +++++++++++------- .../rancher/RancherNodeExecutorPlugin.java | 52 ++++-- .../rancher/RancherResourceModelSource.java | 19 ++- .../com/bioraft/rundeck/rancher/Service.java | 5 + .../rundeck/rancher/HttpClientTest.java | 53 +++++- .../rancher/RancherFileCopierTest.java | 102 ++++++++--- .../RancherNodeExecutorPluginTest.java | 43 ++++- 11 files changed, 395 insertions(+), 128 deletions(-) create mode 100644 src/main/java/com/bioraft/rundeck/rancher/RancherCredentials.java create mode 100644 src/main/java/com/bioraft/rundeck/rancher/Service.java diff --git a/build.gradle b/build.gradle index 2ba7eb8..a538f48 100644 --- a/build.gradle +++ b/build.gradle @@ -77,10 +77,11 @@ configurations { dependencies { implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.1' - implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.13.1' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.12.0' implementation ( - 'org.rundeck:rundeck-core:3.2.+', + 'org.rundeck:rundeck-core:3.2.4-20200318', + 'org.rundeck:rundeck-storage-api:3.2.4-20200318', ) testImplementation group: 'junit', name: 'junit', version:'4.12' diff --git a/src/main/java/com/bioraft/rundeck/rancher/HttpClient.java b/src/main/java/com/bioraft/rundeck/rancher/HttpClient.java index 677d1fc..3eff9e8 100644 --- a/src/main/java/com/bioraft/rundeck/rancher/HttpClient.java +++ b/src/main/java/com/bioraft/rundeck/rancher/HttpClient.java @@ -1,5 +1,6 @@ package com.bioraft.rundeck.rancher; +import com.dtolabs.rundeck.core.execution.ExecutionLogger; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; @@ -8,10 +9,14 @@ import java.util.Map; import java.util.Objects; +import static com.dtolabs.rundeck.core.Constants.DEBUG_LEVEL; + public class HttpClient { private String accessKey; private String secretKey; + + private ExecutionLogger logger; protected final OkHttpClient client; public HttpClient() { @@ -30,6 +35,10 @@ public void setSecretKey(String secretKey) { this.secretKey = secretKey; } + public void setLogger(ExecutionLogger logger) { + this.logger = logger; + } + protected JsonNode get(String url) throws IOException { return this.get(url, null); } @@ -44,7 +53,8 @@ protected JsonNode get(String url, Map query) throws IOException Response response = client.newCall(builder.build()).execute(); // Since URL comes from the Rancher server itself, assume there are no redirects. if (response.code() >= 300) { - throw new IOException("API get failed" + response.message()); + logError(response); + throw new IOException("API get failed: " + response.message()); } ObjectMapper mapper = new ObjectMapper(); if (response.body() == null) { @@ -66,7 +76,8 @@ protected JsonNode post(String url, String data) throws IOException { Response response = client.newCall(builder.build()).execute(); // Since URL comes from the Rancher server itself, assume there are no redirects. if (response.code() >= 300) { - throw new IOException("API post failed" + response.message()); + logError(response); + throw new IOException("API post failed: " + response.message()); } ObjectMapper mapper = new ObjectMapper(); if (response.body() == null) { @@ -74,4 +85,27 @@ protected JsonNode post(String url, String data) throws IOException { } return mapper.readTree(response.body().string()); } + + private void logError(Response response) { + if (logger == null) { + return; + } + ResponseBody body = response.body(); + if (body != null) { + String text; + try { + text = body.string(); + } catch (IOException e) { + return; + } + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(text); + logger.log(DEBUG_LEVEL, node.toPrettyString()); + } catch (IOException e) { + logger.log(DEBUG_LEVEL, text); + } + body.close(); + } + } } diff --git a/src/main/java/com/bioraft/rundeck/rancher/RancherAddService.java b/src/main/java/com/bioraft/rundeck/rancher/RancherAddService.java index 7fcb68b..868a41d 100644 --- a/src/main/java/com/bioraft/rundeck/rancher/RancherAddService.java +++ b/src/main/java/com/bioraft/rundeck/rancher/RancherAddService.java @@ -35,8 +35,7 @@ import static com.bioraft.rundeck.rancher.Constants.*; import static com.bioraft.rundeck.rancher.Errors.ErrorCause.*; -import static com.dtolabs.rundeck.core.Constants.ERR_LEVEL; -import static com.dtolabs.rundeck.core.Constants.INFO_LEVEL; +import static com.dtolabs.rundeck.core.Constants.*; import static com.dtolabs.rundeck.core.plugins.configuration.PropertyResolverFactory.FRAMEWORK_PREFIX; import static com.dtolabs.rundeck.core.plugins.configuration.PropertyResolverFactory.PROJECT_PREFIX; import static com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants.CODE_SYNTAX_MODE; @@ -78,7 +77,7 @@ public class RancherAddService implements StepPlugin { @PluginProperty(title = "Secret IDs", description = "List of secrets IDs, space or comma separated") private String secrets; - final HttpClient client; + private final HttpClient client; public RancherAddService () { this.client = new HttpClient(); @@ -123,6 +122,7 @@ public void executeStep(final PluginStepContext context, final Map map = ImmutableMap.builder() + Map map = ImmutableMap.builder().put("type", "service") .put("assignServiceIpAddress", false).put("startOnCreate", true).put("name", serviceName) - .put("stackId", stackId).put("rancherCompose", "").put("launchConfig", mapBuilder.build()).build(); + .put("scale", 1).put("serviceIndexStrategy", "deploymentUnitBased") + .put("launchConfig", mapBuilder.build()) + .put("stackId", stackId).build(); + String payload = mapper.writeValueAsString(map); + logger.log(DEBUG_LEVEL, mapper.readTree(payload).toPrettyString()); JsonNode serviceResult = client.post(spec, map); logger.log(INFO_LEVEL, "Success!"); logger.log(INFO_LEVEL, "New service ID:" + serviceResult.path("id").asText()); logger.log(INFO_LEVEL, "New service name:" + serviceResult.path("name").asText()); } catch (IOException e) { - throw new StepException("Failed posting to " + spec, e, INVALID_CONFIGURATION); + throw new StepException("Failed at " + spec + "\n" + e.getMessage(), e, INVALID_CONFIGURATION); } } diff --git a/src/main/java/com/bioraft/rundeck/rancher/RancherCredentials.java b/src/main/java/com/bioraft/rundeck/rancher/RancherCredentials.java new file mode 100644 index 0000000..fe8d349 --- /dev/null +++ b/src/main/java/com/bioraft/rundeck/rancher/RancherCredentials.java @@ -0,0 +1,28 @@ +package com.bioraft.rundeck.rancher; + +import com.dtolabs.rundeck.core.execution.ExecutionContext; + +import java.io.IOException; +import java.util.Map; + +import static com.bioraft.rundeck.rancher.Constants.CONFIG_ACCESSKEY_PATH; +import static com.bioraft.rundeck.rancher.Constants.CONFIG_SECRETKEY_PATH; + +public class RancherCredentials { + private String accessKey; + private String secretKey; + + public RancherCredentials(ExecutionContext context, Map nodeAttributes) throws IOException { + Storage storage = new Storage(context); + accessKey = storage.loadStoragePathData(nodeAttributes.get(CONFIG_ACCESSKEY_PATH)); + secretKey = storage.loadStoragePathData(nodeAttributes.get(CONFIG_SECRETKEY_PATH)); + } + + public String getAccessKey() { + return accessKey; + } + + public String getSecretKey() { + return secretKey; + } +} diff --git a/src/main/java/com/bioraft/rundeck/rancher/RancherFileCopier.java b/src/main/java/com/bioraft/rundeck/rancher/RancherFileCopier.java index 3aae9c4..9c43f75 100644 --- a/src/main/java/com/bioraft/rundeck/rancher/RancherFileCopier.java +++ b/src/main/java/com/bioraft/rundeck/rancher/RancherFileCopier.java @@ -111,36 +111,22 @@ public String copyScriptContent(ExecutionContext context, String script, INodeEn return copyFile(context, null, null, script, node, destination); } - private String copyFile(final ExecutionContext context, final File scriptfile, final InputStream input, + private String copyFile(final ExecutionContext context, final File scriptFile, final InputStream input, final String script, final INodeEntry node, final String destinationPath) throws FileCopierException { Map nodeAttributes = node.getAttributes(); - if (nodeAttributes.get("type").equals("service")) { - String message = "File copier is not currently supported for services"; - throw new FileCopierException(message, UNSUPPORTED_NODE_TYPE); - } - String accessKey; - String secretKey; - try { - Storage storage = new Storage(context); - accessKey = storage.loadStoragePathData(nodeAttributes.get(CONFIG_ACCESSKEY_PATH)); - secretKey = storage.loadStoragePathData(nodeAttributes.get(CONFIG_SECRETKEY_PATH)); - } catch (IOException e) { - throw new FileCopierException(e.getMessage(), AUTHENTICATION_FAILURE); - } - - String remotefile = getRemoteFile(destinationPath, context, node, scriptfile); + String remoteFile = getRemoteFile(destinationPath, context, node, scriptFile); // write to a local temp file or use the input file - final File localTempfile = (null != scriptfile) ? scriptfile + final File localTempFile = (null != scriptFile) ? scriptFile : BaseFileCopier.writeTempFile(context, null, input, script); // Copy the file over ExecutionLogger logger = context.getExecutionLogger(); - String absPath = localTempfile.getAbsolutePath(); - String message = "copying file: '" + absPath + "' to: '" + node.getNodename() + ":" + remotefile + "'"; + String absPath = localTempFile.getAbsolutePath(); + String message = "copying file: '" + absPath + "' to: '" + node.getNodename() + ":" + remoteFile + "'"; logger.log(DEBUG_LEVEL, message); Framework framework = context.getFramework(); @@ -152,90 +138,135 @@ private String copyFile(final ExecutionContext context, final File scriptfile, f try { String result; - if (searchPath == null || searchPath.equals("")) { - result = copyViaApi(context, nodeAttributes, accessKey, secretKey, localTempfile, remotefile); + // Use API for multiple containers for now, because we do not have externalId list in service-type of node. + if (searchPath == null || searchPath.equals("") || nodeAttributes.get("type").equals("service")) { + result = copyViaApi(context, nodeAttributes, localTempFile, remoteFile); } else { - result = copyViaCli(context, nodeAttributes, accessKey, secretKey, localTempfile, remotefile, searchPath); + CliCopier cliCopier = new CliCopier(localTempFile, searchPath, context, nodeAttributes); + result = cliCopier.copyViaCli(nodeAttributes, remoteFile, searchPath); } - context.getExecutionLogger().log(DEBUG_LEVEL, "Copied '" + localTempfile + "' to '" + result ); + context.getExecutionLogger().log(DEBUG_LEVEL, "Copied '" + localTempFile + "' to '" + result); return result; + } catch (IOException e) { + throw new FileCopierException(e.getMessage(), IO_EXCEPTION); } finally { - if (null == scriptfile && !ScriptfileUtils.releaseTempFile(localTempfile)) { + if (null == scriptFile && !ScriptfileUtils.releaseTempFile(localTempFile)) { context.getExecutionListener().log(Constants.WARN_LEVEL, - "Unable to remove local temp file: " + localTempfile.getAbsolutePath()); + "Unable to remove local temp file: " + localTempFile.getAbsolutePath()); } } } - private String getRemoteFile(final String destinationPath, final ExecutionContext context, final INodeEntry node, final File scriptfile) { + private String getRemoteFile(final String destinationPath, final ExecutionContext context, final INodeEntry node, final File scriptFile) { if (null == destinationPath) { String identity = null != context.getDataContext() && null != context.getDataContext().get("job") ? context.getDataContext().get("job").get("execid") : null; return BaseFileCopier.generateRemoteFilepathForNode(node, context.getFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), - context.getFramework(), (null != scriptfile ? scriptfile.getName() : "dispatch-script"), null, + context.getFramework(), (null != scriptFile ? scriptFile.getName() : "dispatch-script"), null, identity); } else { return destinationPath; } } - private String copyViaCli(final ExecutionContext context, Map nodeAttributes, String accessKey, String secretKey, - File localTempFile, String remotefile, String searchPath) throws FileCopierException { - ExecutionLogger logger = context.getExecutionLogger(); - logger.log(DEBUG_LEVEL, "PATH: '" + searchPath + "'"); + private String copyViaApi(final ExecutionContext context, Map nodeAttributes, File file, String destination) + throws FileCopierException { + try { + RancherCredentials rancherCredentials = new RancherCredentials(context, nodeAttributes); + String[] instanceIds; + if (nodeAttributes.get("type").equals("service")) { + // "self": "https://rancher.example.com/v2-beta/projects/1a10/services/1s56" + // "execute": "https://rancher.example.com/v2-beta/projects/1a10/containers/1i234/?action=execute", + String self = nodeAttributes.get(NODE_ATT_SELF); + instanceIds = nodeAttributes.get("instanceIds").split(","); + for (String instance : instanceIds) { + String url = self.replaceFirst("/services/[0-9]+s[0-9]+", "/containers/" + instance + "/?action=execute"); + webSocketListener.putFile(url, rancherCredentials.getAccessKey(), rancherCredentials.getSecretKey(), file, destination); + context.getExecutionLogger().log(DEBUG_LEVEL, "PUT: '" + file + "' to " + url); + } + } else { + String url = nodeAttributes.get("execute"); + webSocketListener.putFile(url, rancherCredentials.getAccessKey(), rancherCredentials.getSecretKey(), file, destination); + context.getExecutionLogger().log(DEBUG_LEVEL, "PUT: '" + file + "'"); + } + } catch (IOException e) { + throw new FileCopierException(e.getMessage(), CONNECTION_FAILURE); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FileCopierException(e.getMessage(), CONNECTION_FAILURE); + } + return destination; + } - String path = localTempFile.getAbsolutePath(); - String instance = nodeAttributes.get("externalId"); - String[] command = {"rancher", "docker", "cp", path, instance + ":" + remotefile}; - boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); - try { + private static class CliCopier { + private final String searchPath; + private final String accessKey; + private final String secretKey; + + private final String path; + private final ExecutionLogger logger; + + public CliCopier(File localTempFile, String searchPath, final ExecutionContext context, Map nodeAttributes) + throws IOException { + this.path = localTempFile.getAbsolutePath(); + this.searchPath = searchPath; + RancherCredentials rancherCredentials = new RancherCredentials(context, nodeAttributes); + this.accessKey = rancherCredentials.getAccessKey(); + this.secretKey = rancherCredentials.getSecretKey(); + + this.logger = context.getExecutionLogger(); + } + + public String copyViaCli(Map nodeAttributes, String remoteFile, String searchPath) + throws FileCopierException { + boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + if (isWindows) { + throw new FileCopierException("Windows is not currently supported.", UNSUPPORTED_OPERATING_SYSTEM); + } + + logger.log(DEBUG_LEVEL, "PATH: '" + searchPath + "'"); + + try { + String instance = nodeAttributes.get("externalId"); + String[] command = {"rancher", "docker", "cp", path, instance + ":" + remoteFile}; + logger.log(DEBUG_LEVEL, "OS Copy: '" + String.join(" ", command) + "'"); + this.toOneHostByCli(instance, remoteFile, nodeAttributes); + } catch (IOException e) { + throw new FileCopierException("Child process IO Exception", IO_EXCEPTION, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FileCopierException("Child process interrupted", INTERRUPTED, e); + } + + return remoteFile; + } + + private void toOneHostByCli(String instance, String remoteFile, Map nodeAttributes) + throws IOException, InterruptedException { + String[] command = {"rancher", "docker", "cp", path, instance + ":" + remoteFile}; ProcessBuilder builder = new ProcessBuilder(); Map environment = builder.environment(); - logger.log(DEBUG_LEVEL, "CMD: '" + String.join(" ", command) + "'"); + logger.log(DEBUG_LEVEL, "CLI Copy: '" + String.join(" ", command) + "'"); environment.put("PATH", searchPath); environment.put("RANCHER_ENVIRONMENT", nodeAttributes.get("environment")); environment.put("RANCHER_DOCKER_HOST", nodeAttributes.get("hostname")); environment.put("RANCHER_URL", nodeAttributes.get("execute").replaceFirst("/projects/.*$", "")); environment.put("RANCHER_ACCESS_KEY", accessKey); environment.put("RANCHER_SECRET_KEY", secretKey); - if (isWindows) { - throw new FileCopierException("Windows is not currently supported.", UNSUPPORTED_OPERATING_SYSTEM); - } else { - builder.command(command); - } - logger.log(DEBUG_LEVEL, "CMD: '" + String.join(" ", command) + "'"); + builder.command(command); builder.directory(new File(System.getProperty("java.io.tmpdir"))); Process process = builder.start(); StreamGobbler streamGobbler = new StreamGobbler(process.getInputStream(), System.out::println); Executors.newSingleThreadExecutor().submit(streamGobbler); int exitCode = process.waitFor(); - assert exitCode == 0; - } catch (IOException e) { - throw new FileCopierException("Child process IO Exception", IO_EXCEPTION, e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FileCopierException("Child process interrupted", INTERRUPTED, e); + if (exitCode != 0) { + throw new IOException("CLI process failed"); + } } - return remotefile; - } - - private String copyViaApi(final ExecutionContext context, Map nodeAttributes, String accessKey, String secretKey, File file, - String destination) throws FileCopierException { - try { - String url = nodeAttributes.get("execute"); - webSocketListener.putFile(url, accessKey, secretKey, file, destination); - context.getExecutionLogger().log(DEBUG_LEVEL, "PUT: '" + file + "'"); - } catch (IOException e) { - throw new FileCopierException(e.getMessage(), CONNECTION_FAILURE); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new FileCopierException(e.getMessage(), CONNECTION_FAILURE); - } - return destination; } private static class StreamGobbler implements Runnable { diff --git a/src/main/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPlugin.java b/src/main/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPlugin.java index da1e952..15294d7 100644 --- a/src/main/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPlugin.java +++ b/src/main/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPlugin.java @@ -67,13 +67,28 @@ public class RancherNodeExecutorPlugin implements NodeExecutor, Describable { private RancherWebSocketListener socketListener; private RancherWebSocketListener fileCopier; private Storage storage; + private String accessKey; + private String secretKey; + private ExecutionListener listener; + private ExecutionLogger logger; + private INodeEntry node; + /** + * Constructor called by RunDeck. + */ public RancherNodeExecutorPlugin() { socketListener = new RancherWebSocketListener(); fileCopier = new RancherWebSocketListener(); this.storage = new Storage(); } + /** + * Constructor used for injecting unit testing mocks. + * + * @param rancherWebSocketListener Socket used to execute the command. + * @param webSocketFileCopier Socket used to fetch the PID + status file. + * @param storage Rancher secret storage service. + */ public RancherNodeExecutorPlugin(RancherWebSocketListener rancherWebSocketListener, RancherWebSocketListener webSocketFileCopier, Storage storage) { socketListener = rancherWebSocketListener; fileCopier = webSocketFileCopier; @@ -88,15 +103,9 @@ public Description getDescription() { @Override public NodeExecutorResult executeCommand(final ExecutionContext context, final String[] command, final INodeEntry node) { + this.node = node; Map nodeAttributes = node.getAttributes(); - if (nodeAttributes.get("type").equals("service")) { - String message = "Node executor is not currently supported for services"; - return NodeExecutorResultImpl.createFailure(StepFailureReason.PluginFailed, message, node); - } - - String accessKey; - String secretKey; try { storage.setExecutionContext(context); accessKey = storage.loadStoragePathData(nodeAttributes.get(CONFIG_ACCESSKEY_PATH)); @@ -105,11 +114,8 @@ public NodeExecutorResult executeCommand(final ExecutionContext context, final S return NodeExecutorResultImpl.createFailure(StepFailureReason.IOFailure, e.getMessage(), node); } - ExecutionListener listener = context.getExecutionListener(); - - ExecutionLogger logger = context.getExecutionLogger(); - - String url = nodeAttributes.get("execute"); + listener = context.getExecutionListener(); + logger = context.getExecutionLogger(); Map jobContext = context.getDataContext().get("job"); String temp = this.baseName(command, jobContext); @@ -117,6 +123,28 @@ public NodeExecutorResult executeCommand(final ExecutionContext context, final S int timeout = ResolverUtil.resolveIntProperty(RANCHER_CONFIG_EXECUTOR_TIMEOUT, 300, node, context.getFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()), context.getFramework()); + + if (nodeAttributes.get("type").equals("service")) { + // "self": "https://rancher.example.com/v2-beta/projects/1a10/services/1s56" + // "execute": "https://rancher.example.com/v2-beta/projects/1a10/containers/1i234/?action=execute", + String self = nodeAttributes.get(NODE_ATT_SELF); + String[] instanceIds = nodeAttributes.get("instanceIds").split(","); + NodeExecutorResult result = NodeExecutorResultImpl.createFailure(StepFailureReason.PluginFailed, "No containers in node", node); + for (String instance: instanceIds) { + String url = self.replaceFirst("/services/[0-9]+s[0-9]+", "/containers/" + instance + "/?action=execute"); + result = runJob(url, command, temp, timeout); + if (!result.isSuccess()) { + break; + } + } + return result; + } else { + String url = nodeAttributes.get("execute"); + return runJob(url, command, temp, timeout); + } + } + + private NodeExecutorResult runJob(String url, String[] command, String temp, int timeout) { try { logger.log(DEBUG_LEVEL, "Running " + String.join(" ", command)); socketListener.thisRunJob(url, accessKey, secretKey, command, listener, temp, timeout); diff --git a/src/main/java/com/bioraft/rundeck/rancher/RancherResourceModelSource.java b/src/main/java/com/bioraft/rundeck/rancher/RancherResourceModelSource.java index 320997e..bab11c9 100644 --- a/src/main/java/com/bioraft/rundeck/rancher/RancherResourceModelSource.java +++ b/src/main/java/com/bioraft/rundeck/rancher/RancherResourceModelSource.java @@ -24,6 +24,7 @@ import com.dtolabs.rundeck.core.resources.ResourceModelSource; import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -237,7 +238,7 @@ private class RancherNode { protected final NodeEntryImpl nodeEntry; // Tag set for the node being built. - protected final HashSet tagset; + protected final HashSet tagSet; // Labels read from the node. protected JsonNode labels; @@ -245,9 +246,9 @@ private class RancherNode { public RancherNode() { nodeEntry = new NodeEntryImpl(); if (tags == null) { - tagset = new HashSet<>(); + tagSet = new HashSet<>(); } else { - tagset = new HashSet<>(Arrays.asList(tags.split("\\s*,\\s*"))); + tagSet = new HashSet<>(Arrays.asList(tags.split("\\s*,\\s*"))); } } @@ -262,7 +263,7 @@ protected void processLabels(JsonNode node) { String[] parts = stackService.split("/"); nodeEntry.setAttribute("stack", parts[0]); nodeEntry.setAttribute("service", parts[1]); - tagset.add(parts[1]); + tagSet.add(parts[1]); } if (labels.hasNonNull(NODE_LABEL_STACK_NAME)) { @@ -283,9 +284,9 @@ protected void processLabels(JsonNode node) { this.setTagForLabel(label, value); } if (node.hasNonNull(NODE_IMAGE_UUID)) { - tagset.add(node.get(NODE_IMAGE_UUID).asText().replaceFirst("^[^/]+/", "")); + tagSet.add(node.get(NODE_IMAGE_UUID).asText().replaceFirst("^[^/]+/", "")); } - nodeEntry.setTags(tagset); + nodeEntry.setTags(tagSet); } /** @@ -330,7 +331,7 @@ private void setAttributeForLabel(String label, String value) { */ private void setTagForLabel(String label, String value) { if (tagInclude.length() > 0 && label.matches(tagInclude)) { - tagset.add(value); + tagSet.add(value); } } @@ -401,6 +402,10 @@ public NodeEntryImpl getNodeEntry(String environmentName, JsonNode node) { // Storage path for Rancher API secret key. String secretKeyPath = CONFIG_SECRETKEY_PATH; nodeEntry.setAttribute(secretKeyPath, configuration.getProperty(secretKeyPath)); + StringBuilder instanceIds = new StringBuilder(); + node.path("instanceIds").elements() + .forEachRemaining(instance -> instanceIds.append(instance.asText()).append(",")); + nodeEntry.setAttribute("instanceIds", StringUtils.chomp(instanceIds.toString(), ",")); nodeEntry.setAttribute(NODE_ATT_SELF, node.path(NODE_ATT_LINKS).path(NODE_ATT_SELF).asText()); return nodeEntry; } diff --git a/src/main/java/com/bioraft/rundeck/rancher/Service.java b/src/main/java/com/bioraft/rundeck/rancher/Service.java new file mode 100644 index 0000000..24adcc6 --- /dev/null +++ b/src/main/java/com/bioraft/rundeck/rancher/Service.java @@ -0,0 +1,5 @@ +package com.bioraft.rundeck.rancher; + +public class Service { + +} diff --git a/src/test/java/com/bioraft/rundeck/rancher/HttpClientTest.java b/src/test/java/com/bioraft/rundeck/rancher/HttpClientTest.java index 45e38d9..d355057 100644 --- a/src/test/java/com/bioraft/rundeck/rancher/HttpClientTest.java +++ b/src/test/java/com/bioraft/rundeck/rancher/HttpClientTest.java @@ -1,5 +1,6 @@ package com.bioraft.rundeck.rancher; +import com.dtolabs.rundeck.core.execution.ExecutionLogger; import com.fasterxml.jackson.databind.JsonNode; import okhttp3.Call; import okhttp3.OkHttpClient; @@ -13,11 +14,10 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; import static com.bioraft.rundeck.rancher.TestHelper.*; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class HttpClientTest { @@ -27,6 +27,9 @@ public class HttpClientTest { @Mock Call call; + @Mock + ExecutionLogger logger; + HttpClient subject; static class HttpClientImplementation extends HttpClient { @@ -121,7 +124,7 @@ public void testPostWithNullResponse() throws IOException { } @Test(expected = IOException.class) - public void testPostMethodReturns301() throws IOException { + public void testPostMethodReturns301NoLooger() throws IOException { String url = "https://api.example.com/"; String text = "{\"key\": \"value\"}"; when(call.execute()).thenReturn(response(text, 301)); @@ -129,4 +132,46 @@ public void testPostMethodReturns301() throws IOException { assertEquals(1, json.size()); assertEquals("value", json.get("key").asText()); } + + @Test + public void testPostMethodReturns301() throws IOException { + subject.setLogger(logger); + String url = "https://api.example.com/"; + String text = "{\"key\": \"value\"}"; + when(call.execute()).thenReturn(response(text, 301)); + try { + subject.post(url, text); + fail("This post should have thrown an exception"); + } catch (IOException e) { + verify(logger, times(1)).log(anyInt(), anyString()); + } + } + + @Test + public void testPostMethodReturns301NoBody() throws IOException { + subject.setLogger(logger); + String url = "https://api.example.com/"; + String text = "{\"key\": \"value\"}"; + when(call.execute()).thenReturn(response(null, 301)); + try { + subject.post(url, text); + fail("This post should have thrown an exception"); + } catch (IOException e) { + verify(logger, times(0)).log(anyInt(), anyString()); + } + } + + @Test + public void testPostMethodReturns301NotJsonContent() throws IOException { + subject.setLogger(logger); + String url = "https://api.example.com/"; + String text = "{\"key\": \"value\"}"; + when(call.execute()).thenReturn(response("not json", 301)); + try { + subject.post(url, text); + fail("This post should have thrown an exception"); + } catch (IOException e) { + verify(logger, times(1)).log(anyInt(), anyString()); + } + } } diff --git a/src/test/java/com/bioraft/rundeck/rancher/RancherFileCopierTest.java b/src/test/java/com/bioraft/rundeck/rancher/RancherFileCopierTest.java index 5e59e13..a94c223 100644 --- a/src/test/java/com/bioraft/rundeck/rancher/RancherFileCopierTest.java +++ b/src/test/java/com/bioraft/rundeck/rancher/RancherFileCopierTest.java @@ -2,10 +2,12 @@ import com.dtolabs.rundeck.core.common.Framework; import com.dtolabs.rundeck.core.common.INodeEntry; +import com.dtolabs.rundeck.core.common.ProjectManager; import com.dtolabs.rundeck.core.execution.ExecutionContext; import com.dtolabs.rundeck.core.execution.service.FileCopierException; import com.dtolabs.rundeck.core.storage.ResourceMeta; import com.dtolabs.rundeck.core.storage.StorageTree; +import com.dtolabs.rundeck.core.utils.IPropertyLookup; import com.dtolabs.rundeck.plugins.PluginLogger; import org.junit.Before; import org.junit.Test; @@ -54,29 +56,54 @@ public class RancherFileCopierTest { @Mock ResourceMeta contents; + @Mock + ProjectManager projectManager; + +// @Mock +// IRundeckProject rundeckProject; + + @Mock + IPropertyLookup propertyLookup; + Map map; @Before public void setUp() { - map = Stream - .of(new String[][]{{"services", "https://rancher.example.com/v2-beta/"}, - {"self", "https://rancher.example.com/v2-beta/"}, - {"type", "container"}, - {CONFIG_ACCESSKEY_PATH, "keys/rancher/access.key"}, - {CONFIG_SECRETKEY_PATH, "keys/rancher/secret.key"}}) - .collect(Collectors.toMap(data -> data[0], data -> data[1])); - when(node.getAttributes()).thenReturn(map); when(treeResource.getContents()).thenReturn(contents); when(storageTree.getResource(anyString())).thenReturn(treeResource); when(executionContext.getStorageTree()).thenReturn(storageTree); when(executionContext.getExecutionLogger()).thenReturn(logger); when(executionContext.getFramework()).thenReturn(framework); // when(framework.getProjectProperty(anyString(), anyString())).thenReturn(""); + // when(framework.createFrameworkNode()).thenReturn(host); // when(framework.getProperty(anyString())).thenReturn("/tmp/"); // when(host.getOsFamily()).thenReturn("unix"); } + private void setUpContainer() { + map = Stream + .of(new String[][]{ + {"type", "container"}, + {"services", "https://rancher.example.com/v2-beta/projects/1a8/containers/1i160059/services"}, + {"self", "https://rancher.example.com/v2-beta/projects/1a8/containers/1i22"}, + {CONFIG_ACCESSKEY_PATH, "keys/rancher/access.key"}, + {CONFIG_SECRETKEY_PATH, "keys/rancher/secret.key"}}) + .collect(Collectors.toMap(data -> data[0], data -> data[1])); + when(node.getAttributes()).thenReturn(map); + } + + private void setUpService() { + map = Stream + .of(new String[][]{{"type", "service"}, + {"self", "https://rancher.example.com/v2-beta/projects/1a8/services/1s7 "}, + {"instanceIds", "1i21,1i22"}, + {CONFIG_ACCESSKEY_PATH, "keys/rancher/access.key"}, + {CONFIG_SECRETKEY_PATH, "keys/rancher/secret.key"}}) + .collect(Collectors.toMap(data -> data[0], data -> data[1])); + when(node.getAttributes()).thenReturn(map); + } + @Test public void validateDefaultConstructor() { RancherFileCopier subject = new RancherFileCopier(); @@ -86,7 +113,8 @@ public void validateDefaultConstructor() { } @Test - public void testCopyFile() throws FileCopierException, IOException, InterruptedException { + public void testCopyFileToContainer() throws FileCopierException, IOException, InterruptedException { + this.setUpContainer(); RancherFileCopier subject = new RancherFileCopier(listener); String destination = "/tmp/file.txt"; File file = new File( @@ -96,28 +124,21 @@ public void testCopyFile() throws FileCopierException, IOException, InterruptedE verify(listener, times(1)).putFile(eq(null), anyString(), anyString(), eq(file), anyString()); } -// @Test(expected = FileCopierException.class) -// public void copyScriptThrowListener() throws FileCopierException, IOException, InterruptedException { -// doThrow(new IOException()).when(listener).putFile(anyString(), anyString(), anyString(), any(File.class), anyString()); -// RancherFileCopier subject = new RancherFileCopier(listener); -// String destination = "/tmp/file.txt"; -// String file = "foo"; -// subject.copyScriptContent(executionContext, file, node, destination); -// } - - @Test(expected = FileCopierException.class) - public void servicesAreUnsupported() throws FileCopierException { + @Test + public void testCopyFileToService() throws FileCopierException, IOException, InterruptedException { + this.setUpService(); RancherFileCopier subject = new RancherFileCopier(listener); String destination = "/tmp/file.txt"; - map.put("type", "service"); File file = new File( Objects.requireNonNull(getClass().getClassLoader().getResource("stack.json")).getFile() ); subject.copyFile(executionContext, file, node, destination); + verify(listener, times(2)).putFile(startsWith("https://rancher.example.com/v2-beta/projects/1a8/containers/"), anyString(), anyString(), eq(file), anyString()); } @Test(expected = FileCopierException.class) public void throwExceptionIfNoPasswordPath() throws FileCopierException { + this.setUpContainer(); File file = new File( Objects.requireNonNull(getClass().getClassLoader().getResource("stack.json")).getFile() ); @@ -130,16 +151,51 @@ public void throwExceptionIfNoPasswordPath() throws FileCopierException { } @Test(expected = FileCopierException.class) - public void throwListenerException() throws FileCopierException, IOException, InterruptedException { + public void throwListenerException() throws FileCopierException { + this.setUpContainer(); File file = new File( Objects.requireNonNull(getClass().getClassLoader().getResource("stack.json")).getFile() ); -// doThrow(new IOException()).when(listener).putFile(anyString(), anyString(), anyString(), eq(file), anyString()); map.put(CONFIG_ACCESSKEY_PATH, null); map.put(CONFIG_SECRETKEY_PATH, null); RancherFileCopier subject = new RancherFileCopier(listener); String destination = "/tmp/file.txt"; subject.copyFile(executionContext, file, node, destination); + } +// @Test(expected = FileCopierException.class) +// public void throwListenerExceptionIfInterrupted() throws FileCopierException, InterruptedException, IOException { +// this.setUpContainer(); +// File file = new File( +// Objects.requireNonNull(getClass().getClassLoader().getResource("stack.json")).getFile() +// ); +// doThrow(new InterruptedException()).when(listener).putFile(anyString(), anyString(), anyString(), eq(file), anyString()); +// RancherFileCopier subject = new RancherFileCopier(listener); +// String destination = "/tmp/file.txt"; +// subject.copyFile(executionContext, file, node, destination); +// +// } + + @Test + public void testCopyFileToContainerNoPath() throws FileCopierException, IOException, InterruptedException { + this.setUpContainer(); + +// when(rundeckProject.hasProperty("project.project.file-copy-destination-dir")).thenReturn(true); +// when(rundeckProject.getProperty("project.project.file-copy-destination-dir")).thenReturn("/tmp"); +// +// when(propertyLookup.hasProperty("framework.framework.file-copy-destination-dir")).thenReturn(true); +// when(propertyLookup.getProperty("framework.framework.file-copy-destination-dir")).thenReturn("/tmp"); + + when(framework.getPropertyLookup()).thenReturn(propertyLookup); + when(framework.getFrameworkProjectMgr()).thenReturn(projectManager); + +// when(projectManager.getFrameworkProject(anyString())).thenReturn(rundeckProject); + + RancherFileCopier subject = new RancherFileCopier(listener); + File file = new File( + Objects.requireNonNull(getClass().getClassLoader().getResource("stack.json")).getFile() + ); + subject.copyFile(executionContext, file, node, null); + verify(listener, times(1)).putFile(eq(null), anyString(), anyString(), eq(file), anyString()); } } diff --git a/src/test/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPluginTest.java b/src/test/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPluginTest.java index 39e8a03..ad7c15e 100644 --- a/src/test/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPluginTest.java +++ b/src/test/java/com/bioraft/rundeck/rancher/RancherNodeExecutorPluginTest.java @@ -85,16 +85,45 @@ public void testDescription() { } @Test - public void serviceIsNotYetSupported() { - RancherNodeExecutorPlugin nodeExecutorPlugin = new RancherNodeExecutorPlugin(); + public void serviceIsNotYetSupported() throws IOException, InterruptedException { String[] command = {"ls"}; + String instance1 = "1i10"; + String instance2 = "1i11"; + String expectedUrl1 = "https://rancher.example.com/v2-beta/projects/1a10/containers/" + instance1 + "/?action=execute"; + String expectedUrl2 = "https://rancher.example.com/v2-beta/projects/1a10/containers/" + instance2 + "/?action=execute"; + String instanceIds = instance1 + "," + instance2; + String accessKey = "access"; + String secretKey = "secret"; + nodeAttributes.put("type", "service"); + nodeAttributes.put(CONFIG_ACCESSKEY_PATH, "access_key"); + nodeAttributes.put(CONFIG_SECRETKEY_PATH, "secret_key"); + nodeAttributes.put(RANCHER_CONFIG_EXECUTOR_TIMEOUT, "30"); + nodeAttributes.put(NODE_ATT_SELF, "https://rancher.example.com/v2-beta/projects/1a10/services/1s56"); + nodeAttributes.put("instanceIds", instanceIds); + when(node.getAttributes()).thenReturn(nodeAttributes); - NodeExecutorResult result = nodeExecutorPlugin.executeCommand(executionContext, command, node); - String message = "Node executor is not currently supported for services"; - assertEquals(message, result.getFailureMessage()); - assertEquals(StepFailureReason.PluginFailed, result.getFailureReason()); - assertEquals(-1, result.getResultCode()); + + when(webSocketFileCopier.thisGetFile(eq(expectedUrl1), eq(accessKey), eq(secretKey), anyString())) + .thenReturn("123 0"); + when(webSocketFileCopier.thisGetFile(eq(expectedUrl2), eq(accessKey), eq(secretKey), anyString())) + .thenReturn("123 0"); + + when(storage.loadStoragePathData(nodeAttributes.get(CONFIG_ACCESSKEY_PATH))).thenReturn(accessKey); + when(storage.loadStoragePathData(nodeAttributes.get(CONFIG_SECRETKEY_PATH))).thenReturn(secretKey); + + when(executionContext.getFramework()).thenReturn(framework); + when(framework.getFrameworkProjectMgr()).thenReturn(projectManager); + + when(executionContext.getExecutionLogger()).thenReturn(executionLogger); + when(executionContext.getDataContext()).thenReturn(dataContext); + + RancherNodeExecutorPlugin subject = new RancherNodeExecutorPlugin(rancherWebSocketListener, webSocketFileCopier, storage); + subject.executeCommand(executionContext, command, node); + verify(executionLogger, times(6)).log(anyInt(), anyString()); + verify(rancherWebSocketListener, times(2)).thisRunJob(any(), eq(accessKey), eq(secretKey), any(), any(), anyString(), anyInt()); + verify(webSocketFileCopier, times(1)).thisGetFile(eq(expectedUrl1), anyString(), anyString(), anyString()); + verify(webSocketFileCopier, times(1)).thisGetFile(eq(expectedUrl2), anyString(), anyString(), anyString()); } @Test