Project creation

This commit is contained in:
Wooseung Sim 2017-05-23 09:26:02 -04:00
parent 70233c5be8
commit ffe716d19a
13 changed files with 603 additions and 0 deletions

55
build.gradle Normal file
View File

@ -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'
}

1
gradle.properties Normal file
View File

@ -0,0 +1 @@
version=1.0-SNAPSHOT

View File

@ -0,0 +1,31 @@
<!--
~ Copyright 2016 ThoughtWorks, Inc.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<go-plugin id="${id}" version="1">
<about>
<name>${name}</name>
<version>${version}</version>
<target-go-version>${goCdVersion}</target-go-version>
<target-os>
<value>Windows</value>
</target-os>
<description>${description}</description>
<vendor>
<name>${vendorName}</name>
<url>${vendorUrl}</url>
</vendor>
</about>
</go-plugin>

2
settings.gradle Normal file
View File

@ -0,0 +1,2 @@
rootProject.name = 'task.powershell'

View File

@ -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";
}

View File

@ -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<String, Object> 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<String, Object> response = new HashMap<>();
response.put(SUCCESS, false);
if (isWindows()) {
String scriptFilePath = null;
try {
Map<String, 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<String, Object> response = new HashMap<>();
response.put("displayValue", "PowerShell");
response.put("template", Utils.readResource("/task.template.html"));
return createGoResponse(HttpStatus.SC_OK, response);
}
}

View File

@ -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<String, 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<String, 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<String, Object> 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<String, String> responseHeaders() {
return null;
}
@Override
public String responseBody() {
return json;
}
};
}
private static Map<String, Map<String, String>> getConfigMap(Map<String, Map> requestMap) {
return (Map<String, Map<String, String>>) requestMap.get("config");
}
private static Map<String, String> getEnvironments(Map<String, Map> requestMap) {
return (Map<String, String>) getContext(requestMap).get("environmentVariables");
}
private static String getWorkingDir(Map<String, Map> requestMap) {
return (String) getContext(requestMap).get("workingDirectory");
}
private static Map<String, Object> getContext(Map<String, Map> requestMap) {
return (Map<String, Object>) requestMap.get("context");
}
private static String getConfig(Map<String, Map> requestMap, String key) {
return getConfigMap(requestMap).get(key).get("value");
}
private static int executeCommand(String workingDirectory, Map<String, String> 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<String, String> 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"));
}
}

View File

@ -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<CpuArchitecture, String> 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));
}
}
}

View File

@ -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<String, Field> 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<String, Object> map = getKeyValueMap();
return DefaultGoPluginApiResponse.success(map.size() > 0 ? GSON.toJson(map) : null);
}
private static Map<String, Object> getKeyValueMap() {
Map<String, Object> keyValueMap = new HashMap<>();
keyValueMap.putAll(FIELDS);
return keyValueMap;
}
}

View File

@ -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;
}
}

View File

@ -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() {
}
}
}

View File

@ -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<String, String> validate(String input) {
HashMap<String, String> 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;
}
}

View File

@ -0,0 +1,20 @@
<div class="form_item_block">
<label>Error Action Preference:<span class="asterisk">*</span></label>
<select ng-model="errorAction" ng-required="true" >
<option value="Stop">Stop</option>
<option value="SilentlyContinue">SilentlyContinue</option>
<option value="Continue">Continue</option>
<option value="Ignore">Ignore</option>
<option value="Suspend">Suspend</option>
<option value="Inquire">Inquire</option>
</select>
<label>Script:<span class="asterisk">*</span></label>
<textarea ng-model="script" ng-required="true" rows="14" cols="60"></textarea>
<span class="form_error" ng-show="GOINPUTNAME[script].$error.server">{{ GOINPUTNAME[script].$error.server }}</span>
<span class="form_error" ng-show="GOINPUTNAME[cpuArch].$error.server">{{ GOINPUTNAME[cpuArch].$error.server }}</span>
<label>CPU Architecture:<span class="asterisk">*</span></label>
<select ng-model="cpuArch" ng-required="true" >
<option value="x86">x86</option>
<option value="x64">x64</option>
</select>
</div>