From ffe716d19a2a5030ffedb9df1048059bf7994038 Mon Sep 17 00:00:00 2001 From: Wooseung Sim Date: Tue, 23 May 2017 09:26:02 -0400 Subject: [PATCH] Project creation --- build.gradle | 55 +++++++++ gradle.properties | 1 + resource-templates/plugin.xml | 31 +++++ settings.gradle | 2 + .../go/plugin/task/powershell/Constants.java | 27 +++++ .../task/powershell/PowerShellTask.java | 104 ++++++++++++++++ .../powershell/PowerShellTaskExtension.java | 114 ++++++++++++++++++ .../ws/go/plugin/task/powershell/Utils.java | 71 +++++++++++ .../task/powershell/config/PluginConfig.java | 45 +++++++ .../config/model/CpuArchitecture.java | 24 ++++ .../powershell/config/model/ErrorActions.java | 47 ++++++++ .../task/powershell/config/model/Field.java | 62 ++++++++++ src/main/resources/task.template.html | 20 +++ 13 files changed, 603 insertions(+) create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 resource-templates/plugin.xml create mode 100644 settings.gradle create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/Constants.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/PowerShellTask.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/PowerShellTaskExtension.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/Utils.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/config/PluginConfig.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/config/model/CpuArchitecture.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/config/model/ErrorActions.java create mode 100644 src/main/java/net/ws/go/plugin/task/powershell/config/model/Field.java create mode 100644 src/main/resources/task.template.html diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cebb371 --- /dev/null +++ b/build.gradle @@ -0,0 +1,55 @@ +group 'net.soti.go.plugin' + +apply plugin: 'java' + +sourceCompatibility = 1.8 // java 8 +targetCompatibility = 1.8 + +repositories { + maven { + url 'http://sotiartifacts:8081/artifactory/globalmaven/' + } + mavenLocal() +} + +jar { + from(configurations.compile) { + into "lib/" + } + archiveName project.name + '.' + project.version +'.jar' +} + +version = rootProject.version +def projectName = rootProject.group + '.' + rootProject.name +def pluginDesc = [ + id : projectName, + version : rootProject.version, + goCdVersion: '16.12.0', + name : 'SOTI Powershell GoCD task plugin', + description: 'GoCD task plugin that execute powershell', + vendorName : 'SOTI Inc', + vendorUrl : 'https://www.soti.net' +] + +println "ProjectName = {$rootProject.name}, this.name = {$name}, version {$rootProject.version}" +processResources { + from("resource-templates") { + filesMatching('plugin.xml') { + println pluginDesc + expand pluginDesc + } + } +} + +dependencies { + compileOnly group: 'cd.go.plugin', name: 'go-plugin-api', version: '16.11.0' + compile group: 'com.google.guava', name: 'guava', version: '19.0' + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4' + compile group: 'commons-io', name: 'commons-io', version: '2.4' + compile group: 'com.google.code.gson', name: 'gson', version: '2.7' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2' + + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3' + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.0.96-beta' +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..bd60cdf --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.0-SNAPSHOT \ No newline at end of file diff --git a/resource-templates/plugin.xml b/resource-templates/plugin.xml new file mode 100644 index 0000000..ce1de8f --- /dev/null +++ b/resource-templates/plugin.xml @@ -0,0 +1,31 @@ + + + + + ${name} + ${version} + ${goCdVersion} + + Windows + + ${description} + + ${vendorName} + ${vendorUrl} + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..474430e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'task.powershell' + diff --git a/src/main/java/net/ws/go/plugin/task/powershell/Constants.java b/src/main/java/net/ws/go/plugin/task/powershell/Constants.java new file mode 100644 index 0000000..04bb634 --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/Constants.java @@ -0,0 +1,27 @@ +package net.ws.go.plugin.task.powershell; + +import java.util.Collections; + +import com.thoughtworks.go.plugin.api.GoPluginIdentifier; + +public interface Constants { + // The type of this extension + String EXTENSION_TYPE = "task"; + + // The extension point API version that this plugin understands + String API_VERSION = "1.0"; + + String REQUEST_CONFIGURATION = "configuration"; + String REQUEST_VALIDATION = "validate"; + String REQUEST_TASK_VIEW = "view"; + String REQUEST_EXECUTION = "execute"; + + GoPluginIdentifier PLUGIN_IDENTIFIER = new GoPluginIdentifier(EXTENSION_TYPE, Collections.singletonList(API_VERSION)); + + String ERROR_ACTION_KEY = "errorAction"; + String SCRIPT_KEY = "script"; + String CPU_ARCHITECTURE_KEY = "cpuArch"; + + String SUCCESS = "success"; + String MESSAGE = "message"; +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTask.java b/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTask.java new file mode 100644 index 0000000..5c2df72 --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTask.java @@ -0,0 +1,104 @@ +package net.ws.go.plugin.task.powershell; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import net.ws.go.plugin.task.powershell.config.PluginConfig; + +import com.google.gson.GsonBuilder; +import com.thoughtworks.go.plugin.api.GoApplicationAccessor; +import com.thoughtworks.go.plugin.api.GoPlugin; +import com.thoughtworks.go.plugin.api.GoPluginIdentifier; +import com.thoughtworks.go.plugin.api.annotation.Extension; +import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException; +import com.thoughtworks.go.plugin.api.logging.Logger; +import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; +import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import org.apache.http.HttpStatus; + +import static net.ws.go.plugin.task.powershell.Constants.*; +import static net.ws.go.plugin.task.powershell.PowerShellTaskExtension.*; +import static net.ws.go.plugin.task.powershell.Utils.isWindows; + +@Extension +public class PowerShellTask implements GoPlugin { + static final Logger LOG = Logger.getLoggerFor(PowerShellTask.class); + + @Override + public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { + // do nothing + } + + @Override + public GoPluginApiResponse handle(GoPluginApiRequest requestMessage) throws UnhandledRequestTypeException { + GoPluginApiResponse response = createGoResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Should not reachable."); + LOG.info(String.format("Get request [%s] %n body[%s]", requestMessage.requestName(), requestMessage.requestBody())); + + try { + if (requestMessage.requestName().equals(REQUEST_CONFIGURATION)) { + response = PluginConfig.getConfiguration(); + } else if (requestMessage.requestName().equals(REQUEST_VALIDATION)) { + Map res = new HashMap<>(); + response = createGoResponse(HttpStatus.SC_OK, res); + } else if (requestMessage.requestName().equals(REQUEST_TASK_VIEW)) { + response = handleView(); + } else if (requestMessage.requestName().equals(REQUEST_EXECUTION)) { + response = handleExecute(requestMessage.requestBody()); + } else { + LOG.error(String.format("UnhandledRequestTypeException - requestName::%s", requestMessage.requestName())); + throw new UnhandledRequestTypeException(requestMessage.requestName()); + } + } catch (Exception e) { + LOG.error("Exception on handling", e); + } + + LOG.info("Response" + response.responseCode()); + return response; + } + + @Override + public GoPluginIdentifier pluginIdentifier() { + return PLUGIN_IDENTIFIER; + } + + + private GoPluginApiResponse handleExecute(String requestBody) { + Map response = new HashMap<>(); + response.put(SUCCESS, false); + if (isWindows()) { + String scriptFilePath = null; + + try { + + Map requestMap = new GsonBuilder().create().fromJson(requestBody, Map.class); + + scriptFilePath = creteTempScriptFile(requestMap); + int exitCode = executeScript(requestMap, scriptFilePath); + + if (exitCode == 0) { + response.put(SUCCESS, true); + addMessage(response, "Script completed successfully."); + } else { + addMessage(response, "Script completed with exit code: [" + exitCode + "]."); + } + + } catch (Exception e) { + addMessage(response, "Script execution interrupted. Reason::" + e); + } + Utils.deleteFile(scriptFilePath); + + } else { + addMessage(response, "Assigned agent is not Windows machine."); + } + + return createGoResponse(HttpStatus.SC_OK, response); + } + + private GoPluginApiResponse handleView() throws IOException { + Map response = new HashMap<>(); + response.put("displayValue", "PowerShell"); + response.put("template", Utils.readResource("/task.template.html")); + return createGoResponse(HttpStatus.SC_OK, response); + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTaskExtension.java b/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTaskExtension.java new file mode 100644 index 0000000..072c43e --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/PowerShellTaskExtension.java @@ -0,0 +1,114 @@ +package net.ws.go.plugin.task.powershell; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import net.ws.go.plugin.task.powershell.config.model.CpuArchitecture; +import net.ws.go.plugin.task.powershell.config.model.ErrorActions; + +import com.google.gson.GsonBuilder; +import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import com.thoughtworks.go.plugin.api.task.JobConsoleLogger; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; + +import static net.ws.go.plugin.task.powershell.Constants.*; + +final class PowerShellTaskExtension { + private PowerShellTaskExtension() { + + } + + static String creteTempScriptFile(Map requestMap) throws IOException { + String fileName = createTempFileName(getEnvironments(requestMap)); + String workingDirectory = getWorkingDir(requestMap); + String scriptValue = getConfig(requestMap, SCRIPT_KEY); + String errorAction = getConfig(requestMap, ERROR_ACTION_KEY); + if (StringUtils.isBlank(errorAction)) { + errorAction = ErrorActions.SILENTLY_CONTINUE.getActionString(); + } + + File file = new File(Utils.getScriptPath(workingDirectory, fileName)); + StringBuilder script = new StringBuilder("$script:ErrorActionPreference = [System.Management.Automation.ActionPreference]::"); + script.append(ErrorActions.fromString(errorAction).getActionString()).append(System.getProperty("line.separator")); + script.append(Utils.cleanupScript(scriptValue)); + FileUtils.writeStringToFile(file, script.toString()); + + return file.getAbsolutePath(); + } + + static int executeScript(Map requestMap, String scriptFileName) throws IOException, InterruptedException { + return executeCommand(getWorkingDir(requestMap), + getEnvironments(requestMap), + Utils.getPowerShellPath(CpuArchitecture.fromString(getConfig(requestMap, CPU_ARCHITECTURE_KEY))), + "-ExecutionPolicy", "RemoteSigned", "-NonInteractive", "-Command", + scriptFileName); + } + + static void addMessage(Map response, String message) { + JobConsoleLogger.getConsoleLogger().printLine(String.format("[PowerShell-executor] %s", message)); + response.put(MESSAGE, message); + } + + static GoPluginApiResponse createGoResponse(final int responseCode, Object response) { + final String json = response == null ? null : new GsonBuilder().create().toJson(response); + return new GoPluginApiResponse() { + @Override + public int responseCode() { + return responseCode; + } + + @Override + public Map responseHeaders() { + return null; + } + + @Override + public String responseBody() { + return json; + } + }; + } + + private static Map> getConfigMap(Map requestMap) { + return (Map>) requestMap.get("config"); + } + + private static Map getEnvironments(Map requestMap) { + return (Map) getContext(requestMap).get("environmentVariables"); + } + + private static String getWorkingDir(Map requestMap) { + return (String) getContext(requestMap).get("workingDirectory"); + } + + private static Map getContext(Map requestMap) { + return (Map) requestMap.get("context"); + } + + private static String getConfig(Map requestMap, String key) { + return getConfigMap(requestMap).get(key).get("value"); + } + + private static int executeCommand(String workingDirectory, Map environmentVariables, String... command) throws IOException, InterruptedException { + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.directory(new File(workingDirectory)); + if (environmentVariables != null && !environmentVariables.isEmpty()) { + processBuilder.environment().putAll(environmentVariables); + } + Process process = processBuilder.start(); + + JobConsoleLogger.getConsoleLogger().readOutputOf(process.getInputStream()); + JobConsoleLogger.getConsoleLogger().readErrorOf(process.getErrorStream()); + + return process.waitFor(); + } + + private static String createTempFileName(Map environmentVariables) { + return String.format("%s%s_%s%s_%s.ps1", + environmentVariables.get("GO_PIPELINE_NAME"), environmentVariables.get("GO_PIPELINE_LABEL"), + environmentVariables.get("GO_STAGE_NAME"), environmentVariables.get("GO_STAGE_COUNTER"), + environmentVariables.get("GO_JOB_NAME")); + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/Utils.java b/src/main/java/net/ws/go/plugin/task/powershell/Utils.java new file mode 100644 index 0000000..8502dbc --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/Utils.java @@ -0,0 +1,71 @@ +package net.ws.go.plugin.task.powershell; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; + +import net.ws.go.plugin.task.powershell.config.model.CpuArchitecture; + +import com.google.common.io.CharStreams; +import com.thoughtworks.go.plugin.api.task.JobConsoleLogger; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; + +final class Utils { + private static final HashMap PS_PATH = new HashMap<>(); + private static final String PS_PATH_TEMPLATE = "%s\\%s\\WindowsPowerShell\\v1.0\\powershell.exe"; + + static { + String arch = System.getProperty("os.arch"); + String sysRoot = System.getenv("SystemRoot"); + sysRoot = StringUtils.isEmpty(sysRoot) ? "C:\\WINDOWS" : sysRoot; + + if (arch.contains("64")) { + PS_PATH.put(CpuArchitecture.X86, String.format(PS_PATH_TEMPLATE, sysRoot, "SysWOW64")); + PS_PATH.put(CpuArchitecture.X64, String.format(PS_PATH_TEMPLATE, sysRoot, "system32")); + } else { + PS_PATH.put(CpuArchitecture.X86, String.format(PS_PATH_TEMPLATE, sysRoot, "system32")); + PS_PATH.put(CpuArchitecture.X64, String.format(PS_PATH_TEMPLATE, sysRoot, "sysnative")); + } + } + + private Utils() { + + } + + static String getPowerShellPath(CpuArchitecture cpuArchitecture) { + return PS_PATH.get(cpuArchitecture); + } + + static boolean isWindows() { + String osName = System.getProperty("os.name"); + boolean isWindows = StringUtils.containsIgnoreCase(osName, "windows"); + JobConsoleLogger.getConsoleLogger().printLine("[script-executor] OS detected: '" + osName + "'. Is Windows? " + isWindows); + return isWindows; + } + + static String readResource(String resourceFile) { + try (InputStreamReader reader = new InputStreamReader(Utils.class.getResourceAsStream(resourceFile), StandardCharsets.UTF_8)) { + return CharStreams.toString(reader); + } catch (IOException e) { + PowerShellTask.LOG.error("Could not find resource " + resourceFile, e); + throw new RuntimeException("Could not find resource " + resourceFile, e); + } + } + + static String getScriptPath(String workingDirectory, String scriptFileName) { + return workingDirectory + "/" + scriptFileName; + } + + static String cleanupScript(String scriptValue) { + return scriptValue.replaceAll("(\\r\\n|\\n|\\r)", System.getProperty("line.separator")); + } + + static void deleteFile(String scriptFile) { + if (!StringUtils.isBlank(scriptFile)) { + FileUtils.deleteQuietly(new File(scriptFile)); + } + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/config/PluginConfig.java b/src/main/java/net/ws/go/plugin/task/powershell/config/PluginConfig.java new file mode 100644 index 0000000..a859c66 --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/config/PluginConfig.java @@ -0,0 +1,45 @@ +package net.ws.go.plugin.task.powershell.config; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import net.ws.go.plugin.task.powershell.config.model.CpuArchitecture; +import net.ws.go.plugin.task.powershell.config.model.ErrorActions; +import net.ws.go.plugin.task.powershell.config.model.Field; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; +import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import net.ws.go.plugin.task.powershell.Constants; + +public class PluginConfig { + private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + + static final Map FIELDS = new LinkedHashMap<>(); + + private static final Field ERROR_ACTION = new Field(Constants.ERROR_ACTION_KEY, "Error Action Preference", + ErrorActions.STOP.getActionString(), true, false, "0"); + private static final Field SCRIPT = new Field(Constants.SCRIPT_KEY, "Script", + null, true, false, "1"); + private static final Field CPU_ARCHITECTURE = new Field(Constants.CPU_ARCHITECTURE_KEY, "CPU Architecture", + CpuArchitecture.X64.name().toLowerCase(), true, false, "2"); + + static { + FIELDS.put(ERROR_ACTION.getKey(), ERROR_ACTION); + FIELDS.put(SCRIPT.getKey(), SCRIPT); + FIELDS.put(CPU_ARCHITECTURE.getKey(), CPU_ARCHITECTURE); + } + + public static GoPluginApiResponse getConfiguration() throws Exception { + Map map = getKeyValueMap(); + return DefaultGoPluginApiResponse.success(map.size() > 0 ? GSON.toJson(map) : null); + } + + private static Map getKeyValueMap() { + Map keyValueMap = new HashMap<>(); + keyValueMap.putAll(FIELDS); + return keyValueMap; + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/config/model/CpuArchitecture.java b/src/main/java/net/ws/go/plugin/task/powershell/config/model/CpuArchitecture.java new file mode 100644 index 0000000..e9e8cf8 --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/config/model/CpuArchitecture.java @@ -0,0 +1,24 @@ +package net.ws.go.plugin.task.powershell.config.model; + +import org.apache.commons.lang3.StringUtils; + +public enum CpuArchitecture { + X86, + X64; + + CpuArchitecture() { + + } + + public static CpuArchitecture fromString(String architectureName) { + if (!StringUtils.isEmpty(architectureName)) { + for (CpuArchitecture value : CpuArchitecture.values()) { + if (StringUtils.equalsIgnoreCase(value.name(), architectureName)) { + return value; + } + } + } + + return null; + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/config/model/ErrorActions.java b/src/main/java/net/ws/go/plugin/task/powershell/config/model/ErrorActions.java new file mode 100644 index 0000000..90813db --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/config/model/ErrorActions.java @@ -0,0 +1,47 @@ +package net.ws.go.plugin.task.powershell.config.model; + +import org.apache.commons.lang3.StringUtils; + +public enum ErrorActions { + STOP(Constants.STOP), + CONTINUE(Constants.CONTINUE), + IGNORE(Constants.IGNORE), + SILENTLY_CONTINUE(Constants.SILENTLY_CONTINUE), + SUSPEND(Constants.SUSPEND), + INQUIRE(Constants.INQUIRE); + + private final String actionString; + + ErrorActions(String actionString) { + this.actionString = actionString; + } + + public static ErrorActions fromString(String actionType) { + if(!StringUtils.isEmpty(actionType)) { + for(ErrorActions action : ErrorActions.values()) { + if(StringUtils.equalsIgnoreCase(action.actionString, actionType)) { + return action; + } + } + } + + return null; + } + + public String getActionString() { + return actionString; + } + + private static class Constants { + static final String STOP = "Stop"; + static final String CONTINUE = "Continue"; + static final String IGNORE = "Ignore"; + static final String SILENTLY_CONTINUE = "SilentlyContinue"; + static final String SUSPEND = "Suspend"; + static final String INQUIRE = "Inquire"; + + private Constants() { + + } + } +} diff --git a/src/main/java/net/ws/go/plugin/task/powershell/config/model/Field.java b/src/main/java/net/ws/go/plugin/task/powershell/config/model/Field.java new file mode 100644 index 0000000..1408ab6 --- /dev/null +++ b/src/main/java/net/ws/go/plugin/task/powershell/config/model/Field.java @@ -0,0 +1,62 @@ +package net.ws.go.plugin.task.powershell.config.model; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import org.apache.commons.lang3.StringUtils; + +public class Field { + private final String key; + + @Expose + @SerializedName("display-name") + String displayName; + + @Expose + @SerializedName("default-value") + private String defaultValue; + + @Expose + @SerializedName("required") + protected Boolean required; + + @Expose + @SerializedName("secure") + private Boolean secure; + + @Expose + @SerializedName("display-order") + private String displayOrder; + + public Field(String key, String displayName, String defaultValue, Boolean required, Boolean secure, String displayOrder) { + this.key = key; + this.displayName = displayName; + this.defaultValue = defaultValue; + this.required = required; + this.secure = secure; + this.displayOrder = displayOrder; + } + + public Map validate(String input) { + HashMap result = new HashMap<>(); + String validationError = doValidate(input); + if (StringUtils.isNotBlank(validationError)) { + result.put("key", key); + result.put("message", validationError); + } + return result; + } + + protected String doValidate(String input) { + if (required && StringUtils.isBlank(input)) { + return displayName + " must not be blank."; + } + return null; + } + + public String getKey() { + return key; + } +} \ No newline at end of file diff --git a/src/main/resources/task.template.html b/src/main/resources/task.template.html new file mode 100644 index 0000000..f7cbd1c --- /dev/null +++ b/src/main/resources/task.template.html @@ -0,0 +1,20 @@ +
+ + + + + {{ GOINPUTNAME[script].$error.server }} + {{ GOINPUTNAME[cpuArch].$error.server }} + + +
\ No newline at end of file