From 162df553e76ad92b285331f594553a573885894d Mon Sep 17 00:00:00 2001 From: aberk Date: Tue, 22 Aug 2017 23:29:44 -0400 Subject: [PATCH 01/25] First implementation of connecting to the Connect API websocket service --- lib/cloud-connect-java-1.0.920563.jar.md5 | 1 + lib/cloud-connect-java-1.0.920563.jar.sha1 | 1 + lib/cloud-connect-java-1.0.920563.pom | 40 +++++++ lib/cloud-connect-java-1.0.920563.pom.md5 | 1 + lib/cloud-connect-java-1.0.920563.pom.sha1 | 1 + pom.xml | 16 +++ .../devops/connect/CloudSocketComponent.java | 101 ++++++++++++++++++ .../ibm/devops/connect/CloudWorkListener.java | 38 +++++++ .../connect/ConnectComputerListener.java | 25 +++++ .../com/ibm/devops/connect/IWorkListener.java | 15 +++ 10 files changed, 239 insertions(+) create mode 100644 lib/cloud-connect-java-1.0.920563.jar.md5 create mode 100644 lib/cloud-connect-java-1.0.920563.jar.sha1 create mode 100644 lib/cloud-connect-java-1.0.920563.pom create mode 100644 lib/cloud-connect-java-1.0.920563.pom.md5 create mode 100644 lib/cloud-connect-java-1.0.920563.pom.sha1 create mode 100644 src/main/java/com/ibm/devops/connect/CloudSocketComponent.java create mode 100644 src/main/java/com/ibm/devops/connect/CloudWorkListener.java create mode 100644 src/main/java/com/ibm/devops/connect/ConnectComputerListener.java create mode 100644 src/main/java/com/ibm/devops/connect/IWorkListener.java diff --git a/lib/cloud-connect-java-1.0.920563.jar.md5 b/lib/cloud-connect-java-1.0.920563.jar.md5 new file mode 100644 index 0000000..6d13c5e --- /dev/null +++ b/lib/cloud-connect-java-1.0.920563.jar.md5 @@ -0,0 +1 @@ +6c754e433587ea29f749e3d89e359d68 \ No newline at end of file diff --git a/lib/cloud-connect-java-1.0.920563.jar.sha1 b/lib/cloud-connect-java-1.0.920563.jar.sha1 new file mode 100644 index 0000000..d92f29c --- /dev/null +++ b/lib/cloud-connect-java-1.0.920563.jar.sha1 @@ -0,0 +1 @@ +c200e4c488c2c195d7e9419426963ee0d3f50e3a \ No newline at end of file diff --git a/lib/cloud-connect-java-1.0.920563.pom b/lib/cloud-connect-java-1.0.920563.pom new file mode 100644 index 0000000..706aa89 --- /dev/null +++ b/lib/cloud-connect-java-1.0.920563.pom @@ -0,0 +1,40 @@ + + + 4.0.0 + com.ibm.cloud.urbancode + cloud-connect-java + 1.0.920563 + + + org.apache.wink + wink-json4j + 1.4 + compile + + + io.socket + socket.io-client + 0.7.0 + compile + + + commons-codec + commons-codec + 1.10 + compile + + + log4j + log4j + 1.2.17 + compile + + + junit + junit + 4.11 + test + + + diff --git a/lib/cloud-connect-java-1.0.920563.pom.md5 b/lib/cloud-connect-java-1.0.920563.pom.md5 new file mode 100644 index 0000000..fce6216 --- /dev/null +++ b/lib/cloud-connect-java-1.0.920563.pom.md5 @@ -0,0 +1 @@ +02ccf932b56ed69bb01318c1d83520f9 \ No newline at end of file diff --git a/lib/cloud-connect-java-1.0.920563.pom.sha1 b/lib/cloud-connect-java-1.0.920563.pom.sha1 new file mode 100644 index 0000000..47424c1 --- /dev/null +++ b/lib/cloud-connect-java-1.0.920563.pom.sha1 @@ -0,0 +1 @@ +25c902c0b385cd671c01d25a197e36c4ab6111b4 \ No newline at end of file diff --git a/pom.xml b/pom.xml index ad1a899..a5c57f5 100644 --- a/pom.xml +++ b/pom.xml @@ -189,5 +189,21 @@ ${powermock.version} test + + uc-cloud-connect-java + uc-cloud-connect-java + 1.0-SNAPSHOT + + + org.apache.commons + commons-lang3 + 3.1 + + + io.socket + socket.io-client + 0.8.3 + compile + diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java new file mode 100644 index 0000000..07df462 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Licensed Materials - Property of IBM + * (c) Copyright IBM Corporation 2014. All Rights Reserved. + * + * Note to U.S. Government Users Restricted Rights: Use, + * duplication or disclosure restricted by GSA ADP Schedule + * Contract with IBM Corp. + *******************************************************************************/ +package com.urbancode.jenkins.plugins.ucrelease; + +import java.net.URI; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ibm.cloud.urbancode.connect.client.ConnectSocket; +import com.ibm.cloud.urbancode.connect.client.Listeners; + +import io.socket.client.Socket; + +public class CloudSocketComponent { + + public static final Logger log = LoggerFactory.getLogger(CloudSocketComponent.class); + + final private IWorkListener workListener; + final private String cloudUrl; + private ConnectSocket socket; + + public CloudSocketComponent(IWorkListener workListener, String cloudUrl) { + this.workListener = workListener; + this.cloudUrl = cloudUrl; + } + + public boolean isRegistered() { + return StringUtils.isNotBlank(getSyncToken()); + } + + public String getSyncId() { + return "ce64dea4-6728-46da-ad62-b371706755c9"; + // Properties props = SyncApplicationProperties.getProperties(); + // return props.getProperty(SyncApplicationProperties.SYNC_ID); + } + + public String getSyncToken() { + return "KPZLS8cRrR1Llz2aa7slwV7w9p6Kou8bhcZSvSQ0Xxb6WW1GmwbQF5oRuRBcyXUJsBF2b9SxQ5fA40avD7NqwA"; + // Properties props = SyncApplicationProperties.getProperties(); + // return props.getProperty(SyncApplicationProperties.SYNC_TOKEN); + } + + public void connectToCloudServices() throws Exception { + String syncId = getSyncId(); + String syncToken = getSyncToken(); + if (StringUtils.isBlank(syncId) || StringUtils.isBlank(syncToken)) { + log.info("Not connecting to the cloud. IBM Bluemix DevOps Connect not registered yet."); + return; + } + URI uri = new URI(cloudUrl); + log.info("Starting cloud endpoint " + syncId); + socket = ConnectSocket.builder() + .uri(uri) + .id(syncId) + .token(syncToken) + .onConnect(Listeners.chain(Listeners.INFO_LOGGING, Listeners.EMIT_GET_WORK)) + .onDisconnect(Listeners.INFO_LOGGING) + .onWorkAvailable(Listeners.chain(Listeners.DEBUG_LOGGING, Listeners.EMIT_GET_WORK)) + .onWork(workListener) +// .onWork(Listeners.chain(Listeners.INFO_LOGGING, workListener)) + .onError(Listeners.ERROR_LOGGING) + .build(); + socket.on(Socket.EVENT_CONNECT_ERROR, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_CONNECT_TIMEOUT, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_ERROR, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_FAILED, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_ATTEMPT, Listeners.INFO_LOGGING); + // do not listen for Socket.EVENT_RECONNECT, we will make 2 get work requests + log.info("Connecting to cloud service at {}", uri); + socket.connect(); + + System.out.println("---------------->>>>>>>>>>>>>>>>>>>"); + System.out.println("WE HAVE CONNECTED TO THE SERVER....."); + + } + + // this does get called, but you may not see logging in the console. it will appear in the file. + public void disconnect() { + if (socket != null) { + try { + socket.disconnect(); + log.info("Disconnected from the cloud service"); + } + catch (Exception e) { + log.error("Error disconnecting the cloud service gracefully", e); + } + finally { + socket = null; + } + } + } +} diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java new file mode 100644 index 0000000..ce1e8f5 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -0,0 +1,38 @@ +package com.urbancode.jenkins.plugins.ucrelease; + +import java.util.concurrent.TimeUnit; + +// import org.json.JSONArray; +// import org.json.JSONException; +// import org.json.JSONObject; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.ibm.cloud.urbancode.connect.client.ConnectSocket; +import jenkins.model.Jenkins; +/* + * When Spring is applying the @Transactional annotation, it creates a proxy class which wraps your class. + * So when your bean is created in your application context, you are getting an object that is not of type + * WorkListener but some proxy class that implements the IWorkListener interface. So anywhere you want WorkListener + * injected, you must use IWorkListener. + */ +public class CloudWorkListener implements IWorkListener { + + public CloudWorkListener() { + + } + + /* (non-Javadoc) + * @see com.ibm.cloud.urbancode.sync.IWorkListener#call(com.ibm.cloud.urbancode.connect.client.ConnectSocket, java.lang.String, java.lang.Object) + */ + @Override + public void call(ConnectSocket socket, String event, Object... args) { + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + System.out.println("THIS IS THE CALL FUNCTION...."); + System.out.println(event); + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + System.out.println(args); + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + } +} diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java new file mode 100644 index 0000000..6349eb9 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -0,0 +1,25 @@ +package com.urbancode.jenkins.plugins.ucrelease; + +import hudson.slaves.ComputerListener; +import hudson.model.Computer; +import hudson.Extension; + +@Extension +public class ConnectComputerListener extends ComputerListener { + + @Override + public void onOnline(Computer c) { + + String url = "https://uccloud-connect-stage1.stage1.mybluemix.net"; + + CloudWorkListener listener = new CloudWorkListener(); + CloudSocketComponent socket = new CloudSocketComponent(listener, url); + + try { + socket.connectToCloudServices(); + } catch (Exception e) { + System.out.println("WE CAUGHT AN EXCEPTION: " + e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/IWorkListener.java b/src/main/java/com/ibm/devops/connect/IWorkListener.java new file mode 100644 index 0000000..ac3ab59 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/IWorkListener.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Licensed Materials - Property of IBM + * (c) Copyright IBM Corporation 2014. All Rights Reserved. + * + * Note to U.S. Government Users Restricted Rights: Use, + * duplication or disclosure restricted by GSA ADP Schedule + * Contract with IBM Corp. + *******************************************************************************/ +package com.urbancode.jenkins.plugins.ucrelease; + +import com.ibm.cloud.urbancode.connect.client.Listener; + +public interface IWorkListener extends Listener { + +} \ No newline at end of file From 64f91f0a8a2f3cf81fb2219132201c4d6d2d0835 Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Wed, 23 Aug 2017 11:12:24 +0200 Subject: [PATCH 02/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/958 - add / modify copyrights - modify pox.xml - adapt package - externalize sync id and token - sync id and token on the server's configure page --- pom.xml | 2 +- .../devops/connect/CloudSocketComponent.java | 16 +++++++------- .../ibm/devops/connect/CloudWorkListener.java | 10 ++++++++- .../connect/ConnectComputerListener.java | 10 ++++++++- .../com/ibm/devops/connect/IWorkListener.java | 4 ++-- .../devops/dra/DevOpsGlobalConfiguration.java | 22 +++++++++++++++++++ .../DevOpsGlobalConfiguration/config.jelly | 8 +++++++ .../help-syncId.html | 20 +++++++++++++++++ .../help-syncToken.html | 20 +++++++++++++++++ 9 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html create mode 100644 src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html diff --git a/pom.xml b/pom.xml index a5c57f5..deb1846 100644 --- a/pom.xml +++ b/pom.xml @@ -192,7 +192,7 @@ uc-cloud-connect-java uc-cloud-connect-java - 1.0-SNAPSHOT + 1 org.apache.commons diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 07df462..64a69e2 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -1,20 +1,24 @@ /******************************************************************************* * Licensed Materials - Property of IBM - * (c) Copyright IBM Corporation 2014. All Rights Reserved. + * (c) Copyright IBM Corporation 2017. All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: Use, * duplication or disclosure restricted by GSA ADP Schedule * Contract with IBM Corp. *******************************************************************************/ -package com.urbancode.jenkins.plugins.ucrelease; +package com.ibm.devops.connect; import java.net.URI; import java.util.Properties; +import jenkins.model.Jenkins; + import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.ibm.devops.dra.DevOpsGlobalConfiguration; + import com.ibm.cloud.urbancode.connect.client.ConnectSocket; import com.ibm.cloud.urbancode.connect.client.Listeners; @@ -38,15 +42,11 @@ public boolean isRegistered() { } public String getSyncId() { - return "ce64dea4-6728-46da-ad62-b371706755c9"; - // Properties props = SyncApplicationProperties.getProperties(); - // return props.getProperty(SyncApplicationProperties.SYNC_ID); + return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId(); } public String getSyncToken() { - return "KPZLS8cRrR1Llz2aa7slwV7w9p6Kou8bhcZSvSQ0Xxb6WW1GmwbQF5oRuRBcyXUJsBF2b9SxQ5fA40avD7NqwA"; - // Properties props = SyncApplicationProperties.getProperties(); - // return props.getProperty(SyncApplicationProperties.SYNC_TOKEN); + return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId(); } public void connectToCloudServices() throws Exception { diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index ce1e8f5..29612dd 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -1,4 +1,12 @@ -package com.urbancode.jenkins.plugins.ucrelease; +/******************************************************************************* + * Licensed Materials - Property of IBM + * (c) Copyright IBM Corporation 2017. All Rights Reserved. + * + * Note to U.S. Government Users Restricted Rights: Use, + * duplication or disclosure restricted by GSA ADP Schedule + * Contract with IBM Corp. + *******************************************************************************/ +package com.ibm.devops.connect; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 6349eb9..322f548 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -1,4 +1,12 @@ -package com.urbancode.jenkins.plugins.ucrelease; +/******************************************************************************* + * Licensed Materials - Property of IBM + * (c) Copyright IBM Corporation 2017. All Rights Reserved. + * + * Note to U.S. Government Users Restricted Rights: Use, + * duplication or disclosure restricted by GSA ADP Schedule + * Contract with IBM Corp. + *******************************************************************************/ +package com.ibm.devops.connect; import hudson.slaves.ComputerListener; import hudson.model.Computer; diff --git a/src/main/java/com/ibm/devops/connect/IWorkListener.java b/src/main/java/com/ibm/devops/connect/IWorkListener.java index ac3ab59..705d4ca 100644 --- a/src/main/java/com/ibm/devops/connect/IWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/IWorkListener.java @@ -1,12 +1,12 @@ /******************************************************************************* * Licensed Materials - Property of IBM - * (c) Copyright IBM Corporation 2014. All Rights Reserved. + * (c) Copyright IBM Corporation 2017. All Rights Reserved. * * Note to U.S. Government Users Restricted Rights: Use, * duplication or disclosure restricted by GSA ADP Schedule * Contract with IBM Corp. *******************************************************************************/ -package com.urbancode.jenkins.plugins.ucrelease; +package com.ibm.devops.connect; import com.ibm.cloud.urbancode.connect.client.Listener; diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java index ce44b7c..f600806 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java @@ -30,6 +30,8 @@ public class DevOpsGlobalConfiguration extends GlobalConfiguration { @CopyOnWrite private volatile String consoleUrl; private volatile boolean debug_mode; + private volatile String syncId; + private volatile String syncToken; public DevOpsGlobalConfiguration() { load(); @@ -52,6 +54,24 @@ public void setConsoleUrl(String consoleUrl) { this.consoleUrl = consoleUrl; save(); } + + public String getSyncId() { + return syncId; + } + + public void setSyncId(String syncId) { + this.syncId = syncId; + save(); + } + + public String getSyncToken() { + return syncToken; + } + + public void setSyncToken(String syncToken) { + this.syncToken = syncToken; + save(); + } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { @@ -59,6 +79,8 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc // set that to properties and call save(). consoleUrl = formData.getString("consoleUrl"); debug_mode = Boolean.parseBoolean(formData.getString("debug_mode")); + syncId = formData.getString("syncId"); + syncToken = formData.getString("syncToken"); save(); return super.configure(req,formData); } diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly index 5cbfcdb..3c8a7f7 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly @@ -37,4 +37,12 @@ + + + + + + + + diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html new file mode 100644 index 0000000..2448609 --- /dev/null +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html @@ -0,0 +1,20 @@ + + +
+ Specify your DevOps Connect sync ID (requires a server restart). See Getting +started with Continuous Release documentation. + +
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html new file mode 100644 index 0000000..600a6eb --- /dev/null +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html @@ -0,0 +1,20 @@ + + +
+ Specify your DevOps Connect sync token (requires a server restart). See Getting +started with Continuous Release documentation. + +
\ No newline at end of file From 5690d94c0f192d74386b60cafe9b3d836ad2e618 Mon Sep 17 00:00:00 2001 From: aberk Date: Wed, 23 Aug 2017 10:54:12 -0400 Subject: [PATCH 03/25] Fix get sync token method --- src/main/java/com/ibm/devops/connect/CloudSocketComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 64a69e2..94bcca2 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -46,7 +46,7 @@ public String getSyncId() { } public String getSyncToken() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId(); + return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken(); } public void connectToCloudServices() throws Exception { From dd6200604bbcf5ad342dc1b7bddce3acc99fb93c Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Thu, 24 Aug 2017 12:37:13 +0200 Subject: [PATCH 04/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/961 use Logger - and not System.out.println .... --- .../devops/connect/CloudSocketComponent.java | 6 +----- .../ibm/devops/connect/CloudWorkListener.java | 19 ++++++++++++------- .../connect/ConnectComputerListener.java | 9 +++++++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 94bcca2..23ef44b 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -75,12 +75,8 @@ public void connectToCloudServices() throws Exception { socket.on(Socket.EVENT_RECONNECT_FAILED, Listeners.ERROR_LOGGING); socket.on(Socket.EVENT_RECONNECT_ATTEMPT, Listeners.INFO_LOGGING); // do not listen for Socket.EVENT_RECONNECT, we will make 2 get work requests - log.info("Connecting to cloud service at {}", uri); + socket.connect(); - - System.out.println("---------------->>>>>>>>>>>>>>>>>>>"); - System.out.println("WE HAVE CONNECTED TO THE SERVER....."); - } // this does get called, but you may not see logging in the console. it will appear in the file. diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 29612dd..b596620 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -14,6 +14,11 @@ // import org.json.JSONException; // import org.json.JSONObject; +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.ibm.cloud.urbancode.connect.client.ConnectSocket; @@ -25,7 +30,7 @@ * injected, you must use IWorkListener. */ public class CloudWorkListener implements IWorkListener { - + public static final Logger log = LoggerFactory.getLogger(CloudWorkListener.class); public CloudWorkListener() { } @@ -35,12 +40,12 @@ public CloudWorkListener() { */ @Override public void call(ConnectSocket socket, String event, Object... args) { - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - System.out.println("THIS IS THE CALL FUNCTION...."); - System.out.println(event); - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - System.out.println(args); - System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log.info("THIS IS THE CALL FUNCTION...."); + log.info("Event: " + event); + log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log.info("Args: " + ToStringBuilder.reflectionToString(args)); + log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); } } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 322f548..0f9325d 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -12,9 +12,12 @@ import hudson.model.Computer; import hudson.Extension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Extension public class ConnectComputerListener extends ComputerListener { - + public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); @Override public void onOnline(Computer c) { @@ -24,9 +27,11 @@ public void onOnline(Computer c) { CloudSocketComponent socket = new CloudSocketComponent(listener, url); try { + log.info("Connecting to Cloud Services..."); socket.connectToCloudServices(); + log.info("Connected to Cloud Services!"); } catch (Exception e) { - System.out.println("WE CAUGHT AN EXCEPTION: " + e); + log.error("Exception caught while connecting to Cloud Services: " + e); } } From c041e1b58128a911d6b33c69e18908778457a64e Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Fri, 25 Aug 2017 15:19:00 +0200 Subject: [PATCH 05/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/978 save work in progress --- .../ibm/devops/connect/CloudItemListener.java | 73 +++++++++++++++++++ .../ibm/devops/connect/CloudWorkListener.java | 9 +-- .../com/ibm/devops/connect/JenkinsJob.java | 59 +++++++++++++++ .../com/ibm/devops/connect/JenkinsServer.java | 56 ++++++++++++++ 4 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ibm/devops/connect/CloudItemListener.java create mode 100644 src/main/java/com/ibm/devops/connect/JenkinsJob.java create mode 100644 src/main/java/com/ibm/devops/connect/JenkinsServer.java diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java new file mode 100644 index 0000000..8d24640 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -0,0 +1,73 @@ +/* + + + Copyright 2016, 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; +import hudson.model.listeners.ItemListener; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.json.JSONObject; + +@Extension +public class CloudItemListener extends ItemListener { + public static final Logger log = LoggerFactory.getLogger(CloudItemListener.class); + + public CloudItemListener(){ + log.info("CloudItemListener started..."); + buildJobsList(); + } + + @Override + public void onCreated(Item item) { + handleEvent(item, "CREATED"); + } + + @Override + public void onDeleted(Item item) { + handleEvent(item, "DELETED"); + } + + @Override + public void onUpdated(Item item) { + handleEvent(item, "UPDATED"); + } + + private void handleEvent(Item item, String phase) { + JenkinsJob jenkinsJob= new JenkinsJob(item); + log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); + // we'll handle the updates to the sync app here + } + + private List buildJobsList() { + log.info("Building the list of Jenkins jobs..."); + List allProjects= JenkinsServer.getAllItems(); + List allJobs = new ArrayList(); + for (Item anItem : allProjects) { + JenkinsJob jenkinsJob= new JenkinsJob(anItem); + allJobs.add(jenkinsJob.toJson()); + } + return allJobs; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index b596620..fb25d35 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -22,7 +22,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.ibm.cloud.urbancode.connect.client.ConnectSocket; -import jenkins.model.Jenkins; + /* * When Spring is applying the @Transactional annotation, it creates a proxy class which wraps your class. * So when your bean is created in your application context, you are getting an object that is not of type @@ -43,9 +43,8 @@ public void call(ConnectSocket socket, String event, Object... args) { log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); log.info("THIS IS THE CALL FUNCTION...."); log.info("Event: " + event); - log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - log.info("Args: " + ToStringBuilder.reflectionToString(args)); - log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - +// log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); +// log.info("Args: " + ToStringBuilder.reflectionToString(args)); +// log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java new file mode 100644 index 0000000..a483922 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -0,0 +1,59 @@ +/* + + + Copyright 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import hudson.model.*; +import hudson.model.Item; + +import net.sf.json.JSONObject; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Jenkins server + */ + +public class JenkinsJob { + final private Item item; + public static final Logger log = LoggerFactory.getLogger(JenkinsJob.class); + + public JenkinsJob (Item item) { + this.item= item; + } + // TODO: see what this guy can do for us: + // - start: start a job + // - getStatus: get the status of a job + // - get build history + // - stop / cancel + // - other stuff? + public JSONObject toJson() { + String displayName= this.item.getDisplayName(); + String name= this.item.getName(); + String fullName= this.item.getFullName(); + String jobUrl= this.item.getUrl(); + + JSONObject jobToJson = new JSONObject(); + jobToJson.put("display_name", this.item.getDisplayName()); + jobToJson.put("name", this.item.getName()); + jobToJson.put("full_name", this.item.getFullName()); + jobToJson.put("job_url", this.item.getUrl()); + + // log.info("job: " + ToStringBuilder.reflectionToString(jobToJson)); + return jobToJson; + } +} diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java new file mode 100644 index 0000000..722f764 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -0,0 +1,56 @@ +/* + + + Copyright 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import hudson.model.*; +import hudson.model.Item; +import hudson.model.TopLevelItem; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import jenkins.model.Jenkins; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Jenkins server + */ + +public class JenkinsServer { + public static final Logger log = LoggerFactory.getLogger(JenkinsServer.class); + + // not used yet - but might be used later + public static Collection getJobNames() { + log.info("JenkinsServer: getJobNames()"); + Collection allJobNames= Jenkins.getInstance().getJobNames(); + log.info("retrieved " + allJobNames.size() + " JobNames"); + for (Iterator iterator = allJobNames.iterator(); iterator.hasNext();) { + String aJobName = (String) iterator.next(); + log.info("job: " + aJobName); + } + return Jenkins.getInstance().getJobNames(); + } + + public static List getAllItems() { + log.info("JenkinsServer: getAllItems()"); + List allProjects= Jenkins.getInstance().getAllItems(AbstractItem.class); + log.info("Retrieved " + allProjects.size() + " projects"); + return Jenkins.getInstance().getAllItems(); + } +} From f6441341b6ab68a15836259593ef4a95ddbcfc2b Mon Sep 17 00:00:00 2001 From: aberk Date: Thu, 7 Sep 2017 12:16:30 -0400 Subject: [PATCH 06/25] Now posts jobs through Sync API, Registers integration to Sync Store --- .../ibm/devops/connect/CloudItemListener.java | 4 +- .../ibm/devops/connect/CloudPublisher.java | 285 ++++++++++++++++++ .../devops/connect/CloudSocketComponent.java | 6 + .../ibm/devops/connect/CloudWorkListener.java | 5 +- .../com/ibm/devops/connect/JenkinsJob.java | 14 + 5 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ibm/devops/connect/CloudPublisher.java diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java index 8d24640..2626218 100644 --- a/src/main/java/com/ibm/devops/connect/CloudItemListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -56,7 +56,9 @@ public void onUpdated(Item item) { private void handleEvent(Item item, String phase) { JenkinsJob jenkinsJob= new JenkinsJob(item); - log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); + log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); // we'll handle the updates to the sync app here } diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java new file mode 100644 index 0000000..5ba5442 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -0,0 +1,285 @@ +/* + + + Copyright 2016, 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.ibm.devops.dra.AbstractDevOpsAction; +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.dra.PublishDeploy.PublishDeployImpl; + +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +import com.google.gson.*; +import jenkins.model.Jenkins; +import jenkins.tasks.SimpleBuildStep; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.kohsuke.stapler.*; +import javax.xml.bind.DatatypeConverter; + +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import java.io.*; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.TimeZone; +import java.net.URLEncoder; + +import org.apache.commons.codec.binary.Base64; + +import org.jenkinsci.plugins.uniqueid.IdStore; + +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.BuildStepMonitor; + +public class CloudPublisher { + public static final Logger log = LoggerFactory.getLogger(CloudPublisher.class); + + private final String JENKINS_JOB_ENDPOINT_URL = "api/v1/jenkins/jobs"; + private final String INTEGRATIONS_ENDPOINT_URL = "api/v1/integrations"; + private final String INTEGRATION_ENDPOINT_URL = "api/v1/integrations/{integration_id}"; + + private static String BUILD_API_URL = "/organizations/{org_name}/toolchainids/{toolchain_id}/buildartifacts/{build_artifact}/builds"; + private final static String CONTENT_TYPE_JSON = "application/json"; + private final static String CONTENT_TYPE_XML = "application/xml"; + + // form fields from UI + private String applicationName; + private String orgName; + private String credentialsId; + private String toolchainName; + + private String dlmsUrl; + private PrintStream printStream; + private File root; + private static String bluemixToken; + private static String preCredentials; + + // fields to support jenkins pipeline + private String result; + private String gitRepo; + private String gitBranch; + private String gitCommit; + private String username; + private String password; + // optional customized build number + private String buildNumber; + + public CloudPublisher() { + + } + + private String getSyncApiUrl() { + return "http://localhost:6002/"; + } + + private String getSyncStoreUrl() { + return "https://uccloud-sync-store-stage1.stage1.mybluemix.net/"; + } + + /** + * Upload the build information to DLMS - API V2. + */ + public boolean uploadJobInfo(JSONObject jobJson) { + String resStr = ""; + + try { + CloseableHttpClient httpClient = HttpClients.createDefault(); + String url = this.getSyncApiUrl() + JENKINS_JOB_ENDPOINT_URL; + + JSONArray payload = new JSONArray(); + payload.add(jobJson); + + String jenkinsId; + + if (IdStore.getId(Jenkins.getInstance()) != null) { + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } else { + IdStore.makeId(Jenkins.getInstance()); + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } + + HttpPost postMethod = new HttpPost(url); + // postMethod = addProxyInformation(postMethod); + postMethod.setHeader("sync_token", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()); + postMethod.setHeader("sync_id", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + postMethod.setHeader("instance_type", "JENKINS"); + postMethod.setHeader("instance_id", jenkinsId); + postMethod.setHeader("Content-Type", "application/json"); + + StringEntity data = new StringEntity(payload.toString()); + postMethod.setEntity(data); + + CloseableHttpResponse response = httpClient.execute(postMethod); + + resStr = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().toString().contains("200")) { + // get 200 response + log.info("[IBM Cloud DevOps] Upload Job Information successfully"); + return true; + + } else { + // if gets error status + log.info("[IBM Cloud DevOps] Error: Failed to upload Job, response status " + response.getStatusLine()); + } + } catch (JsonSyntaxException e) { + log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + } catch (IllegalStateException e) { + // will be triggered when 403 Forbidden + try { + log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + } catch (UnsupportedEncodingException e1) { + e1.printStackTrace(); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + public boolean createIntegrationIfNecessary() { + String resStr = ""; + try { + CloseableHttpClient httpClient = HttpClients.createDefault(); + + String jenkinsId; + + if (IdStore.getId(Jenkins.getInstance()) != null) { + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } else { + IdStore.makeId(Jenkins.getInstance()); + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } + + String url = this.getSyncStoreUrl() + INTEGRATION_ENDPOINT_URL.replace("{integration_id}", jenkinsId); + + HttpGet getMethod = new HttpGet(url); + // postMethod = addProxyInformation(postMethod); + getMethod.setHeader("Content-Type", "application/json"); + + String authEncoding = DatatypeConverter.printBase64Binary((Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId() + ":" + Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()).getBytes("UTF-8")); + getMethod.setHeader("Authorization", "Basic " + authEncoding); + + CloseableHttpResponse response = httpClient.execute(getMethod); + + resStr = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { + // get 200 response + log.info("[IBM Cloud DevOps] Integration was "); + return true; + + } else { + // if gets error status + log.info("--------------------------------------------"); + log.info("[IBM Cloud DevOps] No Integration Retrieved"); + + log.info("Attempting to create Integration"); + this.createIntegration(jenkinsId); + + } + } catch (JsonSyntaxException e) { + log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + } catch (IllegalStateException e) { + // will be triggered when 403 Forbidden + try { + log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + } catch (UnsupportedEncodingException e1) { + e1.printStackTrace(); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + private boolean createIntegration(String jenkinsId) { + String resStr = ""; + + try { + CloseableHttpClient httpClient = HttpClients.createDefault(); + + String url = this.getSyncStoreUrl() + INTEGRATIONS_ENDPOINT_URL; + + JSONObject newIntegration = new JSONObject(); + + newIntegration.put("name", "Jenkins Plugin Integration - " + jenkinsId); + newIntegration.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + newIntegration.put("id", jenkinsId); + newIntegration.put("dateCreated", System.currentTimeMillis()); + newIntegration.put("docType", "integration"); + + HttpPost postMethod = new HttpPost(url); + // postMethod = addProxyInformation(postMethod); + postMethod.setHeader("Content-Type", "application/json"); + String authEncoding = DatatypeConverter.printBase64Binary((Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId() + ":" + Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()).getBytes("UTF-8")); + postMethod.setHeader("Authorization", "Basic " + authEncoding); + + log.info("==================================================="); + log.info(authEncoding); + postMethod.setHeader("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + + StringEntity data = new StringEntity(newIntegration.toString()); + postMethod.setEntity(data); + + CloseableHttpResponse response = httpClient.execute(postMethod); + + resStr = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { + // get 200 response + log.info("==================================================="); + log.info("[IBM Cloud DevOps] Created integration successfully"); + return true; + + } else { + // if gets error status + log.info("[IBM Cloud DevOps] Error: Failed to create integration, response status " + response.getStatusLine()); + } + } catch (JsonSyntaxException e) { + log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + } catch (IllegalStateException e) { + // will be triggered when 403 Forbidden + try { + log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + } catch (UnsupportedEncodingException e1) { + e1.printStackTrace(); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + +} diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 23ef44b..8d5281d 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -22,6 +22,8 @@ import com.ibm.cloud.urbancode.connect.client.ConnectSocket; import com.ibm.cloud.urbancode.connect.client.Listeners; +import com.ibm.devops.connect.CloudPublisher; + import io.socket.client.Socket; public class CloudSocketComponent { @@ -56,6 +58,10 @@ public void connectToCloudServices() throws Exception { log.info("Not connecting to the cloud. IBM Bluemix DevOps Connect not registered yet."); return; } + + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.createIntegrationIfNecessary(); + URI uri = new URI(cloudUrl); log.info("Starting cloud endpoint " + syncId); socket = ConnectSocket.builder() diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index fb25d35..04310b9 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -43,8 +43,7 @@ public void call(ConnectSocket socket, String event, Object... args) { log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); log.info("THIS IS THE CALL FUNCTION...."); log.info("Event: " + event); -// log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); -// log.info("Args: " + ToStringBuilder.reflectionToString(args)); -// log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log.info("Args: " + args.toString()); + log.info("Args: " + args[0].toString()); } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index a483922..e091a44 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -24,6 +24,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.jenkinsci.plugins.uniqueid.IdStore; + /** * Jenkins server */ @@ -53,6 +55,18 @@ public JSONObject toJson() { jobToJson.put("full_name", this.item.getFullName()); jobToJson.put("job_url", this.item.getUrl()); + String jobId; + + if(IdStore.getId(this.item) != null) { + jobId = IdStore.getId(this.item); + } else { + IdStore.makeId(this.item); + jobId = IdStore.getId(this.item); + } + + jobToJson.put("id", jobId); + jobToJson.put("instance_type", "JENKINS"); + // log.info("job: " + ToStringBuilder.reflectionToString(jobToJson)); return jobToJson; } From 059738e97181ccde404156cbc34998a2290cc399 Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Tue, 12 Sep 2017 06:19:49 +0200 Subject: [PATCH 07/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/1013 - use logger prefixed with method name - pom.xml - include unique id --- pom.xml | 5 +++ .../ibm/devops/connect/CloudItemListener.java | 6 ++-- .../ibm/devops/connect/CloudPublisher.java | 32 +++++++++++-------- .../devops/connect/CloudSocketComponent.java | 10 +++--- .../ibm/devops/connect/CloudWorkListener.java | 13 +++++--- .../connect/ConnectComputerListener.java | 11 ++++--- .../com/ibm/devops/connect/JenkinsServer.java | 15 +++++---- 7 files changed, 57 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index deb1846..15f6a2c 100644 --- a/pom.xml +++ b/pom.xml @@ -144,6 +144,11 @@ httpcore 4.4.5
+ + org.jenkins-ci.plugins + unique-id + 2.1.3 + org.apache.httpcomponents httpmime diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java index 2626218..efb07d5 100644 --- a/src/main/java/com/ibm/devops/connect/CloudItemListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -33,9 +33,11 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of @Extension public class CloudItemListener extends ItemListener { public static final Logger log = LoggerFactory.getLogger(CloudItemListener.class); + private String logPrefix= "[IBM Cloud DevOps] CloudItemListener#"; public CloudItemListener(){ - log.info("CloudItemListener started..."); + logPrefix= logPrefix + "CloudItemListener "; + log.info(logPrefix + "CloudItemListener started..."); buildJobsList(); } @@ -63,7 +65,7 @@ private void handleEvent(Item item, String phase) { } private List buildJobsList() { - log.info("Building the list of Jenkins jobs..."); + log.info(logPrefix + "Building the list of Jenkins jobs..."); List allProjects= JenkinsServer.getAllItems(); List allJobs = new ArrayList(); for (Item anItem : allProjects) { diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index 5ba5442..0f58934 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -55,6 +55,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of public class CloudPublisher { public static final Logger log = LoggerFactory.getLogger(CloudPublisher.class); + private String logPrefix= "[IBM Cloud DevOps] CloudPublisher#"; private final String JENKINS_JOB_ENDPOINT_URL = "api/v1/jenkins/jobs"; private final String INTEGRATIONS_ENDPOINT_URL = "api/v1/integrations"; @@ -102,6 +103,7 @@ private String getSyncStoreUrl() { * Upload the build information to DLMS - API V2. */ public boolean uploadJobInfo(JSONObject jobJson) { + logPrefix= logPrefix + "uploadJobInfo "; String resStr = ""; try { @@ -136,19 +138,19 @@ public boolean uploadJobInfo(JSONObject jobJson) { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200")) { // get 200 response - log.info("[IBM Cloud DevOps] Upload Job Information successfully"); + log.info(logPrefix + "Upload Job Information successfully"); return true; } else { // if gets error status - log.info("[IBM Cloud DevOps] Error: Failed to upload Job, response status " + response.getStatusLine()); + log.error(logPrefix + "Error: Failed to upload Job, response status " + response.getStatusLine()); } } catch (JsonSyntaxException e) { - log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + log.error(logPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.error(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } @@ -163,6 +165,7 @@ public boolean uploadJobInfo(JSONObject jobJson) { } public boolean createIntegrationIfNecessary() { + logPrefix= logPrefix + "createIntegrationIfNecessary "; String resStr = ""; try { CloseableHttpClient httpClient = HttpClients.createDefault(); @@ -190,24 +193,24 @@ public boolean createIntegrationIfNecessary() { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response - log.info("[IBM Cloud DevOps] Integration was "); + log.info(logPrefix + "Integration was retrieved"); return true; } else { // if gets error status log.info("--------------------------------------------"); - log.info("[IBM Cloud DevOps] No Integration Retrieved"); + log.info(logPrefix + "No Integration Retrieved"); - log.info("Attempting to create Integration"); - this.createIntegration(jenkinsId); + log.info(logPrefix + "Attempting to create a new integration"); + return this.createIntegration(jenkinsId); } } catch (JsonSyntaxException e) { - log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + log.error(logPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.info(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } @@ -222,6 +225,7 @@ public boolean createIntegrationIfNecessary() { } private boolean createIntegration(String jenkinsId) { + logPrefix= logPrefix + "createIntegration "; String resStr = ""; try { @@ -256,19 +260,19 @@ private boolean createIntegration(String jenkinsId) { if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response log.info("==================================================="); - log.info("[IBM Cloud DevOps] Created integration successfully"); + log.info(logPrefix + "Created integration successfully"); return true; } else { // if gets error status - log.info("[IBM Cloud DevOps] Error: Failed to create integration, response status " + response.getStatusLine()); + log.error(logPrefix + "Error: Failed to create integration, response status " + response.getStatusLine()); } } catch (JsonSyntaxException e) { - log.info("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); + log.error(logPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.info("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.error(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 8d5281d..5c32d12 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -29,6 +29,7 @@ public class CloudSocketComponent { public static final Logger log = LoggerFactory.getLogger(CloudSocketComponent.class); + private String logPrefix= "[IBM Cloud DevOps] CloudSocketComponent#"; final private IWorkListener workListener; final private String cloudUrl; @@ -52,10 +53,11 @@ public String getSyncToken() { } public void connectToCloudServices() throws Exception { + logPrefix= logPrefix + "connectToCloudServices "; String syncId = getSyncId(); String syncToken = getSyncToken(); if (StringUtils.isBlank(syncId) || StringUtils.isBlank(syncToken)) { - log.info("Not connecting to the cloud. IBM Bluemix DevOps Connect not registered yet."); + log.info(logPrefix + "Not connecting to the cloud. IBM Bluemix DevOps Connect not registered yet."); return; } @@ -63,7 +65,7 @@ public void connectToCloudServices() throws Exception { cloudPublisher.createIntegrationIfNecessary(); URI uri = new URI(cloudUrl); - log.info("Starting cloud endpoint " + syncId); + log.info(logPrefix + "Starting cloud endpoint " + syncId); socket = ConnectSocket.builder() .uri(uri) .id(syncId) @@ -90,10 +92,10 @@ public void disconnect() { if (socket != null) { try { socket.disconnect(); - log.info("Disconnected from the cloud service"); + log.info(logPrefix + "Disconnected from the cloud service"); } catch (Exception e) { - log.error("Error disconnecting the cloud service gracefully", e); + log.error(logPrefix + "Error disconnecting the cloud service gracefully", e); } finally { socket = null; diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 04310b9..5d82c63 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -31,6 +31,8 @@ */ public class CloudWorkListener implements IWorkListener { public static final Logger log = LoggerFactory.getLogger(CloudWorkListener.class); + private String logPrefix= "[IBM Cloud DevOps] CloudWorkListener#"; + public CloudWorkListener() { } @@ -40,10 +42,11 @@ public CloudWorkListener() { */ @Override public void call(ConnectSocket socket, String event, Object... args) { - log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - log.info("THIS IS THE CALL FUNCTION...."); - log.info("Event: " + event); - log.info("Args: " + args.toString()); - log.info("Args: " + args[0].toString()); + logPrefix= logPrefix + "call "; + log.info(logPrefix + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + log.info(logPrefix + "THIS IS THE CALL FUNCTION...."); + log.info(logPrefix + "Event: " + event); + log.info(logPrefix + "Args: " + args.toString()); + log.info(logPrefix + "Args: " + args[0].toString()); } } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 0f9325d..ccc6bc6 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -18,20 +18,23 @@ @Extension public class ConnectComputerListener extends ComputerListener { public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); + private String logPrefix= "[IBM Cloud DevOps] ConnectComputerListener#"; + @Override public void onOnline(Computer c) { - + logPrefix= logPrefix + "onOnline "; + String url = "https://uccloud-connect-stage1.stage1.mybluemix.net"; CloudWorkListener listener = new CloudWorkListener(); CloudSocketComponent socket = new CloudSocketComponent(listener, url); try { - log.info("Connecting to Cloud Services..."); + log.info(logPrefix + "Connecting to Cloud Services..."); socket.connectToCloudServices(); - log.info("Connected to Cloud Services!"); + log.info(logPrefix + "Connected to Cloud Services!"); } catch (Exception e) { - log.error("Exception caught while connecting to Cloud Services: " + e); + log.error(logPrefix + "Exception caught while connecting to Cloud Services: " + e); } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java index 722f764..2a42f78 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsServer.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -34,23 +34,26 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of public class JenkinsServer { public static final Logger log = LoggerFactory.getLogger(JenkinsServer.class); - + private static String logPrefix= "[IBM Cloud DevOps] JenkinsServer#"; + // not used yet - but might be used later public static Collection getJobNames() { - log.info("JenkinsServer: getJobNames()"); + logPrefix= logPrefix + "getJobNames "; + log.info(logPrefix + "get the list of job names"); Collection allJobNames= Jenkins.getInstance().getJobNames(); - log.info("retrieved " + allJobNames.size() + " JobNames"); + log.info(logPrefix + "retrieved " + allJobNames.size() + " JobNames"); for (Iterator iterator = allJobNames.iterator(); iterator.hasNext();) { String aJobName = (String) iterator.next(); - log.info("job: " + aJobName); + log.info(logPrefix + "job: " + aJobName); } return Jenkins.getInstance().getJobNames(); } public static List getAllItems() { - log.info("JenkinsServer: getAllItems()"); + logPrefix= logPrefix + "getAllItems "; + log.info(logPrefix + "get the list of all items"); List allProjects= Jenkins.getInstance().getAllItems(AbstractItem.class); - log.info("Retrieved " + allProjects.size() + " projects"); + log.info(logPrefix + "Retrieved " + allProjects.size() + " projects"); return Jenkins.getInstance().getAllItems(); } } From 9f5d2e1309d259545687b2f204cc06b56ee5bc5d Mon Sep 17 00:00:00 2001 From: aberk Date: Wed, 4 Oct 2017 14:11:25 -0400 Subject: [PATCH 08/25] Continuous Release Phase 1 --- pom.xml | 17 +- .../connect/CloudBuildStepListener.java | 93 +++++++++++ .../com/ibm/devops/connect/CloudCause.java | 87 ++++++++++ .../ibm/devops/connect/CloudPublisher.java | 39 +++-- .../ibm/devops/connect/CloudWorkListener.java | 80 ++++++++- .../connect/ConnectComputerListener.java | 5 +- .../com/ibm/devops/connect/JenkinsJob.java | 56 ++++++- .../ibm/devops/connect/JenkinsJobStatus.java | 152 ++++++++++++++++++ .../com/ibm/devops/connect/SourceData.java | 51 ++++++ .../devops/dra/DevOpsGlobalConfiguration.java | 11 ++ .../DevOpsGlobalConfiguration/config.jelly | 3 + 11 files changed, 566 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java create mode 100644 src/main/java/com/ibm/devops/connect/CloudCause.java create mode 100644 src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java create mode 100644 src/main/java/com/ibm/devops/connect/SourceData.java diff --git a/pom.xml b/pom.xml index deb1846..5bcade3 100644 --- a/pom.xml +++ b/pom.xml @@ -192,7 +192,7 @@ uc-cloud-connect-java uc-cloud-connect-java - 1 + 1.0-SNAPSHOT org.apache.commons @@ -205,5 +205,20 @@ 0.8.3 compile + + org.jenkins-ci.plugins + unique-id + 2.1.3 + + + org.jenkins-ci.plugins.workflow + workflow-job + 2.1 + + + org.eclipse.hudson.plugins + git + 3.0.1 + diff --git a/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java new file mode 100644 index 0000000..6126567 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java @@ -0,0 +1,93 @@ +/* + + + Copyright 2016, 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import java.util.Map; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; +import hudson.model.BuildStepListener; +import hudson.tasks.BuildStep; +import hudson.model.AbstractBuild; +import hudson.tasks.Builder; +import hudson.model.Result; + +import jenkins.model.Jenkins; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +import com.ibm.devops.connect.CloudCause.JobStatus; + +import com.ibm.devops.dra.DevOpsGlobalConfiguration; + +import org.jenkinsci.plugins.uniqueid.IdStore; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; + +@Extension +public class CloudBuildStepListener extends BuildStepListener { + public static final Logger log = LoggerFactory.getLogger(CloudBuildStepListener.class); + + public void finished(AbstractBuild build, BuildStep bs, BuildListener listener, boolean canContinue) { + // We listen to jobs that are started by IBM Cloud only + if(this.shouldListen(build)) { + JenkinsJobStatus status = new JenkinsJobStatus(build, getCloudCause(build), bs, false, !canContinue); + JSONObject statusUpdate = status.generate(); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); + } + } + + public void started(AbstractBuild build, BuildStep bs, BuildListener listener) { + // We listen to jobs that are started by IBM Cloud only + if(this.shouldListen(build)) { + JenkinsJobStatus status = new JenkinsJobStatus(build, getCloudCause(build), bs, true, false); + JSONObject statusUpdate = status.generate(); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); + } + } + + private boolean shouldListen(AbstractBuild build) { + if(getCloudCause(build) == null) { + return false; + } else { + return true; + } + } + + private CloudCause getCloudCause(AbstractBuild build) { + List causes = build.getCauses(); + + for(Cause cause : causes) { + if (cause instanceof CloudCause ) { + return (CloudCause)cause; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudCause.java b/src/main/java/com/ibm/devops/connect/CloudCause.java new file mode 100644 index 0000000..fa5f938 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudCause.java @@ -0,0 +1,87 @@ +package com.ibm.devops.connect; + +import hudson.model.Cause; +import hudson.model.Node; +import com.ibm.cloud.urbancode.connect.client.ConnectSocket; + +import java.util.List; +import java.util.Set; +import java.util.ArrayList; +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +public class CloudCause extends Cause { + + public enum JobStatus { + unstarted, started, success, failure + } + + private String workId; + private JSONObject returnProps; + private List steps = new ArrayList(); + + private SourceData sourceData; + + // private ConnectSocket socket; + + public CloudCause(ConnectSocket socket, String workId, JSONObject returnProps) { + this.workId = workId; + this.returnProps = returnProps; + // this.socket = socket; + } + + @Override + public String getShortDescription() { + return "Started due to a request from IBM Continuous Release. Work Id: " + this.workId; + } + + public void addStep(String name, String status, String message, boolean isFatal) { + JSONObject obj = new JSONObject(); + obj.put("name", name); + obj.put("status", status); + obj.put("message", message); + obj.put("isFatal", isFatal); + steps.add(obj); + } + + public void setSourceData(SourceData sourceData) { + this.sourceData = sourceData; + } + + public SourceData getSourceData() { + return this.sourceData; + } + + public JSONObject getSourceDataJson() { + if(this.sourceData == null) { + return new JSONObject(); + } else { + return sourceData.toJson(); + } + } + + public void updateLastStep(String name, String status, String message, boolean isFatal) { + JSONObject obj = steps.get(steps.size() - 1); + obj.put("name", name); + obj.put("status", status); + obj.put("message", message); + obj.put("isFatal", isFatal); + } + + public void addSourceData(String branch, String revision, String scmName, Set remoteUrls) { + + } + + public JSONObject getReturnProps() { + return returnProps; + } + + public JSONArray getStepsArray() { + JSONArray result = new JSONArray(); + for(JSONObject obj : steps) { + result.add(obj); + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index 5ba5442..52fbfba 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -57,6 +57,7 @@ public class CloudPublisher { public static final Logger log = LoggerFactory.getLogger(CloudPublisher.class); private final String JENKINS_JOB_ENDPOINT_URL = "api/v1/jenkins/jobs"; + private final String JENKINS_JOB_STATUS_ENDPOINT_URL = "api/v1/jenkins/jobStatus"; private final String INTEGRATIONS_ENDPOINT_URL = "api/v1/integrations"; private final String INTEGRATION_ENDPOINT_URL = "api/v1/integrations/{integration_id}"; @@ -91,7 +92,9 @@ public CloudPublisher() { } private String getSyncApiUrl() { - return "http://localhost:6002/"; + // return "http://localhost:6002/"; + + return "https://ucreporting-sync-api-stage1.stage1.mybluemix.net/"; } private String getSyncStoreUrl() { @@ -99,18 +102,30 @@ private String getSyncStoreUrl() { } /** - * Upload the build information to DLMS - API V2. + * Upload the build information to Sync API - API V1. */ public boolean uploadJobInfo(JSONObject jobJson) { + String url = this.getSyncApiUrl() + JENKINS_JOB_ENDPOINT_URL; + + JSONArray payload = new JSONArray(); + payload.add(jobJson); + + return postToSyncAPI(url, payload.toString()); + } + + public boolean uploadJobStatus(JSONObject jobStatus) { + + String url = this.getSyncApiUrl() + JENKINS_JOB_STATUS_ENDPOINT_URL; + + return postToSyncAPI(url, jobStatus.toString()); + } + + private boolean postToSyncAPI(String url, String payload) { String resStr = ""; try { CloseableHttpClient httpClient = HttpClients.createDefault(); - String url = this.getSyncApiUrl() + JENKINS_JOB_ENDPOINT_URL; - - JSONArray payload = new JSONArray(); - payload.add(jobJson); - + String jenkinsId; if (IdStore.getId(Jenkins.getInstance()) != null) { @@ -128,7 +143,7 @@ public boolean uploadJobInfo(JSONObject jobJson) { postMethod.setHeader("instance_id", jenkinsId); postMethod.setHeader("Content-Type", "application/json"); - StringEntity data = new StringEntity(payload.toString()); + StringEntity data = new StringEntity(payload); postMethod.setEntity(data); CloseableHttpResponse response = httpClient.execute(postMethod); @@ -136,7 +151,7 @@ public boolean uploadJobInfo(JSONObject jobJson) { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200")) { // get 200 response - log.info("[IBM Cloud DevOps] Upload Job Information successfully"); + // log.info("[IBM Cloud DevOps] Upload Job Information successfully"); return true; } else { @@ -190,14 +205,11 @@ public boolean createIntegrationIfNecessary() { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response - log.info("[IBM Cloud DevOps] Integration was "); return true; } else { // if gets error status - log.info("--------------------------------------------"); log.info("[IBM Cloud DevOps] No Integration Retrieved"); - log.info("Attempting to create Integration"); this.createIntegration(jenkinsId); @@ -243,8 +255,6 @@ private boolean createIntegration(String jenkinsId) { String authEncoding = DatatypeConverter.printBase64Binary((Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId() + ":" + Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()).getBytes("UTF-8")); postMethod.setHeader("Authorization", "Basic " + authEncoding); - log.info("==================================================="); - log.info(authEncoding); postMethod.setHeader("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); StringEntity data = new StringEntity(newIntegration.toString()); @@ -255,7 +265,6 @@ private boolean createIntegration(String jenkinsId) { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response - log.info("==================================================="); log.info("[IBM Cloud DevOps] Created integration successfully"); return true; diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 04310b9..0ca1463 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -23,6 +23,25 @@ import com.google.common.cache.CacheBuilder; import com.ibm.cloud.urbancode.connect.client.ConnectSocket; +import net.sf.json.*; +import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; +import hudson.model.AbstractProject; +import hudson.model.Action; +import hudson.model.ParametersAction; +import hudson.model.CauseAction; +import hudson.model.ParameterValue; +import hudson.model.StringParameterValue; +import hudson.model.queue.QueueTaskFuture; +import hudson.model.Queue; + +import java.util.ArrayList; +import java.util.Iterator; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.lang.InterruptedException; + /* * When Spring is applying the @Transactional annotation, it creates a proxy class which wraps your class. * So when your bean is created in your application context, you are getting an object that is not of type @@ -34,16 +53,65 @@ public class CloudWorkListener implements IWorkListener { public CloudWorkListener() { } - + + public enum WorkStatus { + success, failed, started + } + /* (non-Javadoc) * @see com.ibm.cloud.urbancode.sync.IWorkListener#call(com.ibm.cloud.urbancode.connect.client.ConnectSocket, java.lang.String, java.lang.Object) */ @Override public void call(ConnectSocket socket, String event, Object... args) { - log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - log.info("THIS IS THE CALL FUNCTION...."); - log.info("Event: " + event); - log.info("Args: " + args.toString()); - log.info("Args: " + args[0].toString()); + log.info("[IBM Connect Socket] Received event from Connect Socket"); + + JSONArray incomingJobs = JSONArray.fromObject(args[0].toString()); + + for(int i=0; i < incomingJobs.size(); i++) { + JSONObject incomingJob = incomingJobs.getJSONObject(i); + + if (incomingJob.has("fullName")) { + String fullName = incomingJob.get("fullName").toString(); + Jenkins myJenkins = Jenkins.getInstance(); + AbstractProject abstractProject = (AbstractProject)myJenkins.getItem(fullName); + ArrayList parametersList = new ArrayList(); + + if(incomingJob.has("props")) { + JSONObject props = incomingJob.getJSONObject("props"); + Iterator keys = props.keys(); + + while( keys.hasNext() ) { + String key = (String)keys.next(); + + parametersList.add(new StringParameterValue(key, props.get(key).toString())); + } + } + + JSONObject returnProps = new JSONObject(); + if(incomingJob.has("returnProps")) { + returnProps = incomingJob.getJSONObject("returnProps"); + } + + Queue.Item queueItem = ParameterizedJobMixIn.scheduleBuild2(abstractProject, 0, new ParametersAction(parametersList), new CauseAction( + new CloudCause(socket, incomingJob.get("id").toString(), returnProps) )); + + } + + sendResult(socket, incomingJobs.getJSONObject(i).get("id").toString(), WorkStatus.started, "This work has been started"); + } + + } + + private void sendResult(ConnectSocket socket, String id, WorkStatus status, String comment) { + JSONObject json = new JSONObject(); + try { + json.put("id", id); + json.put("status", status.name()); + json.put("description", comment); + } + catch (JSONException e) { + throw new RuntimeException("Error constructing work result JSON", e); + } + socket.emit("set_work_status", json.toString()); } } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 0f9325d..b404314 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -21,7 +21,7 @@ public class ConnectComputerListener extends ComputerListener { @Override public void onOnline(Computer c) { - String url = "https://uccloud-connect-stage1.stage1.mybluemix.net"; + String url = getConnectUrl(); CloudWorkListener listener = new CloudWorkListener(); CloudSocketComponent socket = new CloudSocketComponent(listener, url); @@ -35,4 +35,7 @@ public void onOnline(Computer c) { } } + private String getConnectUrl() { + return "https://uccloud-connect-stage1.stage1.mybluemix.net"; + } } \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index e091a44..720d7a4 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -16,8 +16,13 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import hudson.model.*; import hudson.model.Item; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.queue.SubTask; + +import java.util.Collection; import net.sf.json.JSONObject; +import net.sf.json.JSONArray; import org.apache.commons.lang.builder.ToStringBuilder; @@ -26,6 +31,11 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.jenkinsci.plugins.uniqueid.IdStore; +import java.util.List; +import jenkins.model.Jenkins; +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; + /** * Jenkins server */ @@ -44,16 +54,17 @@ public JenkinsJob (Item item) { // - stop / cancel // - other stuff? public JSONObject toJson() { + String displayName= this.item.getDisplayName(); String name= this.item.getName(); String fullName= this.item.getFullName(); String jobUrl= this.item.getUrl(); JSONObject jobToJson = new JSONObject(); - jobToJson.put("display_name", this.item.getDisplayName()); + jobToJson.put("displayName", this.item.getDisplayName()); jobToJson.put("name", this.item.getName()); - jobToJson.put("full_name", this.item.getFullName()); - jobToJson.put("job_url", this.item.getUrl()); + jobToJson.put("fullName", this.item.getFullName()); + jobToJson.put("jobUrl", this.item.getUrl()); String jobId; @@ -65,9 +76,44 @@ public JSONObject toJson() { } jobToJson.put("id", jobId); - jobToJson.put("instance_type", "JENKINS"); + jobToJson.put("instanceType", "JENKINS"); + jobToJson.put("instanceName", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getInstanceName()); + + if(this.item instanceof WorkflowJob) { + jobToJson.put("isPipeline", true); + // TODO: Find a way to get Stage definitions + } else { + jobToJson.put("isPipeline", false); + } + + jobToJson.put("params", getJobParams()); - // log.info("job: " + ToStringBuilder.reflectionToString(jobToJson)); return jobToJson; } + + private JSONArray getJobParams() { + JSONArray result = new JSONArray(); + + if(this.item instanceof AbstractProject) { + List actions = ((AbstractProject)this.item).getActions(); + + for(Action action : actions) { + if (action instanceof ParametersDefinitionProperty) { + List paraDefs = ((ParametersDefinitionProperty)action).getParameterDefinitions(); + for (ParameterDefinition paramDef : paraDefs) { + + JSONObject paramDefObj = new JSONObject(); + paramDefObj.put("name", paramDef.getName()); + paramDefObj.put("type", paramDef.getType()); + paramDefObj.put("defaultValue", paramDef.getDefaultParameterValue().getValue()); + + result.add(paramDefObj); + } + break; + } + } + } + + return result; + } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java new file mode 100644 index 0000000..3061777 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java @@ -0,0 +1,152 @@ +/* + + + Copyright 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import hudson.model.*; +import hudson.model.Item; +import hudson.tasks.BuildStep; + +import net.sf.json.JSONObject; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jenkinsci.plugins.uniqueid.IdStore; + +import jenkins.model.Jenkins; + +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.CloudCause.JobStatus; + +import org.jenkinsci.plugins.uniqueid.IdStore; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; + +import java.util.Map; +import java.util.List; + +/** + * Jenkins server + */ + +public class JenkinsJobStatus { + + private AbstractBuild build; + private CloudCause cloudCause; + private BuildStep buildStep; + private Boolean newStep; + private Boolean isFatal; + + public JenkinsJobStatus(AbstractBuild build, CloudCause cloudCause, BuildStep buildStep, Boolean newStep, Boolean isFatal) { + this.build = build; + this.cloudCause = cloudCause; + this.buildStep = buildStep; + this.newStep = newStep; + this.isFatal = isFatal; + } + + public JSONObject generate() { + JSONObject result = new JSONObject(); + + evaluateSourceData(build, cloudCause); + + if(!(buildStep instanceof hudson.model.ParametersDefinitionProperty)) { + if (newStep) { + cloudCause.addStep(((Describable)buildStep).getDescriptor().getDisplayName(), JobStatus.started.toString(), "Started a build step", false); + } else { + String newStatus; + String message; + if (!isFatal) { + newStatus = JobStatus.success.toString(); + message = "The build step finished and the job will continue."; + } else { + newStatus = JobStatus.failure.toString(); + message = "The build step failed and the job can not continue."; + } + + cloudCause.updateLastStep(((Describable)buildStep).getDescriptor().getDisplayName(), newStatus, message, isFatal); + } + } + + // TODO: Premature success is causing successful results when job actually fails + // System.out.println("\t\tRESULT \t IS BUILDING \t hasntStartedYet \t isCompleteBuild"); + // System.out.println("\t\t" + build.getResult() + "\t\t" + build.isBuilding() + "\t\t" + build.hasntStartedYet() + "\t\t" + (build.getResult() == null ? "IT was NULL" : build.getResult().isCompleteBuild())); + + if (build.getResult() == null) { + if(build.isBuilding()) { + result.put("status", JobStatus.started.toString()); + } else { + result.put("status", JobStatus.unstarted.toString()); + } + } else { + if(build.getResult() == Result.SUCCESS) { + result.put("status", JobStatus.success.toString()); + } else { + result.put("status", JobStatus.failure.toString()); + } + } + + result.put("timestamp", System.currentTimeMillis()); + result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + result.put("name", build.getDisplayName()); + result.put("steps", cloudCause.getStepsArray()); + result.put("url", Jenkins.getInstance().getRootUrl() + build.getUrl()); + result.put("returnProps", cloudCause.getReturnProps()); + + result.put("jobExternalId", getJobUniqueIdFromBuild(build)); + + result.put("sourceData", cloudCause.getSourceDataJson()); + + return result; + } + + private String getJobUniqueIdFromBuild(AbstractBuild build) { + AbstractProject project = (AbstractProject)build.getProject(); + + String jenkinsId; + + if (IdStore.getId(Jenkins.getInstance()) != null) { + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } else { + IdStore.makeId(Jenkins.getInstance()); + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } + + return jenkinsId; + } + + private void evaluateSourceData(AbstractBuild build, CloudCause cause) { + List actions = build.getActions(); + + for(Action action : actions) { + // If using Hudson Git Plugin + if (action instanceof BuildData) { + Map branchMap = ((BuildData)action).getBuildsByBranchName(); + + for(String branchName : branchMap.keySet()) { + Build gitBuild = branchMap.get(branchName); + + if (gitBuild.getBuildNumber() == build.getNumber()) { + SourceData sourceData = new SourceData(branchName, gitBuild.getSHA1().getName(), "GIT"); + cause.setSourceData(sourceData); + } + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/SourceData.java b/src/main/java/com/ibm/devops/connect/SourceData.java new file mode 100644 index 0000000..8da8d3c --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/SourceData.java @@ -0,0 +1,51 @@ +package com.ibm.devops.connect; + +import net.sf.json.JSONObject; +import java.util.Set; + +public class SourceData { + + private String branch; + private String revision; + private String scmName; + private String type; + private Set remoteUrls; + + public SourceData(String branch, String revision, String type) { + this.branch = branch; + this.revision = revision; + this.type = type; + } + + public void setBranch (String branch) { + this.branch = branch; + } + + public void setRevision (String revision) { + this.revision = revision; + } + + public void setScmName(String scmName) { + this.scmName = scmName; + } + + public void setType (String type) { + this.type = type; + } + + public void setRemoteUrls (Set remoteUrls) { + this.remoteUrls = remoteUrls; + } + + public JSONObject toJson() { + JSONObject result = new JSONObject(); + + result.put("branch", branch); + result.put("revision", revision); + result.put("scmName", scmName); + result.put("type", type); + result.put("remoteUrls", remoteUrls.toArray()); + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java index f600806..901c593 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java @@ -32,6 +32,7 @@ public class DevOpsGlobalConfiguration extends GlobalConfiguration { private volatile boolean debug_mode; private volatile String syncId; private volatile String syncToken; + private volatile String instanceName; public DevOpsGlobalConfiguration() { load(); @@ -68,6 +69,15 @@ public String getSyncToken() { return syncToken; } + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + save(); + } + + public String getInstanceName() { + return instanceName; + } + public void setSyncToken(String syncToken) { this.syncToken = syncToken; save(); @@ -81,6 +91,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc debug_mode = Boolean.parseBoolean(formData.getString("debug_mode")); syncId = formData.getString("syncId"); syncToken = formData.getString("syncToken"); + instanceName = formData.getString("instanceName"); save(); return super.configure(req,formData); } diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly index 3c8a7f7..2b9ce06 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly @@ -38,6 +38,9 @@ + + + From 4b21682f32c5b7c270733b63dbbdff541eb94dd2 Mon Sep 17 00:00:00 2001 From: aberk Date: Mon, 16 Oct 2017 23:30:26 -0400 Subject: [PATCH 09/25] Add test connection button and begin listening to pipelines --- pom.xml | 7 +- .../com/ibm/devops/connect/CloudCause.java | 3 + .../connect/CloudFlowExecutionListener.java | 79 +++++++++++++++++++ .../ibm/devops/connect/CloudItemListener.java | 4 + .../ibm/devops/connect/CloudPublisher.java | 32 ++++---- .../devops/connect/CloudSocketComponent.java | 7 ++ .../ibm/devops/connect/CloudWorkListener.java | 33 +++++++- .../connect/ConnectComputerListener.java | 21 ++++- .../com/ibm/devops/connect/JenkinsJob.java | 13 +++ .../ibm/devops/connect/JenkinsJobStatus.java | 12 +-- .../com/ibm/devops/connect/SourceData.java | 4 +- .../devops/dra/DevOpsGlobalConfiguration.java | 34 +++++++- .../DevOpsGlobalConfiguration/config.jelly | 7 ++ 13 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java diff --git a/pom.xml b/pom.xml index 252b49e..369fdaa 100644 --- a/pom.xml +++ b/pom.xml @@ -218,12 +218,17 @@ org.jenkins-ci.plugins.workflow workflow-job - 2.1 + 2.11.2 org.eclipse.hudson.plugins git 3.0.1 + + org.jenkins-ci.plugins.workflow + workflow-api + 2.22 + diff --git a/src/main/java/com/ibm/devops/connect/CloudCause.java b/src/main/java/com/ibm/devops/connect/CloudCause.java index fa5f938..8a04b36 100644 --- a/src/main/java/com/ibm/devops/connect/CloudCause.java +++ b/src/main/java/com/ibm/devops/connect/CloudCause.java @@ -10,6 +10,9 @@ import net.sf.json.JSONObject; import net.sf.json.JSONArray; +/** +* This is the cause object that is attached to a build if it is started by the IBM Cloud. +*/ public class CloudCause extends Cause { public enum JobStatus { diff --git a/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java new file mode 100644 index 0000000..4b22555 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java @@ -0,0 +1,79 @@ +/* + + + Copyright 2016, 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import java.util.Map; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; +import hudson.model.BuildStepListener; +import hudson.tasks.BuildStep; +import hudson.model.AbstractBuild; +import hudson.tasks.Builder; +import hudson.model.Result; + +import jenkins.model.Jenkins; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +import com.ibm.devops.connect.CloudCause.JobStatus; + +import com.ibm.devops.dra.DevOpsGlobalConfiguration; + +import org.jenkinsci.plugins.uniqueid.IdStore; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; + +import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener; +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +@Extension +public class CloudFlowExecutionListener extends FlowExecutionListener { + public static final Logger log = LoggerFactory.getLogger(CloudFlowExecutionListener.class); + + @Override + public void onRunning(FlowExecution execution) { + System.out.println("11111111111------------> On Running"); + + System.out.println("===========>>>>>"); + for(FlowNode flowNode : execution.getCurrentHeads()){ + System.out.println(flowNode); + } + } + + @Override + public void onResumed(FlowExecution execution) { + System.out.println("222222222222------------> On Resumed"); + } + + @Override + public void onCompleted(FlowExecution execution) { + System.out.println("333333333333------------> On Completed"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java index efb07d5..61cff13 100644 --- a/src/main/java/com/ibm/devops/connect/CloudItemListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -68,9 +68,13 @@ private List buildJobsList() { log.info(logPrefix + "Building the list of Jenkins jobs..."); List allProjects= JenkinsServer.getAllItems(); List allJobs = new ArrayList(); + + CloudPublisher cloudPublisher = new CloudPublisher(); for (Item anItem : allProjects) { JenkinsJob jenkinsJob= new JenkinsJob(anItem); allJobs.add(jenkinsJob.toJson()); + + cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); } return allJobs; } diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index d16ac57..6673a9d 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -122,7 +122,7 @@ public boolean uploadJobStatus(JSONObject jobStatus) { } private boolean postToSyncAPI(String url, String payload) { - logPrefix= logPrefix + "uploadJobInfo "; + String localLogPrefix= logPrefix + "uploadJobInfo "; String resStr = ""; @@ -154,19 +154,19 @@ private boolean postToSyncAPI(String url, String payload) { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200")) { // get 200 response - log.info(logPrefix + "Upload Job Information successfully"); + log.info(localLogPrefix + "Upload Job Information successfully"); return true; } else { // if gets error status - log.error(logPrefix + "Error: Failed to upload Job, response status " + response.getStatusLine()); + log.error(localLogPrefix + "Error: Failed to upload Job, response status " + response.getStatusLine()); } } catch (JsonSyntaxException e) { - log.error(logPrefix + "Invalid Json response, response: " + resStr); + log.error(localLogPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.error(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.error(localLogPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } @@ -181,7 +181,7 @@ private boolean postToSyncAPI(String url, String payload) { } public boolean createIntegrationIfNecessary() { - logPrefix= logPrefix + "createIntegrationIfNecessary "; + String localLogPrefix = logPrefix + "createIntegrationIfNecessary "; String resStr = ""; try { CloseableHttpClient httpClient = HttpClients.createDefault(); @@ -209,22 +209,22 @@ public boolean createIntegrationIfNecessary() { resStr = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response - log.info(logPrefix + "Integration was retrieved"); + log.info(localLogPrefix + "Integration was retrieved"); return true; } else { // if gets error status log.info("--------------------------------------------"); - log.info(logPrefix + "No Integration Retrieved"); - log.info(logPrefix + "Attempting to create a new integration"); + log.info(localLogPrefix + "No Integration Retrieved"); + log.info(localLogPrefix + "Attempting to create a new integration"); return this.createIntegration(jenkinsId); } } catch (JsonSyntaxException e) { - log.error(logPrefix + "Invalid Json response, response: " + resStr); + log.error(localLogPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.info(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.info(localLogPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } @@ -239,7 +239,7 @@ public boolean createIntegrationIfNecessary() { } private boolean createIntegration(String jenkinsId) { - logPrefix= logPrefix + "createIntegration "; + String localLogPrefix= logPrefix + "createIntegration "; String resStr = ""; try { @@ -272,19 +272,19 @@ private boolean createIntegration(String jenkinsId) { if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response log.info("==================================================="); - log.info(logPrefix + "Created integration successfully"); + log.info(localLogPrefix + "Created integration successfully"); return true; } else { // if gets error status - log.error(logPrefix + "Error: Failed to create integration, response status " + response.getStatusLine()); + log.error(localLogPrefix + "Error: Failed to create integration, response status " + response.getStatusLine()); } } catch (JsonSyntaxException e) { - log.error(logPrefix + "Invalid Json response, response: " + resStr); + log.error(localLogPrefix + "Invalid Json response, response: " + resStr); } catch (IllegalStateException e) { // will be triggered when 403 Forbidden try { - log.error(logPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + log.error(localLogPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); } catch (UnsupportedEncodingException e1) { e1.printStackTrace(); } diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index 5c32d12..a8b8d32 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -102,4 +102,11 @@ public void disconnect() { } } } + + public boolean connected() { + if(socket == null) { + return false; + } + return socket.connected(); + } } diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 3c3130f..8585a61 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -34,6 +34,7 @@ import hudson.model.StringParameterValue; import hudson.model.queue.QueueTaskFuture; import hudson.model.Queue; +import hudson.model.Item; import java.util.ArrayList; import java.util.Iterator; @@ -42,6 +43,14 @@ import java.util.concurrent.ExecutionException; import java.lang.InterruptedException; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; + +//////TEMP + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener; + /* * When Spring is applying the @Transactional annotation, it creates a proxy class which wraps your class. * So when your bean is created in your application context, you are getting an object that is not of type @@ -75,7 +84,9 @@ public void call(ConnectSocket socket, String event, Object... args) { if (incomingJob.has("fullName")) { String fullName = incomingJob.get("fullName").toString(); Jenkins myJenkins = Jenkins.getInstance(); - AbstractProject abstractProject = (AbstractProject)myJenkins.getItem(fullName); + + Item item = myJenkins.getItem(fullName); + ArrayList parametersList = new ArrayList(); if(incomingJob.has("props")) { @@ -94,8 +105,24 @@ public void call(ConnectSocket socket, String event, Object... args) { returnProps = incomingJob.getJSONObject("returnProps"); } - Queue.Item queueItem = ParameterizedJobMixIn.scheduleBuild2(abstractProject, 0, new ParametersAction(parametersList), new CauseAction( - new CloudCause(socket, incomingJob.get("id").toString(), returnProps) )); + System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ " + Jenkins.VERSION); + for (FlowExecutionListener listener : ExtensionList.lookup(FlowExecutionListener.class)) { + System.out.println(listener.getClass()); + System.out.println("^^^^^^"); + } + + + if(item instanceof AbstractProject) { + AbstractProject abstractProject = (AbstractProject)item; + + ParameterizedJobMixIn.scheduleBuild2(abstractProject, 0, new ParametersAction(parametersList), new CauseAction(new CloudCause(socket, incomingJob.get("id").toString(), returnProps))); + } else if (item instanceof WorkflowJob) { + WorkflowJob workflowJob = (WorkflowJob)item; + + workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(new CloudCause(socket, incomingJob.get("id").toString(), returnProps) )); + } else { + log.warn("Unhandled job type found: " + item.getClass()); + } } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 9286c49..d16bc1f 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -20,6 +20,8 @@ public class ConnectComputerListener extends ComputerListener { public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); private String logPrefix= "[IBM Cloud DevOps] ConnectComputerListener#"; + private static CloudSocketComponent cloudSocketInstance; + @Override public void onOnline(Computer c) { String url = getConnectUrl(); @@ -27,12 +29,21 @@ public void onOnline(Computer c) { logPrefix= logPrefix + "onOnline "; CloudWorkListener listener = new CloudWorkListener(); - CloudSocketComponent socket = new CloudSocketComponent(listener, url); + + if(cloudSocketInstance != null && cloudSocketInstance.connected()) { + cloudSocketInstance.disconnect(); + } + + cloudSocketInstance = new CloudSocketComponent(listener, url); try { log.info(logPrefix + "Connecting to Cloud Services..."); - socket.connectToCloudServices(); - log.info(logPrefix + "Connected to Cloud Services!"); + getCloudSocketInstance().connectToCloudServices(); + if(getCloudSocketInstance().connected()) { + log.info(logPrefix + "Connected to Cloud Services!"); + } else { + log.warn(logPrefix + "Failed to connected to Cloud Services"); + } } catch (Exception e) { log.error(logPrefix + "Exception caught while connecting to Cloud Services: " + e); } @@ -41,4 +52,8 @@ public void onOnline(Computer c) { private String getConnectUrl() { return "https://uccloud-connect-stage1.stage1.mybluemix.net"; } + + public CloudSocketComponent getCloudSocketInstance() { + return ConnectComputerListener.cloudSocketInstance; + } } \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index 720d7a4..cdb8880 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -18,6 +18,10 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import hudson.model.Item; import hudson.model.ParametersDefinitionProperty; import hudson.model.queue.SubTask; +import hudson.model.ChoiceParameterDefinition; +import hudson.model.PasswordParameterDefinition; +import com.cloudbees.plugins.credentials.CredentialsParameterDefinition; +import hudson.model.BooleanParameterDefinition; import java.util.Collection; @@ -101,14 +105,23 @@ private JSONArray getJobParams() { if (action instanceof ParametersDefinitionProperty) { List paraDefs = ((ParametersDefinitionProperty)action).getParameterDefinitions(); for (ParameterDefinition paramDef : paraDefs) { + + // System.out.println(paramDef.getClass() + "\t - \t" + paramDef.getType()); JSONObject paramDefObj = new JSONObject(); paramDefObj.put("name", paramDef.getName()); paramDefObj.put("type", paramDef.getType()); paramDefObj.put("defaultValue", paramDef.getDefaultParameterValue().getValue()); + if(paramDef instanceof ChoiceParameterDefinition) { + List options = ((ChoiceParameterDefinition)paramDef).getChoices(); + + paramDefObj.put("options", options); + } + result.add(paramDefObj); } + break; } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java index 3061777..2a319a2 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java @@ -117,16 +117,16 @@ public JSONObject generate() { private String getJobUniqueIdFromBuild(AbstractBuild build) { AbstractProject project = (AbstractProject)build.getProject(); - String jenkinsId; + String projectId; - if (IdStore.getId(Jenkins.getInstance()) != null) { - jenkinsId = IdStore.getId(Jenkins.getInstance()); + if (IdStore.getId(project) != null) { + projectId = IdStore.getId(project); } else { - IdStore.makeId(Jenkins.getInstance()); - jenkinsId = IdStore.getId(Jenkins.getInstance()); + IdStore.makeId(project); + projectId = IdStore.getId(project); } - return jenkinsId; + return projectId; } private void evaluateSourceData(AbstractBuild build, CloudCause cause) { diff --git a/src/main/java/com/ibm/devops/connect/SourceData.java b/src/main/java/com/ibm/devops/connect/SourceData.java index 8da8d3c..06bc110 100644 --- a/src/main/java/com/ibm/devops/connect/SourceData.java +++ b/src/main/java/com/ibm/devops/connect/SourceData.java @@ -44,7 +44,9 @@ public JSONObject toJson() { result.put("revision", revision); result.put("scmName", scmName); result.put("type", type); - result.put("remoteUrls", remoteUrls.toArray()); + if(remoteUrls != null) { + result.put("remoteUrls", remoteUrls.toArray()); + } return result; } diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java index 901c593..992d676 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java @@ -16,11 +16,19 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import hudson.CopyOnWrite; import hudson.Extension; +import hudson.model.Computer; import hudson.util.ListBoxModel; import jenkins.model.GlobalConfiguration; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; - +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerResponse; +import hudson.util.FormFieldValidator; +import com.ibm.devops.connect.CloudSocketComponent; +import com.ibm.devops.connect.ConnectComputerListener; + +import java.io.IOException; +import javax.servlet.ServletException; /** * Created by lix on 7/20/17. */ @@ -93,6 +101,9 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc syncToken = formData.getString("syncToken"); instanceName = formData.getString("instanceName"); save(); + + reconnectCloudSocket(); + return super.configure(req,formData); } @@ -101,4 +112,25 @@ public ListBoxModel doFillRegionItems() { ListBoxModel items = new ListBoxModel(); return items; } + + @Deprecated + public void doTestConnection(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + new FormFieldValidator(req, rsp, true) { + @Override + protected void check() throws IOException, ServletException { + CloudSocketComponent socket = new ConnectComputerListener().getCloudSocketInstance(); + if(socket.connected()) { + ok("Success - Connected to IBM Cloud Service"); + } else { + error("Not connected to IBM Cloud Services - Please ensure that current values are applied"); + } + } + }.process(); + } + + private void reconnectCloudSocket() { + ConnectComputerListener connectComputerListener = new ConnectComputerListener(); + + connectComputerListener.onOnline(Computer.currentComputer()); + } } diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly index 2b9ce06..8500446 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly @@ -47,5 +47,12 @@ + +
+ +
+
From f686186547b3c604f26b49adf87f4751d86b7777 Mon Sep 17 00:00:00 2001 From: aberk Date: Wed, 18 Oct 2017 14:50:28 -0400 Subject: [PATCH 10/25] Handle Job Requests with all param types --- .../ibm/devops/connect/CloudWorkListener.java | 77 ++++++++++++++----- .../com/ibm/devops/connect/JenkinsJob.java | 1 + 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 8585a61..09202fa 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -32,12 +32,20 @@ import hudson.model.CauseAction; import hudson.model.ParameterValue; import hudson.model.StringParameterValue; +import hudson.model.BooleanParameterValue; +import hudson.model.TextParameterValue; +import hudson.model.PasswordParameterValue; import hudson.model.queue.QueueTaskFuture; import hudson.model.Queue; import hudson.model.Item; +import hudson.model.ParameterDefinition; +import hudson.model.ParametersDefinitionProperty; import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.HashMap; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -87,31 +95,13 @@ public void call(ConnectSocket socket, String event, Object... args) { Item item = myJenkins.getItem(fullName); - ArrayList parametersList = new ArrayList(); - - if(incomingJob.has("props")) { - JSONObject props = incomingJob.getJSONObject("props"); - Iterator keys = props.keys(); - - while( keys.hasNext() ) { - String key = (String)keys.next(); - - parametersList.add(new StringParameterValue(key, props.get(key).toString())); - } - } + List parametersList = generateParamList(incomingJob, getParameterTypeMap(item)); JSONObject returnProps = new JSONObject(); if(incomingJob.has("returnProps")) { returnProps = incomingJob.getJSONObject("returnProps"); } - System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ " + Jenkins.VERSION); - for (FlowExecutionListener listener : ExtensionList.lookup(FlowExecutionListener.class)) { - System.out.println(listener.getClass()); - System.out.println("^^^^^^"); - } - - if(item instanceof AbstractProject) { AbstractProject abstractProject = (AbstractProject)item; @@ -143,4 +133,53 @@ private void sendResult(ConnectSocket socket, String id, WorkStatus status, Stri } socket.emit("set_work_status", json.toString()); } + + private List generateParamList (JSONObject incomingJob, Map typeMap) { + ArrayList result = new ArrayList(); + + if(incomingJob.has("props")) { + JSONObject props = incomingJob.getJSONObject("props"); + Iterator keys = props.keys(); + while( keys.hasNext() ) { + String key = (String)keys.next(); + Object value = props.get(key); + String type = typeMap.get(key); + + ParameterValue paramValue; + + if(type == null) { + + } else if(type.equalsIgnoreCase("BooleanParameterDefinition")) { + result.add(new BooleanParameterValue(key, (boolean)props.get(key))); + } else if(type.equalsIgnoreCase("PasswordParameterDefinition")) { + result.add(new PasswordParameterValue(key, props.get(key).toString())); + } else if(type.equalsIgnoreCase("TextParameterDefinition")) { + result.add(new TextParameterValue(key, props.get(key).toString())); + } else { + result.add(new StringParameterValue(key, props.get(key).toString())); + } + } + } + + return result; + } + + private Map getParameterTypeMap(Item item) { + Map result = new HashMap(); + + if(item instanceof AbstractProject) { + List actions = ((AbstractProject)item).getActions(); + + for(Action action : actions) { + if (action instanceof ParametersDefinitionProperty) { + List paraDefs = ((ParametersDefinitionProperty)action).getParameterDefinitions(); + for (ParameterDefinition paramDef : paraDefs) { + result.put(paramDef.getName(), paramDef.getType()); + } + } + } + } + + return result; + } } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index cdb8880..5e2546b 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -111,6 +111,7 @@ private JSONArray getJobParams() { JSONObject paramDefObj = new JSONObject(); paramDefObj.put("name", paramDef.getName()); paramDefObj.put("type", paramDef.getType()); + paramDefObj.put("description", paramDef.getDescription()); paramDefObj.put("defaultValue", paramDef.getDefaultParameterValue().getValue()); if(paramDef instanceof ChoiceParameterDefinition) { From 055f383f7f8844db2d27c76f4e79b905ef5beb7a Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Fri, 20 Oct 2017 10:00:35 +0200 Subject: [PATCH 11/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/1113 handle job creation event --- pom.xml | 14 +-- .../ibm/devops/connect/CloudWorkListener.java | 8 +- .../com/ibm/devops/connect/JenkinsServer.java | 107 ++++++++++++++++-- .../help-instanceName.html | 20 ++++ 4 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html diff --git a/pom.xml b/pom.xml index 369fdaa..a994753 100644 --- a/pom.xml +++ b/pom.xml @@ -197,7 +197,12 @@ uc-cloud-connect-java uc-cloud-connect-java - 1.0-SNAPSHOT + 1.0 + + + org.jenkins-ci.plugins + cloudbees-folder + 6.0.4 org.apache.commons @@ -210,11 +215,6 @@ 0.8.3 compile - - org.jenkins-ci.plugins - unique-id - 2.1.3 - org.jenkins-ci.plugins.workflow workflow-job @@ -231,4 +231,4 @@ 2.22 - + \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 09202fa..31475d5 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -88,7 +88,13 @@ public void call(ConnectSocket socket, String event, Object... args) { for(int i=0; i < incomingJobs.size(); i++) { JSONObject incomingJob = incomingJobs.getJSONObject(i); - + // sample job creation request from a toolchain + if (incomingJob.has("jobType") && "new".equalsIgnoreCase(incomingJob.get("jobType").toString())) { + log.info(logPrefix + "Job creation request received."); + // delegating job creation to the Jenkins server + JenkinsServer.createJob(incomingJob); + } + if (incomingJob.has("fullName")) { String fullName = incomingJob.get("fullName").toString(); Jenkins myJenkins = Jenkins.getInstance(); diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java index 2a42f78..1890dc7 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsServer.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -14,15 +14,21 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of package com.ibm.devops.connect; +import com.cloudbees.hudson.plugins.folder.Folder; + import hudson.model.*; import hudson.model.Item; import hudson.model.TopLevelItem; +import java.io.ByteArrayInputStream; import java.util.Collection; import java.util.Iterator; import java.util.List; + import jenkins.model.Jenkins; +import net.sf.json.*; + import org.apache.commons.lang.builder.ToStringBuilder; import org.slf4j.Logger; @@ -36,12 +42,13 @@ public class JenkinsServer { public static final Logger log = LoggerFactory.getLogger(JenkinsServer.class); private static String logPrefix= "[IBM Cloud DevOps] JenkinsServer#"; + private static String FOLDER_SPEC= "Folder created by the IBM Devops plugin"; + private static String jobSrc= "\r\n\r\n \r\n false\r\n \r\n \r\n \r\n \r\n * * * * *\r\n false\r\n \r\n \r\n \r\n \r\n \r\n \r\n 2\r\n \r\n \r\n https://github.com/ejodet/discovery-nodejs\r\n \r\n \r\n \r\n \r\n */mastertoto\r\n \r\n \r\n false\r\n \r\n \r\n \r\n Jenkinsfile\r\n true\r\n \r\n \r\n"; // not used yet - but might be used later public static Collection getJobNames() { - logPrefix= logPrefix + "getJobNames "; - log.info(logPrefix + "get the list of job names"); + log.info(logPrefix + "getJobNames - get the list of job names"); Collection allJobNames= Jenkins.getInstance().getJobNames(); - log.info(logPrefix + "retrieved " + allJobNames.size() + " JobNames"); + log.info(logPrefix + "getJobNames - retrieved " + allJobNames.size() + " JobNames"); for (Iterator iterator = allJobNames.iterator(); iterator.hasNext();) { String aJobName = (String) iterator.next(); log.info(logPrefix + "job: " + aJobName); @@ -50,10 +57,96 @@ public static Collection getJobNames() { } public static List getAllItems() { - logPrefix= logPrefix + "getAllItems "; - log.info(logPrefix + "get the list of all items"); + log.info(logPrefix + "getAllItems - get the list of all items"); List allProjects= Jenkins.getInstance().getAllItems(AbstractItem.class); - log.info(logPrefix + "Retrieved " + allProjects.size() + " projects"); + log.info(logPrefix + "getAllItems - Retrieved " + allProjects.size() + " projects"); return Jenkins.getInstance().getAllItems(); } -} + + public static void createJob(JSONObject newJob) { + log.info(logPrefix + "createJob - Creating a new job."); + if(validCreationRequest(newJob)) { + // temporarily disable security as we are not allowed to create jobs as anonymous + disableSecurity(); + try { + JSONObject props = newJob.getJSONObject("props"); + // create folder + String folderName= props.get("folderName").toString(); + createFolder(folderName); + // verify folder was created + Folder targetFolder= getFolder(folderName); + if (targetFolder == null) { + log.info(logPrefix + "createJob - target folder not retrieved. Exiting creation process"); + } else { + log.info(logPrefix + "createJob - target folder retrieved !!!!!"); + // create job in target folder + String jobSrc= props.get("source").toString(); + String jobName= props.get("jobName").toString(); + createJobInFolder(targetFolder, jobName, jobSrc); + } + } catch (Exception e) { + log.error(logPrefix + "An unexepected error occurred while creating job."); + e.printStackTrace(); + } finally { + // be sure to re-enable security + reloadConfiguration(); + } + } + } + + private static boolean validCreationRequest(JSONObject newJob) { + log.info(logPrefix + "validCreationRequest - Validating creation payload."); + boolean valid= false; + if(newJob.has("props")) { + JSONObject props = newJob.getJSONObject("props"); + if (props.has("folderName") && props.has("jobName") && props.has("source")) { + log.debug(logPrefix + "validCreationRequest - Payload is valid."); + valid= true; + } else { + log.error(logPrefix + "validCreationRequest - Payload is not valid!"); + } + } + return valid; + } + + private static void createFolder(String folderName) { + log.info(logPrefix + "createFolder - Creating folder " + folderName); + try { + Jenkins.getInstance().createProjectFromXML(folderName, new ByteArrayInputStream(FOLDER_SPEC.getBytes())); + log.debug(logPrefix + folderName + " was created successfully!"); + } catch (Exception e) { + // folder might be existing + log.debug(logPrefix + folderName + " was not created."); + e.printStackTrace(); + } + } + + private static void createJobInFolder(Folder targetFolder, String jobName, String source) { + log.info(logPrefix + "createItem - Creating job " + jobName); + try { + targetFolder.createProjectFromXML(jobName, new ByteArrayInputStream(source.getBytes())); + log.debug(logPrefix + jobName + " was created successfully!"); + } catch (Exception e) { + log.error(logPrefix + jobName + " was not created."); + e.printStackTrace(); + } + } + + private static void disableSecurity() { + log.debug(logPrefix + "disableSecurity()"); + Jenkins.getInstance().disableSecurity(); + } + + private static void reloadConfiguration() { + log.debug(logPrefix + "reloadConfiguration()"); + try { + Jenkins.getInstance().reload(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static Folder getFolder(String folderName) { + return (Folder) Jenkins.getInstance().getItem(folderName); + } +} \ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html new file mode 100644 index 0000000..f6a772d --- /dev/null +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html @@ -0,0 +1,20 @@ + + +
+ Specify the instance name of this Jenkins server. See Getting +started with Continuous Release documentation. + +
\ No newline at end of file From 328e12602d478b6e15f033aa89c1ac898f9c0446 Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Wed, 25 Oct 2017 17:02:01 +0200 Subject: [PATCH 12/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/1114 create creds if necessary --- .../com/ibm/devops/connect/JenkinsServer.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java index 1890dc7..9dac07b 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsServer.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -16,6 +16,11 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import com.cloudbees.hudson.plugins.folder.Folder; +import com.cloudbees.plugins.credentials.*; +import com.cloudbees.plugins.credentials.common.*; +import com.cloudbees.plugins.credentials.domains.*; +import com.cloudbees.plugins.credentials.impl.*; + import hudson.model.*; import hudson.model.Item; import hudson.model.TopLevelItem; @@ -42,9 +47,14 @@ public class JenkinsServer { public static final Logger log = LoggerFactory.getLogger(JenkinsServer.class); private static String logPrefix= "[IBM Cloud DevOps] JenkinsServer#"; + // creds + private static String BLX_CREDS= "IBM_CLOUD_DEVOPS_CREDS_API"; + private static String BLX_CREDS_DESC= "IBM DevOps Bluemix credentials"; + // folder and job private static String FOLDER_SPEC= "Folder created by the IBM Devops plugin"; private static String jobSrc= "\r\n\r\n \r\n false\r\n \r\n \r\n \r\n \r\n * * * * *\r\n false\r\n \r\n \r\n \r\n \r\n \r\n \r\n 2\r\n \r\n \r\n https://github.com/ejodet/discovery-nodejs\r\n \r\n \r\n \r\n \r\n */mastertoto\r\n \r\n \r\n false\r\n \r\n \r\n \r\n Jenkinsfile\r\n true\r\n \r\n \r\n"; - // not used yet - but might be used later + + // not used yet - but might be used later public static Collection getJobNames() { log.info(logPrefix + "getJobNames - get the list of job names"); Collection allJobNames= Jenkins.getInstance().getJobNames(); @@ -69,6 +79,8 @@ public static void createJob(JSONObject newJob) { // temporarily disable security as we are not allowed to create jobs as anonymous disableSecurity(); try { + // create creds if necessary + createCredentials(newJob) ; JSONObject props = newJob.getJSONObject("props"); // create folder String folderName= props.get("folderName").toString(); @@ -94,8 +106,28 @@ public static void createJob(JSONObject newJob) { } } + private static void createCredentials(JSONObject newJob) { + if(newJob.has("props")) { + JSONObject props = newJob.getJSONObject("props"); + if (props.has("userName") && props.has("password")) { // not all jobs require creds creation + try { + log.debug(logPrefix + "createCredentials - creating " + BLX_CREDS + " credentials."); + Credentials creds = (Credentials) new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, BLX_CREDS, BLX_CREDS_DESC, props.get("userName").toString(), props.get("password").toString()); + SystemCredentialsProvider.getInstance().getCredentials().add(creds); + SystemCredentialsProvider.getInstance().save(); + log.debug(logPrefix + "createCredentials " + BLX_CREDS + " successfully created."); + } catch (Exception e) { + log.error(logPrefix + "createCredentials - unable to create " + BLX_CREDS + " credentials."); + e.printStackTrace(); + } + } else { + log.debug(logPrefix + "createCredentials - credentials creation not required."); + } + } + } + private static boolean validCreationRequest(JSONObject newJob) { - log.info(logPrefix + "validCreationRequest - Validating creation payload."); + log.debug(logPrefix + "validCreationRequest - Validating creation payload."); boolean valid= false; if(newJob.has("props")) { JSONObject props = newJob.getJSONObject("props"); @@ -110,7 +142,7 @@ private static boolean validCreationRequest(JSONObject newJob) { } private static void createFolder(String folderName) { - log.info(logPrefix + "createFolder - Creating folder " + folderName); + log.debug(logPrefix + "createFolder - Creating folder " + folderName); try { Jenkins.getInstance().createProjectFromXML(folderName, new ByteArrayInputStream(FOLDER_SPEC.getBytes())); log.debug(logPrefix + folderName + " was created successfully!"); @@ -122,7 +154,7 @@ private static void createFolder(String folderName) { } private static void createJobInFolder(Folder targetFolder, String jobName, String source) { - log.info(logPrefix + "createItem - Creating job " + jobName); + log.debug(logPrefix + "createItem - Creating job " + jobName); try { targetFolder.createProjectFromXML(jobName, new ByteArrayInputStream(source.getBytes())); log.debug(logPrefix + jobName + " was created successfully!"); From b07357cab57ff9779c84fcb9f341b4f288e03831 Mon Sep 17 00:00:00 2001 From: aberk Date: Fri, 1 Dec 2017 15:17:20 -0500 Subject: [PATCH 13/25] Update Integrations with Server URl - Don't send Folders up with jobs - Add the prod environment endpoints - Remove name field from global config - Make Sync Token a secure parameter --- .../cloud-connect-java-1.0.920563.jar.md5 | 0 .../cloud-connect-java-1.0.920563.jar.sha1 | 0 .../cloud-connect-java-1.0.920563.pom | 0 .../cloud-connect-java-1.0.920563.pom.md5 | 0 .../cloud-connect-java-1.0.920563.pom.sha1 | 0 .../1.0.920563/maven-metadata.xml | 11 +++ pom.xml | 19 +++-- .../ibm/devops/connect/CloudItemListener.java | 31 +++++--- .../ibm/devops/connect/CloudPublisher.java | 74 +++++++++++++++++-- .../ibm/devops/connect/CloudWorkListener.java | 8 +- .../connect/ConnectComputerListener.java | 5 +- .../ibm/devops/connect/EndpointManager.java | 30 ++++++++ .../com/ibm/devops/connect/EndpointsYP.java | 19 +++++ .../com/ibm/devops/connect/EndpointsYS1.java | 19 +++++ .../com/ibm/devops/connect/IEndpoints.java | 11 +++ .../com/ibm/devops/connect/JenkinsJob.java | 25 ++++--- .../devops/dra/DevOpsGlobalConfiguration.java | 19 +---- .../DevOpsGlobalConfiguration/config.jelly | 5 +- 18 files changed, 217 insertions(+), 59 deletions(-) rename lib/{ => uc-cloud-connect-java/cloud-connect-java/1.0.920563}/cloud-connect-java-1.0.920563.jar.md5 (100%) rename lib/{ => uc-cloud-connect-java/cloud-connect-java/1.0.920563}/cloud-connect-java-1.0.920563.jar.sha1 (100%) rename lib/{ => uc-cloud-connect-java/cloud-connect-java/1.0.920563}/cloud-connect-java-1.0.920563.pom (100%) rename lib/{ => uc-cloud-connect-java/cloud-connect-java/1.0.920563}/cloud-connect-java-1.0.920563.pom.md5 (100%) rename lib/{ => uc-cloud-connect-java/cloud-connect-java/1.0.920563}/cloud-connect-java-1.0.920563.pom.sha1 (100%) create mode 100644 lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml create mode 100644 src/main/java/com/ibm/devops/connect/EndpointManager.java create mode 100644 src/main/java/com/ibm/devops/connect/EndpointsYP.java create mode 100644 src/main/java/com/ibm/devops/connect/EndpointsYS1.java create mode 100644 src/main/java/com/ibm/devops/connect/IEndpoints.java diff --git a/lib/cloud-connect-java-1.0.920563.jar.md5 b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.jar.md5 similarity index 100% rename from lib/cloud-connect-java-1.0.920563.jar.md5 rename to lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.jar.md5 diff --git a/lib/cloud-connect-java-1.0.920563.jar.sha1 b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.jar.sha1 similarity index 100% rename from lib/cloud-connect-java-1.0.920563.jar.sha1 rename to lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.jar.sha1 diff --git a/lib/cloud-connect-java-1.0.920563.pom b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom similarity index 100% rename from lib/cloud-connect-java-1.0.920563.pom rename to lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom diff --git a/lib/cloud-connect-java-1.0.920563.pom.md5 b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom.md5 similarity index 100% rename from lib/cloud-connect-java-1.0.920563.pom.md5 rename to lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom.md5 diff --git a/lib/cloud-connect-java-1.0.920563.pom.sha1 b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom.sha1 similarity index 100% rename from lib/cloud-connect-java-1.0.920563.pom.sha1 rename to lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/cloud-connect-java-1.0.920563.pom.sha1 diff --git a/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml new file mode 100644 index 0000000..370fa1c --- /dev/null +++ b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml @@ -0,0 +1,11 @@ + + + uc-cloud-connect-java + cloud-connect-java/artifactId> + + + 1.0.920563 + + 20150821193926 + + diff --git a/pom.xml b/pom.xml index 369fdaa..8cb67be 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ maven.jenkins-ci.org https://repo.jenkins-ci.org/snapshots/ + @@ -105,6 +106,10 @@ + + Local repository + file://${basedir}/lib + repo.jenkins-ci.org https://repo.jenkins-ci.org/public/ @@ -112,10 +117,6 @@ - - Local repository - file://${basedir}/lib - spring-snapshots Spring Snapshots @@ -196,8 +197,9 @@
uc-cloud-connect-java - uc-cloud-connect-java - 1.0-SNAPSHOT + cloud-connect-java + 1.0.920563 + compile org.apache.commons @@ -230,5 +232,10 @@ workflow-api 2.22 + + org.jenkins-ci.plugins + cloudbees-folder + 6.0.0 + diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java index 61cff13..d9f3e5d 100644 --- a/src/main/java/com/ibm/devops/connect/CloudItemListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -30,11 +30,13 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import net.sf.json.JSONObject; +import com.cloudbees.hudson.plugins.folder.Folder; + @Extension public class CloudItemListener extends ItemListener { public static final Logger log = LoggerFactory.getLogger(CloudItemListener.class); private String logPrefix= "[IBM Cloud DevOps] CloudItemListener#"; - + public CloudItemListener(){ logPrefix= logPrefix + "CloudItemListener "; log.info(logPrefix + "CloudItemListener started..."); @@ -45,36 +47,41 @@ public CloudItemListener(){ public void onCreated(Item item) { handleEvent(item, "CREATED"); } - + @Override public void onDeleted(Item item) { handleEvent(item, "DELETED"); } - + @Override public void onUpdated(Item item) { handleEvent(item, "UPDATED"); } private void handleEvent(Item item, String phase) { - JenkinsJob jenkinsJob= new JenkinsJob(item); - log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); - CloudPublisher cloudPublisher = new CloudPublisher(); - cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + if( !(item instanceof Folder) ) { + JenkinsJob jenkinsJob= new JenkinsJob(item); + log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + } + // we'll handle the updates to the sync app here } - + private List buildJobsList() { log.info(logPrefix + "Building the list of Jenkins jobs..."); List allProjects= JenkinsServer.getAllItems(); List allJobs = new ArrayList(); - + CloudPublisher cloudPublisher = new CloudPublisher(); for (Item anItem : allProjects) { - JenkinsJob jenkinsJob= new JenkinsJob(anItem); - allJobs.add(jenkinsJob.toJson()); + if( !(anItem instanceof Folder) ) { + JenkinsJob jenkinsJob= new JenkinsJob(anItem); + allJobs.add(jenkinsJob.toJson()); - cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + } } return allJobs; } diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index 6673a9d..22bd3ff 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -30,6 +30,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpGet; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -89,17 +90,16 @@ public class CloudPublisher { private String buildNumber; public CloudPublisher() { - } private String getSyncApiUrl() { - // return "http://localhost:6002/"; - - return "https://ucreporting-sync-api-stage1.stage1.mybluemix.net/"; + EndpointManager em = new EndpointManager(); + return em.getSyncApiEndpoint(); } private String getSyncStoreUrl() { - return "https://uccloud-sync-store-stage1.stage1.mybluemix.net/"; + EndpointManager em = new EndpointManager(); + return em.getSyncStoreEndpoint(); } /** @@ -128,7 +128,7 @@ private boolean postToSyncAPI(String url, String payload) { try { CloseableHttpClient httpClient = HttpClients.createDefault(); - + String jenkinsId; if (IdStore.getId(Jenkins.getInstance()) != null) { @@ -210,11 +210,20 @@ public boolean createIntegrationIfNecessary() { if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { // get 200 response log.info(localLogPrefix + "Integration was retrieved"); + + JSONObject jsonBody = JSONObject.fromObject(resStr); + + String serverUrl = Jenkins.getInstance().getRootUrl(); + + if(!serverUrl.equals(jsonBody.get("serverUrl"))) { + log.info(localLogPrefix + "Must update server url on integration..."); + updateIntegrationServerUrl(jsonBody, serverUrl); + } + return true; } else { // if gets error status - log.info("--------------------------------------------"); log.info(localLogPrefix + "No Integration Retrieved"); log.info(localLogPrefix + "Attempting to create a new integration"); return this.createIntegration(jenkinsId); @@ -254,6 +263,7 @@ private boolean createIntegration(String jenkinsId) { newIntegration.put("id", jenkinsId); newIntegration.put("dateCreated", System.currentTimeMillis()); newIntegration.put("docType", "integration"); + newIntegration.put("serverUrl", Jenkins.getInstance().getRootUrl()); HttpPost postMethod = new HttpPost(url); // postMethod = addProxyInformation(postMethod); @@ -298,4 +308,54 @@ private boolean createIntegration(String jenkinsId) { return false; } + private boolean updateIntegrationServerUrl(JSONObject payload, String newServerUrl) { + String localLogPrefix= logPrefix + "updateIntegrationServerUrl "; + + try { + CloseableHttpClient httpClient = HttpClients.createDefault(); + + String url = this.getSyncStoreUrl() + INTEGRATIONS_ENDPOINT_URL; + + payload.put("serverUrl", newServerUrl); + + HttpPost putMethod = new HttpPost(url); + // putMethod = addProxyInformation(putMethod); + putMethod.setHeader("Content-Type", "application/json"); + String authEncoding = DatatypeConverter.printBase64Binary((Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId() + ":" + Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()).getBytes("UTF-8")); + putMethod.setHeader("Authorization", "Basic " + authEncoding); + + putMethod.setHeader("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + + StringEntity data = new StringEntity(payload.toString()); + putMethod.setEntity(data); + + CloseableHttpResponse response = httpClient.execute(putMethod); + + if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { + log.info(localLogPrefix + "Upated integration successfully"); + return true; + + } else { + // if gets error status + log.error(localLogPrefix + "Error: Failed to update integration, response status " + response.getStatusLine()); + } + } catch (JsonSyntaxException e) { + log.error(localLogPrefix + "Invalid Json response, response"); + } catch (IllegalStateException e) { + // will be triggered when 403 Forbidden + try { + log.error(localLogPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + } catch (UnsupportedEncodingException e1) { + e1.printStackTrace(); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + } diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 09202fa..391f467 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -68,7 +68,7 @@ public class CloudWorkListener implements IWorkListener { public static final Logger log = LoggerFactory.getLogger(CloudWorkListener.class); private String logPrefix= "[IBM Cloud DevOps] CloudWorkListener#"; - + public CloudWorkListener() { } @@ -91,9 +91,13 @@ public void call(ConnectSocket socket, String event, Object... args) { if (incomingJob.has("fullName")) { String fullName = incomingJob.get("fullName").toString(); + Jenkins myJenkins = Jenkins.getInstance(); Item item = myJenkins.getItem(fullName); + if(item == null) { + item = myJenkins.getItemByFullName(fullName); + } List parametersList = generateParamList(incomingJob, getParameterTypeMap(item)); @@ -110,6 +114,8 @@ public void call(ConnectSocket socket, String event, Object... args) { WorkflowJob workflowJob = (WorkflowJob)item; workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(new CloudCause(socket, incomingJob.get("id").toString(), returnProps) )); + } else if (item == null) { + log.warn("No Item Found"); } else { log.warn("Unhandled job type found: " + item.getClass()); } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index d16bc1f..986933c 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -19,7 +19,7 @@ public class ConnectComputerListener extends ComputerListener { public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); private String logPrefix= "[IBM Cloud DevOps] ConnectComputerListener#"; - + private static CloudSocketComponent cloudSocketInstance; @Override @@ -50,7 +50,8 @@ public void onOnline(Computer c) { } private String getConnectUrl() { - return "https://uccloud-connect-stage1.stage1.mybluemix.net"; + EndpointManager em = new EndpointManager(); + return em.getConnectEndpoint(); } public CloudSocketComponent getCloudSocketInstance() { diff --git a/src/main/java/com/ibm/devops/connect/EndpointManager.java b/src/main/java/com/ibm/devops/connect/EndpointManager.java new file mode 100644 index 0000000..68fea50 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/EndpointManager.java @@ -0,0 +1,30 @@ +package com.ibm.devops.connect; + +public class EndpointManager { + + // TODO: Make configurable at build time or otherwise + //private static String profile = "YP"; + private static String profile = "YS1"; + + private IEndpoints endpointProvider; + + public EndpointManager() { + if(profile.equals("YS1")) { + endpointProvider = new EndpointsYS1(); + } else { + endpointProvider = new EndpointsYP(); + } + } + + public String getSyncApiEndpoint() { + return endpointProvider.getSyncApiEndpoint(); + } + + public String getSyncStoreEndpoint() { + return endpointProvider.getSyncStoreEndpoint(); + } + + public String getConnectEndpoint() { + return endpointProvider.getConnectEndpoint(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/EndpointsYP.java b/src/main/java/com/ibm/devops/connect/EndpointsYP.java new file mode 100644 index 0000000..116e906 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/EndpointsYP.java @@ -0,0 +1,19 @@ +package com.ibm.devops.connect; + +public class EndpointsYP implements IEndpoints { + private static final String SYNC_API_ENPOINT = "https://ucreporting-sync-api.mybluemix.net/"; + private static final String SYNC_STORE_ENPOINT = "https://uccloud-sync-store.mybluemix.net/"; + private static final String CONNECT_ENPOINT = "https://uccloud-connect.mybluemix.net"; + + public String getSyncApiEndpoint() { + return SYNC_API_ENPOINT; + } + + public String getSyncStoreEndpoint() { + return SYNC_STORE_ENPOINT; + } + + public String getConnectEndpoint() { + return CONNECT_ENPOINT; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/EndpointsYS1.java b/src/main/java/com/ibm/devops/connect/EndpointsYS1.java new file mode 100644 index 0000000..5389869 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/EndpointsYS1.java @@ -0,0 +1,19 @@ +package com.ibm.devops.connect; + +public class EndpointsYS1 implements IEndpoints { + private static final String SYNC_API_ENPOINT = "https://ucreporting-sync-api-stage1.stage1.mybluemix.net/"; + private static final String SYNC_STORE_ENPOINT = "https://uccloud-sync-store-stage1.stage1.mybluemix.net/"; + private static final String CONNECT_ENPOINT = "https://uccloud-connect-stage1.stage1.mybluemix.net"; + + public String getSyncApiEndpoint() { + return SYNC_API_ENPOINT; + } + + public String getSyncStoreEndpoint() { + return SYNC_STORE_ENPOINT; + } + + public String getConnectEndpoint() { + return CONNECT_ENPOINT; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/IEndpoints.java b/src/main/java/com/ibm/devops/connect/IEndpoints.java new file mode 100644 index 0000000..c93a09f --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/IEndpoints.java @@ -0,0 +1,11 @@ +package com.ibm.devops.connect; + +public interface IEndpoints { + + public String getSyncApiEndpoint(); + + public String getSyncStoreEndpoint(); + + public String getConnectEndpoint(); + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index 5e2546b..e43a8dc 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -47,7 +47,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of public class JenkinsJob { final private Item item; public static final Logger log = LoggerFactory.getLogger(JenkinsJob.class); - + public JenkinsJob (Item item) { this.item= item; } @@ -63,12 +63,13 @@ public JSONObject toJson() { String name= this.item.getName(); String fullName= this.item.getFullName(); String jobUrl= this.item.getUrl(); - + JSONObject jobToJson = new JSONObject(); - jobToJson.put("displayName", this.item.getDisplayName()); - jobToJson.put("name", this.item.getName()); - jobToJson.put("fullName", this.item.getFullName()); - jobToJson.put("jobUrl", this.item.getUrl()); + jobToJson.put("displayName", displayName); + jobToJson.put("name", name); + jobToJson.put("fullName", fullName); + jobToJson.put("jobUrl", jobUrl); + jobToJson.put("jenkinsClass", this.item.getClass()); String jobId; @@ -81,13 +82,13 @@ public JSONObject toJson() { jobToJson.put("id", jobId); jobToJson.put("instanceType", "JENKINS"); - jobToJson.put("instanceName", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getInstanceName()); + //jobToJson.put("instanceName", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getInstanceName()); if(this.item instanceof WorkflowJob) { jobToJson.put("isPipeline", true); // TODO: Find a way to get Stage definitions } else { - jobToJson.put("isPipeline", false); + jobToJson.put("isPipeline", false); } jobToJson.put("params", getJobParams()); @@ -105,14 +106,14 @@ private JSONArray getJobParams() { if (action instanceof ParametersDefinitionProperty) { List paraDefs = ((ParametersDefinitionProperty)action).getParameterDefinitions(); for (ParameterDefinition paramDef : paraDefs) { - - // System.out.println(paramDef.getClass() + "\t - \t" + paramDef.getType()); - JSONObject paramDefObj = new JSONObject(); paramDefObj.put("name", paramDef.getName()); paramDefObj.put("type", paramDef.getType()); paramDefObj.put("description", paramDef.getDescription()); - paramDefObj.put("defaultValue", paramDef.getDefaultParameterValue().getValue()); + ParameterValue pValue = paramDef.getDefaultParameterValue(); + if (pValue != null) { + paramDefObj.put("defaultValue", pValue.getValue()); + } if(paramDef instanceof ChoiceParameterDefinition) { List options = ((ChoiceParameterDefinition)paramDef).getChoices(); diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java index 992d676..26b31f6 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java @@ -40,7 +40,6 @@ public class DevOpsGlobalConfiguration extends GlobalConfiguration { private volatile boolean debug_mode; private volatile String syncId; private volatile String syncToken; - private volatile String instanceName; public DevOpsGlobalConfiguration() { load(); @@ -63,29 +62,20 @@ public void setConsoleUrl(String consoleUrl) { this.consoleUrl = consoleUrl; save(); } - + public String getSyncId() { return syncId; } - + public void setSyncId(String syncId) { this.syncId = syncId; save(); } - + public String getSyncToken() { return syncToken; } - - public void setInstanceName(String instanceName) { - this.instanceName = instanceName; - save(); - } - - public String getInstanceName() { - return instanceName; - } - + public void setSyncToken(String syncToken) { this.syncToken = syncToken; save(); @@ -99,7 +89,6 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc debug_mode = Boolean.parseBoolean(formData.getString("debug_mode")); syncId = formData.getString("syncId"); syncToken = formData.getString("syncToken"); - instanceName = formData.getString("instanceName"); save(); reconnectCloudSocket(); diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly index 8500446..80e972c 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly @@ -38,14 +38,11 @@ - - - - +
From 86fc9fe614cd5ee5adfe8c16db56d459127c5740 Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Mon, 11 Dec 2017 12:09:21 +0100 Subject: [PATCH 14/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/1152 - don't change permissions - fix a syntaxt error in pom.xml --- pom.xml | 1 + .../com/ibm/devops/connect/JenkinsServer.java | 80 +++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index 7ab496a..18c7207 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,7 @@ cloud-connect-java 1.0.920563 compile + org.jenkins-ci.plugins cloudbees-folder diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java index 9dac07b..8c6edfd 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsServer.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -25,6 +25,10 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import hudson.model.Item; import hudson.model.TopLevelItem; +import hudson.security.AuthorizationStrategy; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import hudson.security.SecurityRealm; + import java.io.ByteArrayInputStream; import java.util.Collection; import java.util.Iterator; @@ -54,28 +58,60 @@ public class JenkinsServer { private static String FOLDER_SPEC= "Folder created by the IBM Devops plugin"; private static String jobSrc= "\r\n\r\n \r\n false\r\n \r\n \r\n \r\n \r\n * * * * *\r\n false\r\n \r\n \r\n \r\n \r\n \r\n \r\n 2\r\n \r\n \r\n https://github.com/ejodet/discovery-nodejs\r\n \r\n \r\n \r\n \r\n */mastertoto\r\n \r\n \r\n false\r\n \r\n \r\n \r\n Jenkinsfile\r\n true\r\n \r\n \r\n"; - // not used yet - but might be used later public static Collection getJobNames() { - log.info(logPrefix + "getJobNames - get the list of job names"); + log.debug(logPrefix + "getJobNames - get the list of job names"); Collection allJobNames= Jenkins.getInstance().getJobNames(); - log.info(logPrefix + "getJobNames - retrieved " + allJobNames.size() + " JobNames"); + log.debug(logPrefix + "getJobNames - retrieved " + allJobNames.size() + " JobNames"); for (Iterator iterator = allJobNames.iterator(); iterator.hasNext();) { String aJobName = (String) iterator.next(); - log.info(logPrefix + "job: " + aJobName); + log.debug(logPrefix + "job: " + aJobName); } return Jenkins.getInstance().getJobNames(); } public static List getAllItems() { - log.info(logPrefix + "getAllItems - get the list of all items"); - List allProjects= Jenkins.getInstance().getAllItems(AbstractItem.class); - log.info(logPrefix + "getAllItems - Retrieved " + allProjects.size() + " projects"); - return Jenkins.getInstance().getAllItems(); + log.debug(logPrefix + "getAllItems - get the list of all items"); + List allItems= Jenkins.getInstance().getAllItems(); + if (allItems.size() == 0) { // ensure we're able to list all items + AuthorizationStrategy authorizationStrategy= Jenkins.getInstance().getAuthorizationStrategy(); + if (authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { + // allow anoymous read in order to get all items + FullControlOnceLoggedInAuthorizationStrategy strat= (FullControlOnceLoggedInAuthorizationStrategy) authorizationStrategy; + // remember previous settings + boolean isAllowAnonymousRead= strat.isAllowAnonymousRead(); + strat.setAllowAnonymousRead(true); + allItems= Jenkins.getInstance().getAllItems(); + strat.setAllowAnonymousRead(isAllowAnonymousRead); + Jenkins.getInstance().setAuthorizationStrategy(strat); + } + } + log.debug(logPrefix + "getAllItems - Retrieved " + allItems.size() + " projects"); + return allItems; + } + + public static Item getItemByName(String itemName) { + log.info(logPrefix + "Retrieving project " + itemName); + List allProjects= JenkinsServer.getAllItems(); + + for (Item anItem : allProjects) { + String aName = anItem.getFullName(); + log.info(logPrefix + "project " + aName); + if (itemName.equals(aName)) { + log.info(logPrefix + "Project " + itemName + " retrieved!"); + return anItem; + } + } + log.info(logPrefix + "Project " + itemName + " not found!"); + return null; } public static void createJob(JSONObject newJob) { - log.info(logPrefix + "createJob - Creating a new job."); + log.debug(logPrefix + "createJob - Creating a new job."); if(validCreationRequest(newJob)) { + // get current security settings + SecurityRealm securityRealm= Jenkins.getInstance().getSecurityRealm(); + AuthorizationStrategy authorizationStrategy= Jenkins.getInstance().getAuthorizationStrategy(); + // temporarily disable security as we are not allowed to create jobs as anonymous disableSecurity(); try { @@ -88,20 +124,27 @@ public static void createJob(JSONObject newJob) { // verify folder was created Folder targetFolder= getFolder(folderName); if (targetFolder == null) { - log.info(logPrefix + "createJob - target folder not retrieved. Exiting creation process"); + log.debug(logPrefix + "createJob - target folder not retrieved. Exiting creation process"); } else { - log.info(logPrefix + "createJob - target folder retrieved !!!!!"); + log.debug(logPrefix + "createJob - target folder retrieved !!!!!"); // create job in target folder String jobSrc= props.get("source").toString(); String jobName= props.get("jobName").toString(); - createJobInFolder(targetFolder, jobName, jobSrc); + Collection existingJobs= getJobNames(); + if (existingJobs.contains(jobName)) { + // do not create + log.debug(logPrefix + "Job " + jobName + " already exists."); + } else { + createJobInFolder(targetFolder, jobName, jobSrc); + } } } catch (Exception e) { log.error(logPrefix + "An unexepected error occurred while creating job."); e.printStackTrace(); } finally { // be sure to re-enable security - reloadConfiguration(); + Jenkins.getInstance().setSecurityRealm(securityRealm); + Jenkins.getInstance().setAuthorizationStrategy(authorizationStrategy); } } } @@ -149,7 +192,7 @@ private static void createFolder(String folderName) { } catch (Exception e) { // folder might be existing log.debug(logPrefix + folderName + " was not created."); - e.printStackTrace(); + // e.printStackTrace(); } } @@ -169,15 +212,6 @@ private static void disableSecurity() { Jenkins.getInstance().disableSecurity(); } - private static void reloadConfiguration() { - log.debug(logPrefix + "reloadConfiguration()"); - try { - Jenkins.getInstance().reload(); - } catch (Exception e) { - e.printStackTrace(); - } - } - private static Folder getFolder(String folderName) { return (Folder) Jenkins.getInstance().getItem(folderName); } From beeb8ea35cc92e9137a1176e430fac8b59369d82 Mon Sep 17 00:00:00 2001 From: ERIC JODET Date: Tue, 12 Dec 2017 14:52:38 +0100 Subject: [PATCH 15/25] https://github.ibm.com/org-ids/otc-integration-issues/issues/1172 infer the target envionment from webhook URL --- .../ibm/devops/dra/AbstractDevOpsAction.java | 59 +++++++++++-------- src/main/java/com/ibm/devops/dra/Util.java | 22 +++++++ .../devops/notification/MessageHandler.java | 5 ++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java b/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java index febcf33..787dece 100644 --- a/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java +++ b/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java @@ -74,33 +74,42 @@ public abstract class AbstractDevOpsAction extends Recorder { private final static String ORG= "&&organization_guid:"; private final static String SPACE= "&&space_guid:"; - private static Map TARGET_API_MAP = ImmutableMap.of( - "production", "https://api.ng.bluemix.net", - "dev", "https://api.stage1.ng.bluemix.net", - "new", "https://api.stage1.ng.bluemix.net", - "stage1", "https://api.stage1.ng.bluemix.net" - ); - - private static Map ORGANIZATIONS_URL_MAP = ImmutableMap.of( - "production", "https://api.ng.bluemix.net/v2/organizations?q=name:", - "dev", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:", - "new", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:", - "stage1", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:" - ); + // ImmutableMap accepts at most 5 items - https://www.lewuathe.com/guava-immutablemap-limitation.html + private static Map TARGET_API_MAP = ImmutableMap.builder() + .put("production", "https://api.ng.bluemix.net") + .put("dev", "https://api.stage1.ng.bluemix.net") + .put("new", "https://api.stage1.ng.bluemix.net") + .put("stage1", "https://api.stage1.ng.bluemix.net") + .put("eu-de", "https://api.eu-de.bluemix.net") + .put("eu-gb", "https://api.eu-gb.bluemix.net") + .build(); - private static Map SPACES_URL_MAP = ImmutableMap.of( - "production", "https://api.ng.bluemix.net/v2/spaces?q=name:", - "dev", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:", - "new", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:", - "stage1", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:" - ); + private static Map ORGANIZATIONS_URL_MAP = ImmutableMap.builder() + .put("production", "https://api.ng.bluemix.net/v2/organizations?q=name:") + .put("dev", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") + .put("new", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") + .put("stage1", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") + .put("eu-de", "https://api.eu-de.bluemix.net/v2/organizations?q=name:") + .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/organizations?q=name:") + .build(); - private static Map APPS_URL_MAP = ImmutableMap.of( - "production", "https://api.ng.bluemix.net/v2/apps?q=name:", - "dev", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:", - "new", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:", - "stage1", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:" - ); + private static Map SPACES_URL_MAP = ImmutableMap.builder() + .put("production", "https://api.ng.bluemix.net/v2/spaces?q=name:") + .put("dev", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") + .put("new", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") + .put("stage1", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") + .put("eu-de", "https://api.eu-de.bluemix.net/v2/spaces?q=name:") + .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/spaces?q=name:") + .build(); + + private static Map APPS_URL_MAP = ImmutableMap.builder() + .put("production", "https://api.ng.bluemix.net/v2/apps?q=name:") + .put("dev", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") + .put("new", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") + .put("stage1", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") + .put("eu-de", "https://api.eu-de.bluemix.net/v2/apps?q=name:") + .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/apps?q=name:") + .build(); private static Map TOOLCHAINS_URL_MAP = ImmutableMap.of( "production", "https://otc-api.ng.bluemix.net/api/v1/toolchains?organization_guid=", diff --git a/src/main/java/com/ibm/devops/dra/Util.java b/src/main/java/com/ibm/devops/dra/Util.java index 41dac8d..e242781 100644 --- a/src/main/java/com/ibm/devops/dra/Util.java +++ b/src/main/java/com/ibm/devops/dra/Util.java @@ -15,6 +15,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of package com.ibm.devops.dra; +import com.google.common.collect.ImmutableMap; import hudson.EnvVars; import java.io.PrintStream; import java.util.HashMap; @@ -25,6 +26,13 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of */ public class Util { + + private static Map TARGET_API_MAP = ImmutableMap.of( + "devops-api.ng.bluemix.net", "production", + "devops-api.stage1.ng.bluemix.net", "stage1", + "devops-api.eu-de.bluemix.net", "eu-de", + "devops-api.eu-gb.bluemix.net", "eu-gb" + ); /** * check if the str is null or empty * @param str @@ -59,6 +67,20 @@ public static boolean allNotNullOrEmpty(HashMap vars, PrintStrea return true; } + public static String getTargetEnv(String webHookUrl, PrintStream printStream) { + if (!isNullOrEmpty(webHookUrl)) { + String baseUrl = webHookUrl.split("@")[1]; + String environment = baseUrl.split("/")[0]; + if (TARGET_API_MAP.keySet().contains(environment)) { + return TARGET_API_MAP.get(environment); + } else { + printStream.println("[IBM Cloud DevOps] WARNING - environment not found: " + environment); + } + } + // default to production + return TARGET_API_MAP.get("production"); + } + public static boolean validateEnvVariables(EnvVars envVars, PrintStream printStream) { Boolean valid = true; if(envVars != null) { diff --git a/src/main/java/com/ibm/devops/notification/MessageHandler.java b/src/main/java/com/ibm/devops/notification/MessageHandler.java index 95fd130..6a1d2e0 100644 --- a/src/main/java/com/ibm/devops/notification/MessageHandler.java +++ b/src/main/java/com/ibm/devops/notification/MessageHandler.java @@ -147,6 +147,9 @@ public static void postToWebhook(String webhook, boolean deployableMessage, JSON postMethod.addHeader("x-create-connection", "true"); printStream.println("[IBM Cloud DevOps] Sending Deployable Mapping message to webhook:"); printStream.println(message); + } else { + printStream.println("[IBM Cloud DevOps] Sending DLMS message to webhook:"); + printStream.println(message); } CloseableHttpResponse response = httpClient.execute(postMethod); @@ -170,6 +173,8 @@ public static JSONObject buildDeployableMappingMessage(EnvVars envVars, PrintStr try { JSONObject deployableMappingMessage; // API + String webHookUrl= Util.getWebhookUrl(envVars); + environment= Util.getTargetEnv(webHookUrl, printStream); String apiUrl= AbstractDevOpsAction.chooseTargetAPI(environment); // get bluemix token first From 1b54207ee141730ce7af9ff7c305583d70c07a4d Mon Sep 17 00:00:00 2001 From: aberk Date: Mon, 15 Jan 2018 10:08:41 -0500 Subject: [PATCH 16/25] Protect against re-use of sync ID - Added credentials to be passed in - Better failure callbacks to CR --- .../1.0.920563/maven-metadata.xml | 2 +- pom.xml | 8 +- .../connect/CloudBuildStepListener.java | 6 +- .../connect/CloudFlowExecutionListener.java | 4 - .../ibm/devops/connect/CloudItemListener.java | 16 ++- .../ibm/devops/connect/CloudPublisher.java | 130 ++++++++++++++---- .../devops/connect/CloudSocketComponent.java | 80 +++++++---- .../ibm/devops/connect/CloudWorkListener.java | 69 +++++++++- .../connect/ConnectComputerListener.java | 7 +- .../devops/connect/JenkinsIntegrationId.java | 32 +++++ .../ibm/devops/connect/JenkinsJobStatus.java | 27 +++- .../com/ibm/devops/connect/JenkinsServer.java | 32 +++-- .../ibm/devops/connect/OnConnectListener.java | 20 +++ .../SecuredActions/AbstractSecuredAction.java | 77 +++++++++++ .../connect/SecuredActions/BuildJobsList.java | 19 +++ .../connect/SecuredActions/TriggerJob.java | 35 +++++ .../devops/dra/DevOpsGlobalConfiguration.java | 59 +++++++- .../DevOpsGlobalConfiguration/config.jelly | 9 +- 18 files changed, 534 insertions(+), 98 deletions(-) create mode 100644 src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java create mode 100644 src/main/java/com/ibm/devops/connect/OnConnectListener.java create mode 100644 src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java create mode 100644 src/main/java/com/ibm/devops/connect/SecuredActions/BuildJobsList.java create mode 100644 src/main/java/com/ibm/devops/connect/SecuredActions/TriggerJob.java diff --git a/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml index 370fa1c..83a0c8c 100644 --- a/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml +++ b/lib/uc-cloud-connect-java/cloud-connect-java/1.0.920563/maven-metadata.xml @@ -1,7 +1,7 @@ uc-cloud-connect-java - cloud-connect-java/artifactId> + cloud-connect-java 1.0.920563 diff --git a/pom.xml b/pom.xml index 18c7207..dfa189d 100644 --- a/pom.xml +++ b/pom.xml @@ -236,6 +236,12 @@ org.jenkins-ci.plugins cloudbees-folder 6.0.0 - + + + org.acegisecurity + acegi-security + 1.0.7 + provided + \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java index 6126567..b2d3ef3 100644 --- a/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java @@ -43,14 +43,10 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import com.ibm.devops.dra.DevOpsGlobalConfiguration; -import org.jenkinsci.plugins.uniqueid.IdStore; -import hudson.plugins.git.util.BuildData; -import hudson.plugins.git.util.Build; - @Extension public class CloudBuildStepListener extends BuildStepListener { public static final Logger log = LoggerFactory.getLogger(CloudBuildStepListener.class); - + public void finished(AbstractBuild build, BuildStep bs, BuildListener listener, boolean canContinue) { // We listen to jobs that are started by IBM Cloud only if(this.shouldListen(build)) { diff --git a/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java index 4b22555..5e96881 100644 --- a/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java @@ -43,10 +43,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import com.ibm.devops.dra.DevOpsGlobalConfiguration; -import org.jenkinsci.plugins.uniqueid.IdStore; -import hudson.plugins.git.util.BuildData; -import hudson.plugins.git.util.Build; - import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.steps.StepExecution; diff --git a/src/main/java/com/ibm/devops/connect/CloudItemListener.java b/src/main/java/com/ibm/devops/connect/CloudItemListener.java index d9f3e5d..9c9a2a6 100644 --- a/src/main/java/com/ibm/devops/connect/CloudItemListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudItemListener.java @@ -40,7 +40,6 @@ public class CloudItemListener extends ItemListener { public CloudItemListener(){ logPrefix= logPrefix + "CloudItemListener "; log.info(logPrefix + "CloudItemListener started..."); - buildJobsList(); } @Override @@ -59,17 +58,20 @@ public void onUpdated(Item item) { } private void handleEvent(Item item, String phase) { - if( !(item instanceof Folder) ) { - JenkinsJob jenkinsJob= new JenkinsJob(item); - log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); - CloudPublisher cloudPublisher = new CloudPublisher(); - cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + CloudSocketComponent socket = new ConnectComputerListener().getCloudSocketInstance(); + if(socket.connected()) { + if( !(item instanceof Folder) ) { + JenkinsJob jenkinsJob= new JenkinsJob(item); + log.info(ToStringBuilder.reflectionToString(jenkinsJob.toJson()) + " was " + phase); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobInfo(jenkinsJob.toJson()); + } } // we'll handle the updates to the sync app here } - private List buildJobsList() { + public List buildJobsList() { log.info(logPrefix + "Building the list of Jenkins jobs..."); List allProjects= JenkinsServer.getAllItems(); List allJobs = new ArrayList(); diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index 22bd3ff..65cb2ec 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -129,14 +129,8 @@ private boolean postToSyncAPI(String url, String payload) { try { CloseableHttpClient httpClient = HttpClients.createDefault(); - String jenkinsId; - - if (IdStore.getId(Jenkins.getInstance()) != null) { - jenkinsId = IdStore.getId(Jenkins.getInstance()); - } else { - IdStore.makeId(Jenkins.getInstance()); - jenkinsId = IdStore.getId(Jenkins.getInstance()); - } + JenkinsIntegrationId jenkinsIntegrationId = new JenkinsIntegrationId(); + String jenkinsId = jenkinsIntegrationId.getIntegrationId(); HttpPost postMethod = new HttpPost(url); // postMethod = addProxyInformation(postMethod); @@ -144,6 +138,7 @@ private boolean postToSyncAPI(String url, String payload) { postMethod.setHeader("sync_id", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); postMethod.setHeader("instance_type", "JENKINS"); postMethod.setHeader("instance_id", jenkinsId); + postMethod.setHeader("integration_id", jenkinsId); postMethod.setHeader("Content-Type", "application/json"); StringEntity data = new StringEntity(payload); @@ -180,20 +175,14 @@ private boolean postToSyncAPI(String url, String payload) { return false; } - public boolean createIntegrationIfNecessary() { - String localLogPrefix = logPrefix + "createIntegrationIfNecessary "; + public boolean doesIntegrationExist() { + String localLogPrefix = logPrefix + "doesIntegrationExist "; String resStr = ""; try { CloseableHttpClient httpClient = HttpClients.createDefault(); - String jenkinsId; - - if (IdStore.getId(Jenkins.getInstance()) != null) { - jenkinsId = IdStore.getId(Jenkins.getInstance()); - } else { - IdStore.makeId(Jenkins.getInstance()); - jenkinsId = IdStore.getId(Jenkins.getInstance()); - } + JenkinsIntegrationId jenkinsIntegrationId = new JenkinsIntegrationId(); + String jenkinsId = jenkinsIntegrationId.getIntegrationId(); String url = this.getSyncStoreUrl() + INTEGRATION_ENDPOINT_URL.replace("{integration_id}", jenkinsId); @@ -223,10 +212,10 @@ public boolean createIntegrationIfNecessary() { return true; } else { - // if gets error status + log.info(localLogPrefix + "No Integration Retrieved"); - log.info(localLogPrefix + "Attempting to create a new integration"); - return this.createIntegration(jenkinsId); + + return false; } } catch (JsonSyntaxException e) { log.error(localLogPrefix + "Invalid Json response, response: " + resStr); @@ -247,10 +236,13 @@ public boolean createIntegrationIfNecessary() { return false; } - private boolean createIntegration(String jenkinsId) { + public boolean createIntegration() { String localLogPrefix= logPrefix + "createIntegration "; String resStr = ""; + JenkinsIntegrationId jenkinsIntegrationId = new JenkinsIntegrationId(); + String jenkinsId = jenkinsIntegrationId.getIntegrationId(); + try { CloseableHttpClient httpClient = HttpClients.createDefault(); @@ -264,6 +256,7 @@ private boolean createIntegration(String jenkinsId) { newIntegration.put("dateCreated", System.currentTimeMillis()); newIntegration.put("docType", "integration"); newIntegration.put("serverUrl", Jenkins.getInstance().getRootUrl()); + newIntegration.put("type", "JENKINS"); HttpPost postMethod = new HttpPost(url); // postMethod = addProxyInformation(postMethod); @@ -311,13 +304,19 @@ private boolean createIntegration(String jenkinsId) { private boolean updateIntegrationServerUrl(JSONObject payload, String newServerUrl) { String localLogPrefix= logPrefix + "updateIntegrationServerUrl "; + payload.put("serverUrl", newServerUrl); + + return (updateIntegration(payload)); + } + + private boolean updateIntegration(JSONObject payload) { + String localLogPrefix= logPrefix + "updateIntegrationServerUrl "; + try { CloseableHttpClient httpClient = HttpClients.createDefault(); String url = this.getSyncStoreUrl() + INTEGRATIONS_ENDPOINT_URL; - payload.put("serverUrl", newServerUrl); - HttpPost putMethod = new HttpPost(url); // putMethod = addProxyInformation(putMethod); putMethod.setHeader("Content-Type", "application/json"); @@ -358,4 +357,87 @@ private boolean updateIntegrationServerUrl(JSONObject payload, String newServerU return false; } + public boolean doesOtherIntegrationExist() { + String localLogPrefix= logPrefix + "doesOtherIntegrationExist "; + + String resStr = ""; + + try { + CloseableHttpClient httpClient = HttpClients.createDefault(); + + JenkinsIntegrationId jenkinsIntegrationIdGenerator = new JenkinsIntegrationId(); + String jenkinsIntegrationId = jenkinsIntegrationIdGenerator.getIntegrationId(); + + String url = this.getSyncStoreUrl() + INTEGRATIONS_ENDPOINT_URL; + + HttpGet getMethod = new HttpGet(url); + // postMethod = addProxyInformation(postMethod); + getMethod.setHeader("Content-Type", "application/json"); + + String authEncoding = DatatypeConverter.printBase64Binary((Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId() + ":" + Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncToken()).getBytes("UTF-8")); + getMethod.setHeader("Authorization", "Basic " + authEncoding); + + CloseableHttpResponse response = httpClient.execute(getMethod); + + resStr = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().toString().contains("200") || response.getStatusLine().toString().contains("201")) { + + JSONArray jsonBody = JSONArray.fromObject(resStr); + + boolean updatedOldIntegration = false; + boolean integrationFound = false; + + for (int i = 0; i < jsonBody.size(); i++) { + JSONObject integrationObj = jsonBody.getJSONObject(i); + updatedOldIntegration = updateLegacyIntegrationIfNecessary(integrationObj); + if(integrationObj.get("id").equals(jenkinsIntegrationId)) { + integrationFound = true; + } + } + + if(!updatedOldIntegration && jsonBody.size() > 0 && !integrationFound) { + return true; + } + } + } catch (JsonSyntaxException e) { + log.error(localLogPrefix + "Invalid Json response, response: " + resStr); + } catch (IllegalStateException e) { + // will be triggered when 403 Forbidden + try { + log.info(localLogPrefix + "Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); + } catch (UnsupportedEncodingException e1) { + e1.printStackTrace(); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + /* + * currently, the integration ID is the sync ID concatenated with an underscore and the Jenkins Server ID. The original integration ID was + * just the Jenkins Server ID. If we find the Jenkins Server Id, we update that record with the proper Integration Id + */ + + private boolean updateLegacyIntegrationIfNecessary(JSONObject integrationObj) { + String jenkinsId; + if (IdStore.getId(Jenkins.getInstance()) != null) { + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } else { + IdStore.makeId(Jenkins.getInstance()); + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } + + if(integrationObj.get("id").equals(jenkinsId)) { + JenkinsIntegrationId jenkinsIntegrationId = new JenkinsIntegrationId(); + integrationObj.put("id", jenkinsIntegrationId.getIntegrationId()); + return updateIntegration(integrationObj); + } + + return false; + } } diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index a8b8d32..fc231b6 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -21,6 +21,7 @@ import com.ibm.cloud.urbancode.connect.client.ConnectSocket; import com.ibm.cloud.urbancode.connect.client.Listeners; +import com.ibm.devops.connect.OnConnectListener; import com.ibm.devops.connect.CloudPublisher; @@ -34,12 +35,14 @@ public class CloudSocketComponent { final private IWorkListener workListener; final private String cloudUrl; private ConnectSocket socket; - + + private static boolean otherIntegrationExists = false; + public CloudSocketComponent(IWorkListener workListener, String cloudUrl) { this.workListener = workListener; this.cloudUrl = cloudUrl; } - + public boolean isRegistered() { return StringUtils.isNotBlank(getSyncToken()); } @@ -62,31 +65,48 @@ public void connectToCloudServices() throws Exception { } CloudPublisher cloudPublisher = new CloudPublisher(); - cloudPublisher.createIntegrationIfNecessary(); - - URI uri = new URI(cloudUrl); - log.info(logPrefix + "Starting cloud endpoint " + syncId); - socket = ConnectSocket.builder() - .uri(uri) - .id(syncId) - .token(syncToken) - .onConnect(Listeners.chain(Listeners.INFO_LOGGING, Listeners.EMIT_GET_WORK)) - .onDisconnect(Listeners.INFO_LOGGING) - .onWorkAvailable(Listeners.chain(Listeners.DEBUG_LOGGING, Listeners.EMIT_GET_WORK)) - .onWork(workListener) -// .onWork(Listeners.chain(Listeners.INFO_LOGGING, workListener)) - .onError(Listeners.ERROR_LOGGING) - .build(); - socket.on(Socket.EVENT_CONNECT_ERROR, Listeners.ERROR_LOGGING); - socket.on(Socket.EVENT_CONNECT_TIMEOUT, Listeners.ERROR_LOGGING); - socket.on(Socket.EVENT_RECONNECT_ERROR, Listeners.ERROR_LOGGING); - socket.on(Socket.EVENT_RECONNECT_FAILED, Listeners.ERROR_LOGGING); - socket.on(Socket.EVENT_RECONNECT_ATTEMPT, Listeners.INFO_LOGGING); - // do not listen for Socket.EVENT_RECONNECT, we will make 2 get work requests - - socket.connect(); + + boolean shouldConnect = true; + // Does integration exist + if(!cloudPublisher.doesIntegrationExist()) { + // Does another integration exist + if(cloudPublisher.doesOtherIntegrationExist()) { + log.warn(logPrefix + "These credentials have been used by another Jenkins Instance. Please generate another Sync Id and provide those credentials here."); + shouldConnect = false; + otherIntegrationExists = true; + } else { + // Create Integration + cloudPublisher.createIntegration(); + } + } else { + otherIntegrationExists = false; + } + + if(shouldConnect) { + URI uri = new URI(cloudUrl); + log.info(logPrefix + "Starting cloud endpoint " + syncId); + socket = ConnectSocket.builder() + .uri(uri) + .id(syncId) + .token(syncToken) + .onConnect(Listeners.chain(Listeners.chain(Listeners.INFO_LOGGING, Listeners.EMIT_GET_WORK), OnConnectListener.BUILD_JOBS_LIST)) + .onDisconnect(Listeners.INFO_LOGGING) + .onWorkAvailable(Listeners.chain(Listeners.DEBUG_LOGGING, Listeners.EMIT_GET_WORK)) + .onWork(workListener) + // .onWork(Listeners.chain(Listeners.INFO_LOGGING, workListener)) + .onError(Listeners.ERROR_LOGGING) + .build(); + socket.on(Socket.EVENT_CONNECT_ERROR, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_CONNECT_TIMEOUT, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_ERROR, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_FAILED, Listeners.ERROR_LOGGING); + socket.on(Socket.EVENT_RECONNECT_ATTEMPT, Listeners.INFO_LOGGING); + // do not listen for Socket.EVENT_RECONNECT, we will make 2 get work requests + + socket.connect(); + } } - + // this does get called, but you may not see logging in the console. it will appear in the file. public void disconnect() { if (socket != null) { @@ -109,4 +129,12 @@ public boolean connected() { } return socket.connected(); } + + public String getCauseOfFailure() { + if (otherIntegrationExists) { + return "These credentials have been used by another Jenkins Instance. Please generate another Sync Id and provide those credentials here."; + } + + return null; + } } diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index a99dc2d..dfc94b1 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -53,6 +53,12 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import net.sf.json.JSONObject; + +import com.ibm.devops.connect.CloudCause.JobStatus; +import com.ibm.devops.connect.SecuredAction.TriggerJob.TriggerJobParamObj; +import com.ibm.devops.connect.SecuredAction.TriggerJob; + //////TEMP import hudson.ExtensionList; @@ -82,6 +88,13 @@ public enum WorkStatus { */ @Override public void call(ConnectSocket socket, String event, Object... args) { + TriggerJob triggerJob = new TriggerJob(); + + TriggerJobParamObj paramObj = triggerJob.new TriggerJobParamObj(socket, event, args); + triggerJob.runAsJenkinsUser(paramObj); + } + + public void callSecured(ConnectSocket socket, String event, Object... args) { log.info(logPrefix + " Received event from Connect Socket"); JSONArray incomingJobs = JSONArray.fromObject(args[0].toString()); @@ -94,15 +107,28 @@ public void call(ConnectSocket socket, String event, Object... args) { // delegating job creation to the Jenkins server JenkinsServer.createJob(incomingJob); } - + + if (incomingJob.has("fullName")) { String fullName = incomingJob.get("fullName").toString(); Jenkins myJenkins = Jenkins.getInstance(); + // Get item by name Item item = myJenkins.getItem(fullName); + + log.info("Item Found (1): " + item); + + // If item is not retrieved, get by full name if(item == null) { item = myJenkins.getItemByFullName(fullName); + log.info("Item Found (2): " + item); + } + + // If item is not retrieved, get by full name with escaped characters + if(item == null) { + item = myJenkins.getItemByFullName(escapeItemName(fullName)); + log.info("Item Found (3): " + item); } List parametersList = generateParamList(incomingJob, getParameterTypeMap(item)); @@ -112,18 +138,39 @@ public void call(ConnectSocket socket, String event, Object... args) { returnProps = incomingJob.getJSONObject("returnProps"); } + CloudCause cloudCause = new CloudCause(socket, incomingJob.get("id").toString(), returnProps); + Queue.Item queuedItem = null; + String errorMessage = null; + if(item instanceof AbstractProject) { AbstractProject abstractProject = (AbstractProject)item; - ParameterizedJobMixIn.scheduleBuild2(abstractProject, 0, new ParametersAction(parametersList), new CauseAction(new CloudCause(socket, incomingJob.get("id").toString(), returnProps))); + queuedItem = ParameterizedJobMixIn.scheduleBuild2(abstractProject, 0, new ParametersAction(parametersList), new CauseAction(cloudCause)); + + if (queuedItem == null) { + errorMessage = "Could not start parameterized build."; + } } else if (item instanceof WorkflowJob) { WorkflowJob workflowJob = (WorkflowJob)item; - workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(new CloudCause(socket, incomingJob.get("id").toString(), returnProps) )); + workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(cloudCause)); + + if (queuedItem == null) { + errorMessage = "Could not start pipeline build."; + } } else if (item == null) { - log.warn("No Item Found"); + errorMessage = "No Item Found"; + log.warn(errorMessage); } else { - log.warn("Unhandled job type found: " + item.getClass()); + errorMessage = "Unhandled job type found: " + item.getClass(); + log.warn(errorMessage); + } + + if( errorMessage != null ) { + JenkinsJobStatus erroredJobStatus = new JenkinsJobStatus(null, cloudCause, null, true, true); + JSONObject statusUpdate = erroredJobStatus.generateErrorStatus(errorMessage); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); } } @@ -162,7 +209,12 @@ private List generateParamList (JSONObject incomingJob, Map getParameterTypeMap(Item item) { return result; } + + private String escapeItemName(String itemName) { + String result = item.replace("\'", "'"); + return result + } } diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index 986933c..fafdf8b 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -15,6 +15,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.ibm.devops.connect.CloudItemListener; + @Extension public class ConnectComputerListener extends ComputerListener { public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); @@ -39,11 +41,6 @@ public void onOnline(Computer c) { try { log.info(logPrefix + "Connecting to Cloud Services..."); getCloudSocketInstance().connectToCloudServices(); - if(getCloudSocketInstance().connected()) { - log.info(logPrefix + "Connected to Cloud Services!"); - } else { - log.warn(logPrefix + "Failed to connected to Cloud Services"); - } } catch (Exception e) { log.error(logPrefix + "Exception caught while connecting to Cloud Services: " + e); } diff --git a/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java b/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java new file mode 100644 index 0000000..ba99094 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java @@ -0,0 +1,32 @@ +package com.ibm.devops.connect; + +import org.jenkinsci.plugins.uniqueid.IdStore; +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import jenkins.model.Jenkins; + +public class JenkinsIntegrationId { + public JenkinsIntegrationId () { + + } + + public String getIntegrationId() { + String result = getSyncId() + "_" + getJenkinsId(); + return result; + } + + private String getJenkinsId() { + String jenkinsId; + if (IdStore.getId(Jenkins.getInstance()) != null) { + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } else { + IdStore.makeId(Jenkins.getInstance()); + jenkinsId = IdStore.getId(Jenkins.getInstance()); + } + + return jenkinsId; + } + + private String getSyncId() { + return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java index 2a319a2..6f78c6b 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java @@ -61,7 +61,7 @@ public JenkinsJobStatus(AbstractBuild build, CloudCause cloudCause, BuildStep bu public JSONObject generate() { JSONObject result = new JSONObject(); - + evaluateSourceData(build, cloudCause); if(!(buildStep instanceof hudson.model.ParametersDefinitionProperty)) { @@ -85,7 +85,7 @@ public JSONObject generate() { // TODO: Premature success is causing successful results when job actually fails // System.out.println("\t\tRESULT \t IS BUILDING \t hasntStartedYet \t isCompleteBuild"); // System.out.println("\t\t" + build.getResult() + "\t\t" + build.isBuilding() + "\t\t" + build.hasntStartedYet() + "\t\t" + (build.getResult() == null ? "IT was NULL" : build.getResult().isCompleteBuild())); - + if (build.getResult() == null) { if(build.isBuilding()) { result.put("status", JobStatus.started.toString()); @@ -114,6 +114,29 @@ public JSONObject generate() { return result; } + public JSONObject generateErrorStatus(String errorMessage) { + JSONObject result = new JSONObject(); + + cloudCause.addStep("Error: " + errorMessage, JobStatus.failure.toString(), "Failed due to error", true); + + result.put("status", JobStatus.failure.toString()); + result.put("timestamp", System.currentTimeMillis()); + result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + result.put("steps", cloudCause.getStepsArray()); + result.put("returnProps", cloudCause.getReturnProps()); + + if(build != null) { + result.put("url", Jenkins.getInstance().getRootUrl() + build.getUrl()); + result.put("jobExternalId", getJobUniqueIdFromBuild(build)); + result.put("name", build.getDisplayName()); + } else { + result.put("url", Jenkins.getInstance().getRootUrl()); + result.put("name", "Job Error"); + } + + return result; + } + private String getJobUniqueIdFromBuild(AbstractBuild build) { AbstractProject project = (AbstractProject)build.getProject(); diff --git a/src/main/java/com/ibm/devops/connect/JenkinsServer.java b/src/main/java/com/ibm/devops/connect/JenkinsServer.java index 8c6edfd..163f94e 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsServer.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsServer.java @@ -33,6 +33,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.ArrayList; import jenkins.model.Jenkins; @@ -50,25 +51,26 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of public class JenkinsServer { public static final Logger log = LoggerFactory.getLogger(JenkinsServer.class); private static String logPrefix= "[IBM Cloud DevOps] JenkinsServer#"; - + // creds private static String BLX_CREDS= "IBM_CLOUD_DEVOPS_CREDS_API"; private static String BLX_CREDS_DESC= "IBM DevOps Bluemix credentials"; // folder and job private static String FOLDER_SPEC= "Folder created by the IBM Devops plugin"; private static String jobSrc= "\r\n\r\n \r\n false\r\n \r\n \r\n \r\n \r\n * * * * *\r\n false\r\n \r\n \r\n \r\n \r\n \r\n \r\n 2\r\n \r\n \r\n https://github.com/ejodet/discovery-nodejs\r\n \r\n \r\n \r\n \r\n */mastertoto\r\n \r\n \r\n false\r\n \r\n \r\n \r\n Jenkinsfile\r\n true\r\n \r\n \r\n"; - + public static Collection getJobNames() { log.debug(logPrefix + "getJobNames - get the list of job names"); Collection allJobNames= Jenkins.getInstance().getJobNames(); log.debug(logPrefix + "getJobNames - retrieved " + allJobNames.size() + " JobNames"); for (Iterator iterator = allJobNames.iterator(); iterator.hasNext();) { - String aJobName = (String) iterator.next(); + + String aJobName = (String) iterator.next(); log.debug(logPrefix + "job: " + aJobName); } return Jenkins.getInstance().getJobNames(); } - + public static List getAllItems() { log.debug(logPrefix + "getAllItems - get the list of all items"); List allItems= Jenkins.getInstance().getAllItems(); @@ -88,11 +90,11 @@ public static List getAllItems() { log.debug(logPrefix + "getAllItems - Retrieved " + allItems.size() + " projects"); return allItems; } - + public static Item getItemByName(String itemName) { log.info(logPrefix + "Retrieving project " + itemName); List allProjects= JenkinsServer.getAllItems(); - + for (Item anItem : allProjects) { String aName = anItem.getFullName(); log.info(logPrefix + "project " + aName); @@ -104,14 +106,14 @@ public static Item getItemByName(String itemName) { log.info(logPrefix + "Project " + itemName + " not found!"); return null; } - + public static void createJob(JSONObject newJob) { log.debug(logPrefix + "createJob - Creating a new job."); if(validCreationRequest(newJob)) { // get current security settings SecurityRealm securityRealm= Jenkins.getInstance().getSecurityRealm(); AuthorizationStrategy authorizationStrategy= Jenkins.getInstance().getAuthorizationStrategy(); - + // temporarily disable security as we are not allowed to create jobs as anonymous disableSecurity(); try { @@ -146,9 +148,9 @@ public static void createJob(JSONObject newJob) { Jenkins.getInstance().setSecurityRealm(securityRealm); Jenkins.getInstance().setAuthorizationStrategy(authorizationStrategy); } - } + } } - + private static void createCredentials(JSONObject newJob) { if(newJob.has("props")) { JSONObject props = newJob.getJSONObject("props"); @@ -168,7 +170,7 @@ private static void createCredentials(JSONObject newJob) { } } } - + private static boolean validCreationRequest(JSONObject newJob) { log.debug(logPrefix + "validCreationRequest - Validating creation payload."); boolean valid= false; @@ -183,7 +185,7 @@ private static boolean validCreationRequest(JSONObject newJob) { } return valid; } - + private static void createFolder(String folderName) { log.debug(logPrefix + "createFolder - Creating folder " + folderName); try { @@ -195,7 +197,7 @@ private static void createFolder(String folderName) { // e.printStackTrace(); } } - + private static void createJobInFolder(Folder targetFolder, String jobName, String source) { log.debug(logPrefix + "createItem - Creating job " + jobName); try { @@ -206,12 +208,12 @@ private static void createJobInFolder(Folder targetFolder, String jobName, Strin e.printStackTrace(); } } - + private static void disableSecurity() { log.debug(logPrefix + "disableSecurity()"); Jenkins.getInstance().disableSecurity(); } - + private static Folder getFolder(String folderName) { return (Folder) Jenkins.getInstance().getItem(folderName); } diff --git a/src/main/java/com/ibm/devops/connect/OnConnectListener.java b/src/main/java/com/ibm/devops/connect/OnConnectListener.java new file mode 100644 index 0000000..129227b --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/OnConnectListener.java @@ -0,0 +1,20 @@ +package com.ibm.devops.connect; + +import com.ibm.cloud.urbancode.connect.client.ConnectSocket; +import com.ibm.cloud.urbancode.connect.client.Listener; +import com.ibm.devops.connect.CloudItemListener; +import com.ibm.devops.connect.SecuredAction.BuildJobsList; + +public class OnConnectListener { + static final public Listener BUILD_JOBS_LIST = new Listener() { + @Override + public void call(ConnectSocket socket, String event, Object... args) { + System.out.println("/n/n/nHEY....................................\n\n"); + // CloudItemListener cil = new CloudItemListener(); + // cil.buildJobsList(); + + BuildJobsList buildJobList = new BuildJobsList(); + buildJobList.runAsJenkinsUser(null); + } + }; +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java b/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java new file mode 100644 index 0000000..f4010b0 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java @@ -0,0 +1,77 @@ +package com.ibm.devops.connect.SecuredAction; + +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import hudson.security.SecurityRealm; +import org.acegisecurity.userdetails.UserDetails; +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; + +public abstract class AbstractSecuredAction { + protected abstract void run(ParamObj paramObj); + + public class ParamObj { + + } + + public void runAsJenkinsUser(ParamObj paramObj) { + + StandardUsernamePasswordCredentials providedCredentials = Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getCredentialsObj(); + + Authentication originalAuth = null; + + if(providedCredentials != null) { + originalAuth = Jenkins.getInstance().getAuthentication(); + Authentication authenticatedAuth = authenticateCredentials(providedCredentials); + SecurityContextHolder.getContext().setAuthentication(authenticatedAuth); + } + + try{ + run(paramObj); + } finally { + if (originalAuth != null) { + SecurityContextHolder.getContext().setAuthentication(originalAuth); + } + } + + } + + private Authentication authenticateCredentials(StandardUsernamePasswordCredentials providedCredentials) { + SecurityRealm realm = Jenkins.getInstance().getSecurityRealm(); + SecurityRealm.SecurityComponents securityComponents = realm.createSecurityComponents(); + + Authentication auth = getAuth(providedCredentials, realm); + + Authentication result = null; + if(auth != null) { + try { + result = securityComponents.manager.authenticate(auth); + } catch (AuthenticationException e) { + if ( e instanceof BadCredentialsException ) { + System.out.println("Wrong username or password"); + } else { + System.out.println("Something else went wrong"); + } + } + } + + return result; + } + + private Authentication getAuth(StandardUsernamePasswordCredentials providedCredentials, SecurityRealm realm) { + UserDetails userDetails = realm.loadUserByUsername(providedCredentials.getUsername()); + + userDetails.getAuthorities(); + + Authentication auth = new UsernamePasswordAuthenticationToken (providedCredentials.getUsername(), providedCredentials.getPassword(), userDetails.getAuthorities()); + + return auth; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/SecuredActions/BuildJobsList.java b/src/main/java/com/ibm/devops/connect/SecuredActions/BuildJobsList.java new file mode 100644 index 0000000..92a6fc8 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/SecuredActions/BuildJobsList.java @@ -0,0 +1,19 @@ +package com.ibm.devops.connect.SecuredAction; + +import com.ibm.devops.connect.CloudItemListener; +import jenkins.model.Jenkins; +import hudson.model.AbstractItem; + +import java.util.List; + +public class BuildJobsList extends AbstractSecuredAction { + + protected void run(AbstractSecuredAction.ParamObj paramObj) { + System.out.println("Running Operation As Another USER!!!!!!"); + + List allProjects= Jenkins.getInstance().getAllItems(AbstractItem.class); + + CloudItemListener cil = new CloudItemListener(); + cil.buildJobsList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/SecuredActions/TriggerJob.java b/src/main/java/com/ibm/devops/connect/SecuredActions/TriggerJob.java new file mode 100644 index 0000000..c286f9c --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/SecuredActions/TriggerJob.java @@ -0,0 +1,35 @@ +package com.ibm.devops.connect.SecuredAction; + +import com.ibm.devops.connect.CloudWorkListener; +import jenkins.model.Jenkins; +import hudson.model.AbstractItem; + + +import com.ibm.cloud.urbancode.connect.client.ConnectSocket; + +import java.util.List; + +public class TriggerJob extends AbstractSecuredAction { + + protected void run(AbstractSecuredAction.ParamObj paramObj) { + System.out.println("Running Operation As Another USER!!!!!!"); + + TriggerJobParamObj triggerJobParamObj = (TriggerJobParamObj)paramObj; + + CloudWorkListener cwl = new CloudWorkListener(); + cwl.callSecured(triggerJobParamObj.socket, triggerJobParamObj.event, triggerJobParamObj.args); + } + + public class TriggerJobParamObj extends AbstractSecuredAction.ParamObj { + + public ConnectSocket socket; + public String event; + public Object[] args; + + public TriggerJobParamObj(ConnectSocket socket, String event, Object... args) { + this.socket = socket; + this.event = event; + this.args = args; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java index 26b31f6..e034623 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java @@ -14,6 +14,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of package com.ibm.devops.dra; +import java.util.List; + import hudson.CopyOnWrite; import hudson.Extension; import hudson.model.Computer; @@ -23,10 +25,20 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.AncestorInPath; import hudson.util.FormFieldValidator; import com.ibm.devops.connect.CloudSocketComponent; import com.ibm.devops.connect.ConnectComputerListener; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; +import hudson.security.ACL; +import jenkins.model.Jenkins; + import java.io.IOException; import javax.servlet.ServletException; /** @@ -40,6 +52,7 @@ public class DevOpsGlobalConfiguration extends GlobalConfiguration { private volatile boolean debug_mode; private volatile String syncId; private volatile String syncToken; + private String credentialsId; public DevOpsGlobalConfiguration() { load(); @@ -81,6 +94,15 @@ public void setSyncToken(String syncToken) { save(); } + public String getCredentialsId() { + return credentialsId; + } + + public void setCredentialsId(String crendentialsId) { + this.credentialsId = credentialsId; + save(); + } + @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { // To persist global configuration information, @@ -89,6 +111,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc debug_mode = Boolean.parseBoolean(formData.getString("debug_mode")); syncId = formData.getString("syncId"); syncToken = formData.getString("syncToken"); + credentialsId = formData.getString("credentialsId"); save(); reconnectCloudSocket(); @@ -111,12 +134,46 @@ protected void check() throws IOException, ServletException { if(socket.connected()) { ok("Success - Connected to IBM Cloud Service"); } else { - error("Not connected to IBM Cloud Services - Please ensure that current values are applied"); + String cause = socket.getCauseOfFailure(); + if(cause != null) { + error("Not connected to IBM Cloud Services - " + cause); + } else { + error("Not connected to IBM Cloud Services - Please ensure that the current values are applied"); + } } } }.process(); } + /** + * This method is called to populate the credentials list on the Jenkins config page. + */ + public ListBoxModel doFillCredentialsIdItems(@QueryParameter("target") final String target) { + StandardListBoxModel result = new StandardListBoxModel(); + result.includeEmptyValue(); + result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), + CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM, + URIRequirementBuilder.fromUri(target).build() + ) + ); + return result; + } + + public StandardUsernamePasswordCredentials getCredentialsObj() { + List standardCredentials = CredentialsProvider.lookupCredentials( + StandardUsernamePasswordCredentials.class, + Jenkins.getInstance(), + ACL.SYSTEM); + + StandardUsernamePasswordCredentials credentials = + CredentialsMatchers.firstOrNull(standardCredentials, CredentialsMatchers.withId(this.credentialsId)); + + return credentials; + } + private void reconnectCloudSocket() { ConnectComputerListener connectComputerListener = new ConnectComputerListener(); diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly index 80e972c..d19e4e6 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly @@ -13,7 +13,7 @@ --> - + On Running"); - System.out.println("===========>>>>>"); - for(FlowNode flowNode : execution.getCurrentHeads()){ - System.out.println(flowNode); - } } @Override public void onResumed(FlowExecution execution) { - System.out.println("222222222222------------> On Resumed"); + } @Override public void onCompleted(FlowExecution execution) { - System.out.println("333333333333------------> On Completed"); - } + } } \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudGraphListener.java b/src/main/java/com/ibm/devops/connect/CloudGraphListener.java new file mode 100644 index 0000000..a15ea95 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/CloudGraphListener.java @@ -0,0 +1,101 @@ +/* + + + Copyright 2016, 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import java.util.Map; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.*; +import hudson.model.BuildStepListener; +import hudson.tasks.BuildStep; +import hudson.model.AbstractBuild; +import hudson.tasks.Builder; +import hudson.model.Result; + +import jenkins.model.Jenkins; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +import com.ibm.devops.connect.CloudCause.JobStatus; + +import com.ibm.devops.dra.DevOpsGlobalConfiguration; + +import org.jenkinsci.plugins.workflow.flow.GraphListener; +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.support.actions.PauseAction; + +import java.io.IOException; + +@Extension +public class CloudGraphListener implements GraphListener { + public static final Logger log = LoggerFactory.getLogger(CloudGraphListener.class); + + public void onNewHead(FlowNode node) { + FlowExecution execution = node.getExecution(); + + WorkflowRun workflowRun = null; + + try { + if (execution.getOwner().getExecutable() instanceof WorkflowRun) { + workflowRun = (WorkflowRun)(execution.getOwner().getExecutable()); + } + } catch (IOException e) { + log.error("HIT THE IOEXCEPTION: " + e); + return; + } + + CloudCause cloudCause = getCloudCause(workflowRun); + if (cloudCause == null) { + cloudCause = new CloudCause(); + } + + boolean isStartNode = node.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"); + boolean isEndNode = node.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"); + boolean isPauseNode = PauseAction.isPaused(node); + + if(isStartNode || isEndNode || isPauseNode) { + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, node, isStartNode, isPauseNode); + JSONObject statusUpdate = status.generate(); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); + } + } + + private CloudCause getCloudCause(WorkflowRun workflowRun) { + List causes = workflowRun.getCauses(); + + for(Cause cause : causes) { + if (cause instanceof CloudCause ) { + return (CloudCause)cause; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudRunListener.java b/src/main/java/com/ibm/devops/connect/CloudRunListener.java index 0fe2a00..0fcbe05 100644 --- a/src/main/java/com/ibm/devops/connect/CloudRunListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudRunListener.java @@ -17,22 +17,59 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import hudson.Extension; import hudson.model.listeners.RunListener; import hudson.model.TaskListener; +import hudson.model.Cause; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.sf.json.JSONObject; +import net.sf.json.JSONArray; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; + +import com.ibm.devops.connect.CloudCause.JobStatus; + @Extension public class CloudRunListener extends RunListener { public static final Logger log = LoggerFactory.getLogger(CloudRunListener.class); @Override - public void onStarted(WorkflowRun workflow, TaskListener listener) { + public void onStarted(WorkflowRun workflowRun, TaskListener listener) { // http://javadoc.jenkins.io/plugin/workflow-job/org/jenkinsci/plugins/workflow/job/WorkflowRun.html + CloudCause cloudCause = getCloudCause(workflowRun); + if (cloudCause == null) { + cloudCause = new CloudCause(); + } + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, true, false); + JSONObject statusUpdate = status.generate(); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); } @Override - public void onCompleted(WorkflowRun workflow, TaskListener listener) { - // TODO - should match + public void onCompleted(WorkflowRun workflowRun, TaskListener listener) { + CloudCause cloudCause = getCloudCause(workflowRun); + if (cloudCause == null) { + cloudCause = new CloudCause(); + } + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, false, false); + JSONObject statusUpdate = status.generate(); + CloudPublisher cloudPublisher = new CloudPublisher(); + cloudPublisher.uploadJobStatus(statusUpdate); + } + + private CloudCause getCloudCause(WorkflowRun workflowRun) { + List causes = workflowRun.getCauses(); + + for(Cause cause : causes) { + if (cause instanceof CloudCause ) { + return (CloudCause)cause; + } + } + + return null; } } diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index af2191d..6559e39 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -153,9 +153,9 @@ public void callSecured(ConnectSocket socket, String event, Object... args) { } else if (item instanceof WorkflowJob) { WorkflowJob workflowJob = (WorkflowJob)item; - workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(cloudCause)); + QueueTaskFuture queuedTask = workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(cloudCause)); - if (queuedItem == null) { + if (queuedTask == null) { errorMessage = "Could not start pipeline build."; } } else if (item == null) { diff --git a/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java new file mode 100644 index 0000000..e785683 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java @@ -0,0 +1,202 @@ +/* + + + Copyright 2017 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect; + +import hudson.model.*; +import hudson.model.Item; +import hudson.tasks.BuildStep; + +import net.sf.json.JSONObject; + +import org.apache.commons.lang.builder.ToStringBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jenkinsci.plugins.uniqueid.IdStore; + +import jenkins.model.Jenkins; + +import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.CloudCause.JobStatus; + +import org.jenkinsci.plugins.uniqueid.IdStore; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; + +import com.ibm.devops.dra.EvaluateGate; +import com.ibm.devops.dra.GatePublisherAction; + +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +import java.util.Map; +import java.util.List; + +/** + * Jenkins server + */ + +public class JenkinsPipelineStatus { + + private WorkflowRun workflowRun; + private CloudCause cloudCause; + private FlowNode node; + private Boolean newStep; + private Boolean isPaused; + + public JenkinsPipelineStatus(WorkflowRun workflowRun, CloudCause cloudCause, FlowNode node, boolean newStep, boolean isPaused) { + this.workflowRun = workflowRun; + this.cloudCause = cloudCause; + this.node = node; + this.newStep = newStep; + this.isPaused = isPaused; + } + + public JSONObject generate() { + JSONObject result = new JSONObject(); + + evaluateSourceData(workflowRun, cloudCause); + evaluateDRAData(); + + if(newStep && node == null) { + cloudCause.addStep("Starting Jenkins Pipeline", JobStatus.success.toString(), "Successfully started pipeline...", false); + } else if(newStep && node != null) { + cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Started stage", false); + } else if (isPaused && node != null) { + cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Please acknowledge the Jenkins Pipeline input", false); + } else if(!newStep && node != null) { + + if(node.getError() == null) { + cloudCause.updateLastStep(null, JobStatus.success.toString(), "Stage is successful", false); + } else { + cloudCause.updateLastStep(null, JobStatus.failure.toString(), node.getError().getDisplayName(), false); + } + } + + if (workflowRun.getResult() == null) { + if(workflowRun.isBuilding()) { + result.put("status", JobStatus.started.toString()); + } else { + result.put("status", JobStatus.unstarted.toString()); + } + } else { + if(workflowRun.getResult() == Result.SUCCESS) { + result.put("status", JobStatus.success.toString()); + } else { + result.put("status", JobStatus.failure.toString()); + } + } + + result.put("timestamp", System.currentTimeMillis()); + result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + result.put("name", workflowRun.getDisplayName()); + result.put("steps", cloudCause.getStepsArray()); + result.put("url", Jenkins.getInstance().getRootUrl() + workflowRun.getUrl()); + result.put("returnProps", cloudCause.getReturnProps()); + result.put("isPipeline", true); + result.put("isPaused", isPaused); + + //TODO + //result.put("jobExternalId", getJobUniqueIdFromBuild(build)); + + // AbstractProject project = (AbstractProject)build.getProject(); + // String jobName = project.getName(); + // result.put("jobName", jobName); + + result.put("sourceData", cloudCause.getSourceDataJson()); + result.put("draData", cloudCause.getDRADataJson()); + + return result; + } + + public JSONObject generateErrorStatus(String errorMessage) { + + return null; + } + + private String getJobUniqueIdFromBuild(AbstractBuild build) { + + return null; + } + + private void evaluateSourceData(WorkflowRun workflowRun, CloudCause cause) { + List actions = workflowRun.getActions(); + + for(Action action : actions) { + // If using Hudson Git Plugin + if (action instanceof BuildData) { + Map branchMap = ((BuildData)action).getBuildsByBranchName(); + + for(String branchName : branchMap.keySet()) { + Build gitBuild = branchMap.get(branchName); + + if (gitBuild.getBuildNumber() == workflowRun.getNumber()) { + SourceData sourceData = new SourceData(branchName, gitBuild.getSHA1().getName(), "GIT"); + cause.setSourceData(sourceData); + } + } + } + } + } + + private void evaluateDRAData() { + DRAData data = cloudCause.getDRAData(); + + List actions = workflowRun.getActions(); + if(data == null) { + for(Action action : actions) { + if (action instanceof CrDraAction) { + CrDraAction cda = (CrDraAction)action; + data = cda.getDRAData(); + cloudCause.setDRAData(data); + } + } + } + + if(data == null) { + // CAN NOT GET THIS DATA FROM PIPELINE + // data.setApplicationName(applicationName); + // data.setOrgName(orgName); + // data.setToolchainName(toolchainName); + // data.setEnvironment(environment); + + for(Action action : actions) { + if (action instanceof GatePublisherAction) { + data = new DRAData(); + GatePublisherAction gpa = (GatePublisherAction)action; + + String gateText = gpa.getText(); + String riskDashboardLink = gpa.getRiskDashboardLink(); + String decision = gpa.getDecision(); + String policy = gpa.getPolicyName(); + + data.setGateText(gateText); + data.setDecision(decision); + data.setRiskDahboardLink(riskDashboardLink); + data.setPolicy(policy); + data.setBuildNumber(Integer.toString(workflowRun.getNumber())); + + CrDraAction cda = new CrDraAction(data); + workflowRun.addAction(cda); + + cloudCause.setDRAData(data); + } + } + + } + } +} \ No newline at end of file From 54293a391ef2e3f09f34392f59d0b0a4e44b7435 Mon Sep 17 00:00:00 2001 From: aberk Date: Tue, 6 Feb 2018 15:26:54 -0500 Subject: [PATCH 22/25] Include job name on pipeline status info --- .../devops/connect/JenkinsPipelineStatus.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java index e785683..505bad8 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java @@ -41,6 +41,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.graph.FlowNode; import java.util.Map; @@ -110,12 +111,9 @@ public JSONObject generate() { result.put("isPipeline", true); result.put("isPaused", isPaused); - //TODO - //result.put("jobExternalId", getJobUniqueIdFromBuild(build)); - - // AbstractProject project = (AbstractProject)build.getProject(); - // String jobName = project.getName(); - // result.put("jobName", jobName); + WorkflowJob workflowJob = (WorkflowJob)(workflowRun.getParent()); + result.put("jobName", workflowJob.getName()); + result.put("jobExternalId", getJobUniqueIdFromBuild(workflowJob)); result.put("sourceData", cloudCause.getSourceDataJson()); result.put("draData", cloudCause.getDRADataJson()); @@ -128,9 +126,17 @@ public JSONObject generateErrorStatus(String errorMessage) { return null; } - private String getJobUniqueIdFromBuild(AbstractBuild build) { + private String getJobUniqueIdFromBuild(WorkflowJob job) { + String jobId; - return null; + if (IdStore.getId(job) != null) { + jobId = IdStore.getId(job); + } else { + IdStore.makeId(job); + jobId = IdStore.getId(job); + } + + return jobId; } private void evaluateSourceData(WorkflowRun workflowRun, CloudCause cause) { From ce3866a11a0b04fa18624c1b5fcb88a9348776aa Mon Sep 17 00:00:00 2001 From: aberkeb1 Date: Tue, 27 Feb 2018 13:54:10 -0500 Subject: [PATCH 23/25] Refactored to New Plugin - Added more execution properties being passed - Added Git Commit Message --- screenshots/PostBuild-WebHookNotification.png | Bin 11779 -> 0 bytes screenshots/Upload-Test-Result.png | Bin 182199 -> 0 bytes .../ContinuousReleaseProperties.java | 166 +++ .../connect/CloudBuildStepListener.java | 7 +- .../com/ibm/devops/connect/CloudCause.java | 24 +- .../connect/CloudFlowExecutionListener.java | 2 - .../devops/connect/CloudGraphListener.java | 8 +- .../ibm/devops/connect/CloudPublisher.java | 7 +- .../ibm/devops/connect/CloudRunListener.java | 5 +- .../devops/connect/CloudSocketComponent.java | 2 +- .../ibm/devops/connect/CloudWorkListener.java | 38 +- .../connect/ConnectComputerListener.java | 2 + .../com/ibm/devops/connect/CrDraAction.java | 31 - .../DevOpsGlobalConfiguration.java | 24 +- .../{ => Endpoints}/EndpointManager.java | 2 +- .../connect/{ => Endpoints}/EndpointsYP.java | 2 +- .../connect/{ => Endpoints}/EndpointsYS1.java | 2 +- .../connect/{ => Endpoints}/IEndpoints.java | 2 +- .../devops/connect/JenkinsIntegrationId.java | 2 +- .../com/ibm/devops/connect/JenkinsJob.java | 1 - .../ibm/devops/connect/JenkinsJobStatus.java | 244 ----- .../devops/connect/JenkinsPipelineStatus.java | 208 ---- .../Proxy.java => connect/Proxy.javaBOGUS} | 0 .../SecuredActions/AbstractSecuredAction.java | 2 +- .../com/ibm/devops/connect/SourceData.java | 53 - .../connect/Status/AbstractJenkinsStatus.java | 266 +++++ .../ibm/devops/connect/Status/CrAction.java | 63 ++ .../devops/connect/{ => Status}/DRAData.java | 2 +- .../connect/Status/JenkinsJobStatus.java | 87 ++ .../connect/Status/JenkinsPipelineStatus.java | 104 ++ .../ibm/devops/connect/Status/SourceData.java | 95 ++ .../ibm/devops/dra/AbstractDevOpsAction.java | 985 ------------------ .../ibm/devops/dra/AbstractGateAction.java | 45 - .../com/ibm/devops/dra/BuildInfoModel.java | 77 -- .../ibm/devops/dra/BuildPublisherAction.java | 45 - .../ibm/devops/dra/DeploymentInfoModel.java | 51 - .../com/ibm/devops/dra/EnvironmentScope.java | 89 -- .../java/com/ibm/devops/dra/EvaluateGate.java | 599 ----------- .../ibm/devops/dra/GatePublisherAction.java | 73 -- .../java/com/ibm/devops/dra/PublishBuild.java | 502 --------- .../com/ibm/devops/dra/PublishDeploy.java | 517 --------- .../java/com/ibm/devops/dra/PublishSQ.java | 617 ----------- .../java/com/ibm/devops/dra/PublishTest.java | 961 ----------------- .../com/ibm/devops/dra/TestResultModel.java | 162 --- src/main/java/com/ibm/devops/dra/Util.java | 192 ---- .../devops/dra/steps/AbstractDevOpsStep.java | 51 - .../devops/dra/steps/EvaluateGateStep.java | 87 -- .../dra/steps/EvaluateGateStepExecution.java | 90 -- .../devops/dra/steps/PublishBuildStep.java | 85 -- .../dra/steps/PublishBuildStepExecution.java | 92 -- .../devops/dra/steps/PublishDeployStep.java | 79 -- .../dra/steps/PublishDeployStepExecution.java | 91 -- .../ibm/devops/dra/steps/PublishSQStep.java | 104 -- .../dra/steps/PublishSQStepExecution.java | 85 -- .../ibm/devops/dra/steps/PublishTestStep.java | 84 -- .../dra/steps/PublishTestStepExecution.java | 90 -- .../devops/notification/BuildListener.java | 84 -- .../ibm/devops/notification/EventHandler.java | 103 -- .../devops/notification/MessageHandler.java | 222 ---- .../ibm/devops/notification/MessageUtil.java | 69 -- .../ibm/devops/notification/OTCNotifier.java | 104 -- ...eployableMappingNotificationExecution.java | 66 -- .../DeployableMappingNotificationStep.java | 65 -- .../steps/OTCNotificationExecution.java | 66 -- .../steps/OTCNotificationStep.java | 71 -- .../DevOpsGlobalConfiguration/config.jelly | 15 +- .../help-consoleUrl.html | 0 .../help-instanceName.html | 0 .../help-syncId.html | 0 .../help-syncToken.html | 0 .../dra/BuildPublisherAction/summary.jelly | 26 - .../ibm/devops/dra/EvaluateGate/config.jelly | 64 -- .../help-additionalBuildInfo.html | 17 - .../EvaluateGate/help-applicationName.html | 17 - .../dra/EvaluateGate/help-buildJobName.html | 17 - .../dra/EvaluateGate/help-buildNumber.html | 17 - .../dra/EvaluateGate/help-credentialsId.html | 17 - .../devops/dra/EvaluateGate/help-envName.html | 17 - .../devops/dra/EvaluateGate/help-orgName.html | 17 - .../dra/EvaluateGate/help-policyName.html | 17 - .../dra/EvaluateGate/help-toolchainName.html | 17 - .../dra/EvaluateGate/help-willDisrupt.html | 17 - .../dra/GatePublisherAction/summary.jelly | 30 - .../ibm/devops/dra/PublishBuild/config.jelly | 46 - .../help-additionalBuildInfo.html | 17 - .../PublishBuild/help-applicationName.html | 17 - .../dra/PublishBuild/help-buildNumber.html | 17 - .../dra/PublishBuild/help-credentialsId.html | 17 - .../devops/dra/PublishBuild/help-orgName.html | 17 - .../dra/PublishBuild/help-toolchainName.html | 17 - .../ibm/devops/dra/PublishDeploy/config.jelly | 58 -- .../help-additionalBuildInfo.html | 17 - .../PublishDeploy/help-applicationName.html | 17 - .../PublishDeploy/help-applicationUrl.html | 17 - .../dra/PublishDeploy/help-buildJobName.html | 17 - .../dra/PublishDeploy/help-buildNumber.html | 17 - .../dra/PublishDeploy/help-credentialsId.html | 17 - .../PublishDeploy/help-environmentName.html | 17 - .../dra/PublishDeploy/help-orgName.html | 17 - .../dra/PublishDeploy/help-toolchainName.html | 17 - .../com/ibm/devops/dra/PublishSQ/config.jelly | 62 -- .../dra/PublishSQ/help-SQAuthToken.html | 17 - .../devops/dra/PublishSQ/help-SQHostName.html | 17 - .../dra/PublishSQ/help-SQProjectKey.html | 17 - .../PublishSQ/help-additionalBuildInfo.html | 17 - .../dra/PublishSQ/help-applicationName.html | 17 - .../dra/PublishSQ/help-buildJobName.html | 17 - .../dra/PublishSQ/help-buildNumber.html | 17 - .../dra/PublishSQ/help-credentialsId.html | 17 - .../devops/dra/PublishSQ/help-orgName.html | 17 - .../dra/PublishSQ/help-toolchainName.html | 17 - .../ibm/devops/dra/PublishTest/config.jelly | 91 -- .../PublishTest/help-additionalBuildInfo.html | 17 - .../PublishTest/help-additionalContents.html | 20 - .../help-additionalLifecycleStage.html | 22 - .../PublishTest/help-additionalUpload.html | 17 - .../dra/PublishTest/help-applicationName.html | 18 - .../dra/PublishTest/help-buildJobName.html | 17 - .../dra/PublishTest/help-buildNumber.html | 17 - .../devops/dra/PublishTest/help-contents.html | 20 - .../dra/PublishTest/help-credentialsId.html | 17 - .../devops/dra/PublishTest/help-envName.html | 17 - .../dra/PublishTest/help-environment.html | 17 - .../dra/PublishTest/help-lifecycleStage.html | 22 - .../devops/dra/PublishTest/help-orgName.html | 17 - .../dra/PublishTest/help-policyName.html | 17 - .../dra/PublishTest/help-toolchainName.html | 17 - .../notification/OTCNotifier/config.jelly | 39 - .../devops/notification/OTCNotifier/help.html | 21 - 129 files changed, 849 insertions(+), 8624 deletions(-) delete mode 100644 screenshots/PostBuild-WebHookNotification.png delete mode 100644 screenshots/Upload-Test-Result.png create mode 100644 src/main/java/com/ibm/devops/connect/CRPipeline/ContinuousReleaseProperties.java delete mode 100644 src/main/java/com/ibm/devops/connect/CrDraAction.java rename src/main/java/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration.java (90%) rename src/main/java/com/ibm/devops/connect/{ => Endpoints}/EndpointManager.java (94%) rename src/main/java/com/ibm/devops/connect/{ => Endpoints}/EndpointsYP.java (93%) rename src/main/java/com/ibm/devops/connect/{ => Endpoints}/EndpointsYS1.java (93%) rename src/main/java/com/ibm/devops/connect/{ => Endpoints}/IEndpoints.java (79%) delete mode 100644 src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java delete mode 100644 src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java rename src/main/java/com/ibm/devops/{notification/Proxy.java => connect/Proxy.javaBOGUS} (100%) delete mode 100644 src/main/java/com/ibm/devops/connect/SourceData.java create mode 100644 src/main/java/com/ibm/devops/connect/Status/AbstractJenkinsStatus.java create mode 100644 src/main/java/com/ibm/devops/connect/Status/CrAction.java rename src/main/java/com/ibm/devops/connect/{ => Status}/DRAData.java (97%) create mode 100644 src/main/java/com/ibm/devops/connect/Status/JenkinsJobStatus.java create mode 100644 src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java create mode 100644 src/main/java/com/ibm/devops/connect/Status/SourceData.java delete mode 100644 src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java delete mode 100644 src/main/java/com/ibm/devops/dra/AbstractGateAction.java delete mode 100644 src/main/java/com/ibm/devops/dra/BuildInfoModel.java delete mode 100644 src/main/java/com/ibm/devops/dra/BuildPublisherAction.java delete mode 100644 src/main/java/com/ibm/devops/dra/DeploymentInfoModel.java delete mode 100644 src/main/java/com/ibm/devops/dra/EnvironmentScope.java delete mode 100644 src/main/java/com/ibm/devops/dra/EvaluateGate.java delete mode 100644 src/main/java/com/ibm/devops/dra/GatePublisherAction.java delete mode 100644 src/main/java/com/ibm/devops/dra/PublishBuild.java delete mode 100644 src/main/java/com/ibm/devops/dra/PublishDeploy.java delete mode 100644 src/main/java/com/ibm/devops/dra/PublishSQ.java delete mode 100644 src/main/java/com/ibm/devops/dra/PublishTest.java delete mode 100644 src/main/java/com/ibm/devops/dra/TestResultModel.java delete mode 100644 src/main/java/com/ibm/devops/dra/Util.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/AbstractDevOpsStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/EvaluateGateStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/EvaluateGateStepExecution.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishBuildStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishBuildStepExecution.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishDeployStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishDeployStepExecution.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishSQStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishSQStepExecution.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishTestStep.java delete mode 100644 src/main/java/com/ibm/devops/dra/steps/PublishTestStepExecution.java delete mode 100644 src/main/java/com/ibm/devops/notification/BuildListener.java delete mode 100644 src/main/java/com/ibm/devops/notification/EventHandler.java delete mode 100644 src/main/java/com/ibm/devops/notification/MessageHandler.java delete mode 100644 src/main/java/com/ibm/devops/notification/MessageUtil.java delete mode 100644 src/main/java/com/ibm/devops/notification/OTCNotifier.java delete mode 100644 src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationExecution.java delete mode 100644 src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationStep.java delete mode 100644 src/main/java/com/ibm/devops/notification/steps/OTCNotificationExecution.java delete mode 100644 src/main/java/com/ibm/devops/notification/steps/OTCNotificationStep.java rename src/main/resources/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration/config.jelly (84%) rename src/main/resources/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration/help-consoleUrl.html (100%) rename src/main/resources/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration/help-instanceName.html (100%) rename src/main/resources/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration/help-syncId.html (100%) rename src/main/resources/com/ibm/devops/{dra => connect}/DevOpsGlobalConfiguration/help-syncToken.html (100%) delete mode 100755 src/main/resources/com/ibm/devops/dra/BuildPublisherAction/summary.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-additionalBuildInfo.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-applicationName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildJobName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildNumber.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-credentialsId.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-envName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-orgName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-policyName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-toolchainName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/EvaluateGate/help-willDisrupt.html delete mode 100755 src/main/resources/com/ibm/devops/dra/GatePublisherAction/summary.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-additionalBuildInfo.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-applicationName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-buildNumber.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-credentialsId.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-orgName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishBuild/help-toolchainName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-additionalBuildInfo.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationUrl.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildJobName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildNumber.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-credentialsId.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-environmentName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-orgName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishDeploy/help-toolchainName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQAuthToken.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQHostName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQProjectKey.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-additionalBuildInfo.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-applicationName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildJobName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildNumber.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-credentialsId.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-orgName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishSQ/help-toolchainName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalBuildInfo.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalContents.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalLifecycleStage.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalUpload.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-applicationName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-buildJobName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-buildNumber.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-contents.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-credentialsId.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-envName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-environment.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-lifecycleStage.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-orgName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-policyName.html delete mode 100644 src/main/resources/com/ibm/devops/dra/PublishTest/help-toolchainName.html delete mode 100644 src/main/resources/com/ibm/devops/notification/OTCNotifier/config.jelly delete mode 100644 src/main/resources/com/ibm/devops/notification/OTCNotifier/help.html diff --git a/screenshots/PostBuild-WebHookNotification.png b/screenshots/PostBuild-WebHookNotification.png deleted file mode 100644 index ff9eeff6ff08488685fe4f414101a1f5d2c28206..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11779 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 qF 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 properties; + + /** + * Constructor used for data-binding fields from the corresponding + * config.jelly + * + * @param siteName + * The profile name of the UrbanDeploy site + * @param component + * The object holding the Create Version Block structure + * @param deploy + * The object holding the Deploy Block structure + */ + @DataBoundConstructor + public ContinuousReleaseProperties( + Map properties) { + this.properties = properties; + } + + /* + * Accessors and mutators required for data-binding access + */ + + public Map getProperties() { + return this.properties; + } + + /** + * {@inheritDoc} + * + * @param build + * @param launcher + * @param listener + * @return A boolean to represent if the build can continue + * @throws InterruptedException + * @throws java.io.IOException + * {@inheritDoc} + * @see hudson.tasks.BuildStep#perform(hudson.model.Build, hudson.Launcher, + * hudson.model.TaskListener) + */ + @Override + public void perform(final Run build, FilePath workspace, Launcher launcher, final TaskListener listener) + throws AbortException, InterruptedException, IOException { + CrAction action = build.getAction(CrAction.class); + + if(action == null) { + action = new CrAction(); + build.addAction(action); + } + + if (properties != null) { + action.updateCrProperties(properties); + } + } + + /** + * This class holds the metadata for the Publisher and allows it's data + * fields to persist + * + */ + @Extension + public static class ContinuousReleasePropertiesDescriptor extends BuildStepDescriptor { + + public ContinuousReleasePropertiesDescriptor() { + load(); + } + + /** + * Return the location of the help document for this builder. + *

+ * {@inheritDoc} + * + * @return {@inheritDoc} + * @see hudson.model.Descriptor#getHelpFile() + */ + @Override + public String getHelpFile() { + return "/plugin/ibm-ucdeploy-build-steps/publish.html"; + } + + /** + * Bind data fields to user defined values {@inheritDoc} + * + * @param req + * {@inheritDoc} + * @param formData + * {@inheritDoc} + * @return {@inheritDoc} + * @see hudson.model.Descriptor#configure(org.kohsuke.stapler.StaplerRequest) + */ + @Override + public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { + req.bindJSON(this, formData); + save(); + return super.configure(req, formData); + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Pass Properties to Continuous Release Version"; + } + + /** + * {@inheritDoc} + * + * @param jobType + * {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean isApplicable(Class jobType) { + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java index 16a2f50..fb51e25 100644 --- a/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudBuildStepListener.java @@ -40,8 +40,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import net.sf.json.JSONArray; import com.ibm.devops.connect.CloudCause.JobStatus; - -import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.Status.JenkinsJobStatus; @Extension public class CloudBuildStepListener extends BuildStepListener { @@ -53,7 +52,7 @@ public void finished(AbstractBuild build, BuildStep bs, BuildListener listener, if (cloudCause == null) { cloudCause = new CloudCause(); } - JenkinsJobStatus status = new JenkinsJobStatus(build, cloudCause, bs, false, !canContinue); + JenkinsJobStatus status = new JenkinsJobStatus(build, cloudCause, bs, listener, false, !canContinue); JSONObject statusUpdate = status.generate(); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); @@ -62,7 +61,7 @@ public void finished(AbstractBuild build, BuildStep bs, BuildListener listener, public void started(AbstractBuild build, BuildStep bs, BuildListener listener) { // We listen to jobs that are started by IBM Cloud only if(this.shouldListen(build)) { - JenkinsJobStatus status = new JenkinsJobStatus(build, getCloudCause(build), bs, true, false); + JenkinsJobStatus status = new JenkinsJobStatus(build, getCloudCause(build), bs, listener, true, false); JSONObject statusUpdate = status.generate(); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); diff --git a/src/main/java/com/ibm/devops/connect/CloudCause.java b/src/main/java/com/ibm/devops/connect/CloudCause.java index f15626f..7952d94 100644 --- a/src/main/java/com/ibm/devops/connect/CloudCause.java +++ b/src/main/java/com/ibm/devops/connect/CloudCause.java @@ -8,6 +8,8 @@ import java.util.ArrayList; import net.sf.json.JSONObject; import net.sf.json.JSONArray; +import com.ibm.devops.connect.Status.DRAData; +import com.ibm.devops.connect.Status.SourceData; /** * This is the cause object that is attached to a build if it is started by the IBM Cloud. @@ -86,17 +88,17 @@ public JSONObject getDRADataJson() { } public void updateLastStep(String name, String status, String message, boolean isFatal) { - JSONObject obj = steps.get(steps.size() - 1); - if(name != null) { - obj.put("name", name); + if (steps.size() == 0) { + addStep(name, status, message, isFatal); + } else { + JSONObject obj = steps.get(steps.size() - 1); + if(name != null) { + obj.put("name", name); + } + obj.put("status", status); + obj.put("message", message); + obj.put("isFatal", isFatal); } - obj.put("status", status); - obj.put("message", message); - obj.put("isFatal", isFatal); - } - - public void addSourceData(String branch, String revision, String scmName, Set remoteUrls) { - } public Boolean isCreatedByCR() { @@ -109,7 +111,7 @@ public JSONObject getReturnProps() { public JSONArray getStepsArray() { JSONArray result = new JSONArray(); - for(JSONObject obj : steps) { + for (JSONObject obj : steps) { result.add(obj); } diff --git a/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java index fb4fd4f..000d7fa 100644 --- a/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudFlowExecutionListener.java @@ -41,8 +41,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import com.ibm.devops.connect.CloudCause.JobStatus; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; - import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.steps.StepExecution; diff --git a/src/main/java/com/ibm/devops/connect/CloudGraphListener.java b/src/main/java/com/ibm/devops/connect/CloudGraphListener.java index a15ea95..536f57e 100644 --- a/src/main/java/com/ibm/devops/connect/CloudGraphListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudGraphListener.java @@ -41,8 +41,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import com.ibm.devops.connect.CloudCause.JobStatus; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; - import org.jenkinsci.plugins.workflow.flow.GraphListener; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -50,6 +48,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.support.actions.PauseAction; +import com.ibm.devops.connect.Status.JenkinsPipelineStatus; + import java.io.IOException; @Extension @@ -76,11 +76,11 @@ public void onNewHead(FlowNode node) { } boolean isStartNode = node.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"); - boolean isEndNode = node.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"); + boolean isEndNode = node.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode"); boolean isPauseNode = PauseAction.isPaused(node); if(isStartNode || isEndNode || isPauseNode) { - JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, node, isStartNode, isPauseNode); + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, node, null, isStartNode, isPauseNode); JSONObject statusUpdate = status.generate(); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); diff --git a/src/main/java/com/ibm/devops/connect/CloudPublisher.java b/src/main/java/com/ibm/devops/connect/CloudPublisher.java index 65cb2ec..3e4cbe1 100644 --- a/src/main/java/com/ibm/devops/connect/CloudPublisher.java +++ b/src/main/java/com/ibm/devops/connect/CloudPublisher.java @@ -17,9 +17,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.ibm.devops.dra.AbstractDevOpsAction; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; -import com.ibm.devops.dra.PublishDeploy.PublishDeployImpl; +import com.ibm.devops.connect.DevOpsGlobalConfiguration; import net.sf.json.JSONObject; import net.sf.json.JSONArray; @@ -49,6 +47,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import org.apache.commons.codec.binary.Base64; +import com.ibm.devops.connect.Endpoints.EndpointManager; + import org.jenkinsci.plugins.uniqueid.IdStore; import hudson.tasks.BuildStepDescriptor; @@ -117,7 +117,6 @@ public boolean uploadJobInfo(JSONObject jobJson) { public boolean uploadJobStatus(JSONObject jobStatus) { String url = this.getSyncApiUrl() + JENKINS_JOB_STATUS_ENDPOINT_URL; - return postToSyncAPI(url, jobStatus.toString()); } diff --git a/src/main/java/com/ibm/devops/connect/CloudRunListener.java b/src/main/java/com/ibm/devops/connect/CloudRunListener.java index 0fcbe05..f5b7ee5 100644 --- a/src/main/java/com/ibm/devops/connect/CloudRunListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudRunListener.java @@ -31,6 +31,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import java.util.HashSet; import com.ibm.devops.connect.CloudCause.JobStatus; +import com.ibm.devops.connect.Status.JenkinsPipelineStatus; @Extension public class CloudRunListener extends RunListener { @@ -43,7 +44,7 @@ public void onStarted(WorkflowRun workflowRun, TaskListener listener) { if (cloudCause == null) { cloudCause = new CloudCause(); } - JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, true, false); + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, listener, true, false); JSONObject statusUpdate = status.generate(); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); @@ -55,7 +56,7 @@ public void onCompleted(WorkflowRun workflowRun, TaskListener listener) { if (cloudCause == null) { cloudCause = new CloudCause(); } - JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, false, false); + JenkinsPipelineStatus status = new JenkinsPipelineStatus(workflowRun, cloudCause, null, listener, false, false); JSONObject statusUpdate = status.generate(); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); diff --git a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java index fc231b6..ac381cb 100644 --- a/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java +++ b/src/main/java/com/ibm/devops/connect/CloudSocketComponent.java @@ -17,7 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.DevOpsGlobalConfiguration; import com.ibm.cloud.urbancode.connect.client.ConnectSocket; import com.ibm.cloud.urbancode.connect.client.Listeners; diff --git a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java index 6559e39..1025d78 100644 --- a/src/main/java/com/ibm/devops/connect/CloudWorkListener.java +++ b/src/main/java/com/ibm/devops/connect/CloudWorkListener.java @@ -27,6 +27,7 @@ import jenkins.model.Jenkins; import jenkins.model.ParameterizedJobMixIn; import hudson.model.AbstractProject; +import hudson.model.AbstractItem; import hudson.model.Action; import hudson.model.ParametersAction; import hudson.model.CauseAction; @@ -40,6 +41,7 @@ import hudson.model.Item; import hudson.model.ParameterDefinition; import hudson.model.ParametersDefinitionProperty; +import hudson.model.JobProperty; import java.util.ArrayList; import java.util.Iterator; @@ -59,11 +61,7 @@ import com.ibm.devops.connect.SecuredAction.TriggerJob.TriggerJobParamObj; import com.ibm.devops.connect.SecuredAction.TriggerJob; -//////TEMP - -import hudson.ExtensionList; -import hudson.ExtensionPoint; -import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener; +import com.ibm.devops.connect.Status.JenkinsJobStatus; /* * When Spring is applying the @Transactional annotation, it creates a proxy class which wraps your class. @@ -131,6 +129,7 @@ public void callSecured(ConnectSocket socket, String event, Object... args) { log.info("Item Found (3): " + item); } + System.out.println("HEEEEEEEYYYYYYY------------------------->>>>"); List parametersList = generateParamList(incomingJob, getParameterTypeMap(item)); JSONObject returnProps = new JSONObject(); @@ -153,6 +152,9 @@ public void callSecured(ConnectSocket socket, String event, Object... args) { } else if (item instanceof WorkflowJob) { WorkflowJob workflowJob = (WorkflowJob)item; + System.out.println("\n\n\t\t\t&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&"); + System.out.println(parametersList); + QueueTaskFuture queuedTask = workflowJob.scheduleBuild2(0, new ParametersAction(parametersList), new CauseAction(cloudCause)); if (queuedTask == null) { @@ -167,7 +169,7 @@ public void callSecured(ConnectSocket socket, String event, Object... args) { } if( errorMessage != null ) { - JenkinsJobStatus erroredJobStatus = new JenkinsJobStatus(null, cloudCause, null, true, true); + JenkinsJobStatus erroredJobStatus = new JenkinsJobStatus(null, cloudCause, null, null, true, true); JSONObject statusUpdate = erroredJobStatus.generateErrorStatus(errorMessage); CloudPublisher cloudPublisher = new CloudPublisher(); cloudPublisher.uploadJobStatus(statusUpdate); @@ -196,6 +198,11 @@ private void sendResult(ConnectSocket socket, String id, WorkStatus status, Stri private List generateParamList (JSONObject incomingJob, Map typeMap) { ArrayList result = new ArrayList(); + System.out.println("000000000000000000000000000000000"); + System.out.println(incomingJob.toString()); + System.out.println(typeMap.toString()); + System.out.println("000000000000000000000000000000000"); + if(incomingJob.has("props")) { JSONObject props = incomingJob.getJSONObject("props"); Iterator keys = props.keys(); @@ -206,6 +213,10 @@ private List generateParamList (JSONObject incomingJob, Map\t\t" + key); + System.out.println("->\t\t" + value); + System.out.println("->\t\t" + type); + if(type == null) { } else if(type.equalsIgnoreCase("BooleanParameterDefinition")) { @@ -231,8 +242,19 @@ private List generateParamList (JSONObject incomingJob, Map getParameterTypeMap(Item item) { Map result = new HashMap(); - if(item instanceof AbstractProject) { - List actions = ((AbstractProject)item).getActions(); + if(item instanceof WorkflowJob) { + List> properties = ((WorkflowJob)item).getAllProperties(); + + for(JobProperty property : properties) { + if (property instanceof ParametersDefinitionProperty) { + List paraDefs = ((ParametersDefinitionProperty)property).getParameterDefinitions(); + for (ParameterDefinition paramDef : paraDefs) { + result.put(paramDef.getName(), paramDef.getType()); + } + } + } + } else if(item instanceof AbstractItem) { + List actions = ((AbstractItem)item).getActions(); for(Action action : actions) { if (action instanceof ParametersDefinitionProperty) { diff --git a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java index fafdf8b..899c0e5 100644 --- a/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java +++ b/src/main/java/com/ibm/devops/connect/ConnectComputerListener.java @@ -17,6 +17,8 @@ import com.ibm.devops.connect.CloudItemListener; +import com.ibm.devops.connect.Endpoints.EndpointManager; + @Extension public class ConnectComputerListener extends ComputerListener { public static final Logger log = LoggerFactory.getLogger(ConnectComputerListener.class); diff --git a/src/main/java/com/ibm/devops/connect/CrDraAction.java b/src/main/java/com/ibm/devops/connect/CrDraAction.java deleted file mode 100644 index 3a7cb30..0000000 --- a/src/main/java/com/ibm/devops/connect/CrDraAction.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.ibm.devops.connect; - -import hudson.model.Action; - -public class CrDraAction implements Action { - - private DRAData draData; - - public CrDraAction(DRAData data) { - this.draData = data; - } - - public DRAData getDRAData() { - return draData; - } - - @Override - public String getIconFileName() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public String getUrlName() { - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java b/src/main/java/com/ibm/devops/connect/DevOpsGlobalConfiguration.java similarity index 90% rename from src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java rename to src/main/java/com/ibm/devops/connect/DevOpsGlobalConfiguration.java index e034623..58ced02 100644 --- a/src/main/java/com/ibm/devops/dra/DevOpsGlobalConfiguration.java +++ b/src/main/java/com/ibm/devops/connect/DevOpsGlobalConfiguration.java @@ -12,7 +12,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of */ -package com.ibm.devops.dra; +package com.ibm.devops.connect; import java.util.List; @@ -48,8 +48,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of public class DevOpsGlobalConfiguration extends GlobalConfiguration { @CopyOnWrite - private volatile String consoleUrl; - private volatile boolean debug_mode; private volatile String syncId; private volatile String syncToken; private String credentialsId; @@ -58,24 +56,6 @@ public DevOpsGlobalConfiguration() { load(); } - public String getConsoleUrl() { - return consoleUrl; - } - - public boolean isDebug_mode() { - return debug_mode; - } - - public void setDebug_mode(boolean debug_mode) { - this.debug_mode = debug_mode; - save(); - } - - public void setConsoleUrl(String consoleUrl) { - this.consoleUrl = consoleUrl; - save(); - } - public String getSyncId() { return syncId; } @@ -107,8 +87,6 @@ public void setCredentialsId(String crendentialsId) { public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { // To persist global configuration information, // set that to properties and call save(). - consoleUrl = formData.getString("consoleUrl"); - debug_mode = Boolean.parseBoolean(formData.getString("debug_mode")); syncId = formData.getString("syncId"); syncToken = formData.getString("syncToken"); credentialsId = formData.getString("credentialsId"); diff --git a/src/main/java/com/ibm/devops/connect/EndpointManager.java b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointManager.java similarity index 94% rename from src/main/java/com/ibm/devops/connect/EndpointManager.java rename to src/main/java/com/ibm/devops/connect/Endpoints/EndpointManager.java index 68fea50..d63d797 100644 --- a/src/main/java/com/ibm/devops/connect/EndpointManager.java +++ b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointManager.java @@ -1,4 +1,4 @@ -package com.ibm.devops.connect; +package com.ibm.devops.connect.Endpoints; public class EndpointManager { diff --git a/src/main/java/com/ibm/devops/connect/EndpointsYP.java b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYP.java similarity index 93% rename from src/main/java/com/ibm/devops/connect/EndpointsYP.java rename to src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYP.java index 116e906..41e457e 100644 --- a/src/main/java/com/ibm/devops/connect/EndpointsYP.java +++ b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYP.java @@ -1,4 +1,4 @@ -package com.ibm.devops.connect; +package com.ibm.devops.connect.Endpoints; public class EndpointsYP implements IEndpoints { private static final String SYNC_API_ENPOINT = "https://ucreporting-sync-api.mybluemix.net/"; diff --git a/src/main/java/com/ibm/devops/connect/EndpointsYS1.java b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYS1.java similarity index 93% rename from src/main/java/com/ibm/devops/connect/EndpointsYS1.java rename to src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYS1.java index 5389869..9a7ca2a 100644 --- a/src/main/java/com/ibm/devops/connect/EndpointsYS1.java +++ b/src/main/java/com/ibm/devops/connect/Endpoints/EndpointsYS1.java @@ -1,4 +1,4 @@ -package com.ibm.devops.connect; +package com.ibm.devops.connect.Endpoints; public class EndpointsYS1 implements IEndpoints { private static final String SYNC_API_ENPOINT = "https://ucreporting-sync-api-stage1.stage1.mybluemix.net/"; diff --git a/src/main/java/com/ibm/devops/connect/IEndpoints.java b/src/main/java/com/ibm/devops/connect/Endpoints/IEndpoints.java similarity index 79% rename from src/main/java/com/ibm/devops/connect/IEndpoints.java rename to src/main/java/com/ibm/devops/connect/Endpoints/IEndpoints.java index c93a09f..4b50ebf 100644 --- a/src/main/java/com/ibm/devops/connect/IEndpoints.java +++ b/src/main/java/com/ibm/devops/connect/Endpoints/IEndpoints.java @@ -1,4 +1,4 @@ -package com.ibm.devops.connect; +package com.ibm.devops.connect.Endpoints; public interface IEndpoints { diff --git a/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java b/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java index ba99094..84b8d2e 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsIntegrationId.java @@ -1,7 +1,7 @@ package com.ibm.devops.connect; import org.jenkinsci.plugins.uniqueid.IdStore; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.DevOpsGlobalConfiguration; import jenkins.model.Jenkins; public class JenkinsIntegrationId { diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJob.java b/src/main/java/com/ibm/devops/connect/JenkinsJob.java index b8756ca..bd79cc2 100644 --- a/src/main/java/com/ibm/devops/connect/JenkinsJob.java +++ b/src/main/java/com/ibm/devops/connect/JenkinsJob.java @@ -37,7 +37,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of import java.util.List; import jenkins.model.Jenkins; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; import org.jenkinsci.plugins.workflow.job.WorkflowJob; /** diff --git a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java deleted file mode 100644 index 14211a3..0000000 --- a/src/main/java/com/ibm/devops/connect/JenkinsJobStatus.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.connect; - -import hudson.model.*; -import hudson.model.Item; -import hudson.tasks.BuildStep; - -import net.sf.json.JSONObject; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.jenkinsci.plugins.uniqueid.IdStore; - -import jenkins.model.Jenkins; - -import com.ibm.devops.dra.DevOpsGlobalConfiguration; -import com.ibm.devops.connect.CloudCause.JobStatus; - -import org.jenkinsci.plugins.uniqueid.IdStore; -import hudson.plugins.git.util.BuildData; -import hudson.plugins.git.util.Build; - -import com.ibm.devops.dra.EvaluateGate; -import com.ibm.devops.dra.GatePublisherAction; - -import java.util.Map; -import java.util.List; - -/** - * Jenkins server - */ - -public class JenkinsJobStatus { - - private AbstractBuild build; - private CloudCause cloudCause; - private BuildStep buildStep; - private Boolean newStep; - private Boolean isFatal; - - public JenkinsJobStatus(AbstractBuild build, CloudCause cloudCause, BuildStep buildStep, Boolean newStep, Boolean isFatal) { - this.build = build; - this.cloudCause = cloudCause; - this.buildStep = buildStep; - this.newStep = newStep; - this.isFatal = isFatal; - } - - public JSONObject generate() { - JSONObject result = new JSONObject(); - - evaluateSourceData(build, cloudCause); - evaluateDRAData(); - - if(!(buildStep instanceof hudson.model.ParametersDefinitionProperty)) { - if (newStep) { - cloudCause.addStep(((Describable)buildStep).getDescriptor().getDisplayName(), JobStatus.started.toString(), "Started a build step", false); - } else { - String newStatus; - String message; - if (!isFatal) { - newStatus = JobStatus.success.toString(); - message = "The build step finished and the job will continue."; - } else { - newStatus = JobStatus.failure.toString(); - message = "The build step failed and the job can not continue."; - } - - if (cloudCause.isCreatedByCR()) { - cloudCause.updateLastStep(((Describable)buildStep).getDescriptor().getDisplayName(), newStatus, message, isFatal); - } - } - } - - // TODO: Premature success is causing successful results when job actually fails - // System.out.println("\t\tRESULT \t IS BUILDING \t hasntStartedYet \t isCompleteBuild"); - // System.out.println("\t\t" + build.getResult() + "\t\t" + build.isBuilding() + "\t\t" + build.hasntStartedYet() + "\t\t" + (build.getResult() == null ? "IT was NULL" : build.getResult().isCompleteBuild())); - - if (build.getResult() == null) { - if(build.isBuilding()) { - result.put("status", JobStatus.started.toString()); - } else { - result.put("status", JobStatus.unstarted.toString()); - } - } else { - if(build.getResult() == Result.SUCCESS) { - result.put("status", JobStatus.success.toString()); - } else { - result.put("status", JobStatus.failure.toString()); - } - } - - result.put("timestamp", System.currentTimeMillis()); - result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); - result.put("name", build.getDisplayName()); - result.put("steps", cloudCause.getStepsArray()); - result.put("url", Jenkins.getInstance().getRootUrl() + build.getUrl()); - result.put("returnProps", cloudCause.getReturnProps()); - - result.put("jobExternalId", getJobUniqueIdFromBuild(build)); - - AbstractProject project = (AbstractProject)build.getProject(); - String jobName = project.getName(); - result.put("jobName", jobName); - - result.put("sourceData", cloudCause.getSourceDataJson()); - result.put("draData", cloudCause.getDRADataJson()); - - return result; - } - - public JSONObject generateErrorStatus(String errorMessage) { - JSONObject result = new JSONObject(); - - cloudCause.addStep("Error: " + errorMessage, JobStatus.failure.toString(), "Failed due to error", true); - - result.put("status", JobStatus.failure.toString()); - result.put("timestamp", System.currentTimeMillis()); - result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); - result.put("steps", cloudCause.getStepsArray()); - result.put("returnProps", cloudCause.getReturnProps()); - - if(build != null) { - result.put("url", Jenkins.getInstance().getRootUrl() + build.getUrl()); - result.put("jobExternalId", getJobUniqueIdFromBuild(build)); - result.put("name", build.getDisplayName()); - } else { - result.put("url", Jenkins.getInstance().getRootUrl()); - result.put("name", "Job Error"); - } - - return result; - } - - private String getJobUniqueIdFromBuild(AbstractBuild build) { - AbstractProject project = (AbstractProject)build.getProject(); - - String projectId; - - if (IdStore.getId(project) != null) { - projectId = IdStore.getId(project); - } else { - IdStore.makeId(project); - projectId = IdStore.getId(project); - } - - return projectId; - } - - private void evaluateSourceData(AbstractBuild build, CloudCause cause) { - List actions = build.getActions(); - - for(Action action : actions) { - // If using Hudson Git Plugin - if (action instanceof BuildData) { - Map branchMap = ((BuildData)action).getBuildsByBranchName(); - - for(String branchName : branchMap.keySet()) { - Build gitBuild = branchMap.get(branchName); - - if (gitBuild.getBuildNumber() == build.getNumber()) { - SourceData sourceData = new SourceData(branchName, gitBuild.getSHA1().getName(), "GIT"); - cause.setSourceData(sourceData); - } - } - } - } - } - - private void evaluateDRAData() { - DRAData data = cloudCause.getDRAData(); - - List actions = build.getActions(); - if(data == null) { - for(Action action : actions) { - if (action instanceof CrDraAction) { - CrDraAction cda = (CrDraAction)action; - data = cda.getDRAData(); - cloudCause.setDRAData(data); - } - } - } - - if(data == null) { - data = new DRAData(); - } - - - if (this.buildStep instanceof EvaluateGate) { - - EvaluateGate egs = (EvaluateGate)buildStep; - - String buildNumber = egs.getBuildNumber(); - String environment = egs.getEnvName(); - String policy = egs.getPolicyName(); - String applicationName = egs.getApplicationName(); - String orgName = egs.getOrgName(); - String toolchainName = egs.getToolchainName(); - - data.setApplicationName(applicationName); - data.setOrgName(orgName); - data.setToolchainName(toolchainName); - data.setEnvironment(environment); - data.setBuildNumber(buildNumber); - data.setPolicy(policy); - - for(Action action : actions) { - if (action instanceof GatePublisherAction) { - GatePublisherAction gpa = (GatePublisherAction)action; - - String gateText = gpa.getText(); - String riskDashboardLink = gpa.getRiskDashboardLink(); - String decision = gpa.getDecision(); - - data.setGateText(gateText); - data.setDecision(decision); - data.setRiskDahboardLink(riskDashboardLink); - - } - } - - CrDraAction cda = new CrDraAction(data); - build.addAction(cda); - - cloudCause.setDRAData(data); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java b/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java deleted file mode 100644 index 505bad8..0000000 --- a/src/main/java/com/ibm/devops/connect/JenkinsPipelineStatus.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.connect; - -import hudson.model.*; -import hudson.model.Item; -import hudson.tasks.BuildStep; - -import net.sf.json.JSONObject; - -import org.apache.commons.lang.builder.ToStringBuilder; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.jenkinsci.plugins.uniqueid.IdStore; - -import jenkins.model.Jenkins; - -import com.ibm.devops.dra.DevOpsGlobalConfiguration; -import com.ibm.devops.connect.CloudCause.JobStatus; - -import org.jenkinsci.plugins.uniqueid.IdStore; -import hudson.plugins.git.util.BuildData; -import hudson.plugins.git.util.Build; - -import com.ibm.devops.dra.EvaluateGate; -import com.ibm.devops.dra.GatePublisherAction; - -import org.jenkinsci.plugins.workflow.flow.FlowExecution; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.graph.FlowNode; - -import java.util.Map; -import java.util.List; - -/** - * Jenkins server - */ - -public class JenkinsPipelineStatus { - - private WorkflowRun workflowRun; - private CloudCause cloudCause; - private FlowNode node; - private Boolean newStep; - private Boolean isPaused; - - public JenkinsPipelineStatus(WorkflowRun workflowRun, CloudCause cloudCause, FlowNode node, boolean newStep, boolean isPaused) { - this.workflowRun = workflowRun; - this.cloudCause = cloudCause; - this.node = node; - this.newStep = newStep; - this.isPaused = isPaused; - } - - public JSONObject generate() { - JSONObject result = new JSONObject(); - - evaluateSourceData(workflowRun, cloudCause); - evaluateDRAData(); - - if(newStep && node == null) { - cloudCause.addStep("Starting Jenkins Pipeline", JobStatus.success.toString(), "Successfully started pipeline...", false); - } else if(newStep && node != null) { - cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Started stage", false); - } else if (isPaused && node != null) { - cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Please acknowledge the Jenkins Pipeline input", false); - } else if(!newStep && node != null) { - - if(node.getError() == null) { - cloudCause.updateLastStep(null, JobStatus.success.toString(), "Stage is successful", false); - } else { - cloudCause.updateLastStep(null, JobStatus.failure.toString(), node.getError().getDisplayName(), false); - } - } - - if (workflowRun.getResult() == null) { - if(workflowRun.isBuilding()) { - result.put("status", JobStatus.started.toString()); - } else { - result.put("status", JobStatus.unstarted.toString()); - } - } else { - if(workflowRun.getResult() == Result.SUCCESS) { - result.put("status", JobStatus.success.toString()); - } else { - result.put("status", JobStatus.failure.toString()); - } - } - - result.put("timestamp", System.currentTimeMillis()); - result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); - result.put("name", workflowRun.getDisplayName()); - result.put("steps", cloudCause.getStepsArray()); - result.put("url", Jenkins.getInstance().getRootUrl() + workflowRun.getUrl()); - result.put("returnProps", cloudCause.getReturnProps()); - result.put("isPipeline", true); - result.put("isPaused", isPaused); - - WorkflowJob workflowJob = (WorkflowJob)(workflowRun.getParent()); - result.put("jobName", workflowJob.getName()); - result.put("jobExternalId", getJobUniqueIdFromBuild(workflowJob)); - - result.put("sourceData", cloudCause.getSourceDataJson()); - result.put("draData", cloudCause.getDRADataJson()); - - return result; - } - - public JSONObject generateErrorStatus(String errorMessage) { - - return null; - } - - private String getJobUniqueIdFromBuild(WorkflowJob job) { - String jobId; - - if (IdStore.getId(job) != null) { - jobId = IdStore.getId(job); - } else { - IdStore.makeId(job); - jobId = IdStore.getId(job); - } - - return jobId; - } - - private void evaluateSourceData(WorkflowRun workflowRun, CloudCause cause) { - List actions = workflowRun.getActions(); - - for(Action action : actions) { - // If using Hudson Git Plugin - if (action instanceof BuildData) { - Map branchMap = ((BuildData)action).getBuildsByBranchName(); - - for(String branchName : branchMap.keySet()) { - Build gitBuild = branchMap.get(branchName); - - if (gitBuild.getBuildNumber() == workflowRun.getNumber()) { - SourceData sourceData = new SourceData(branchName, gitBuild.getSHA1().getName(), "GIT"); - cause.setSourceData(sourceData); - } - } - } - } - } - - private void evaluateDRAData() { - DRAData data = cloudCause.getDRAData(); - - List actions = workflowRun.getActions(); - if(data == null) { - for(Action action : actions) { - if (action instanceof CrDraAction) { - CrDraAction cda = (CrDraAction)action; - data = cda.getDRAData(); - cloudCause.setDRAData(data); - } - } - } - - if(data == null) { - // CAN NOT GET THIS DATA FROM PIPELINE - // data.setApplicationName(applicationName); - // data.setOrgName(orgName); - // data.setToolchainName(toolchainName); - // data.setEnvironment(environment); - - for(Action action : actions) { - if (action instanceof GatePublisherAction) { - data = new DRAData(); - GatePublisherAction gpa = (GatePublisherAction)action; - - String gateText = gpa.getText(); - String riskDashboardLink = gpa.getRiskDashboardLink(); - String decision = gpa.getDecision(); - String policy = gpa.getPolicyName(); - - data.setGateText(gateText); - data.setDecision(decision); - data.setRiskDahboardLink(riskDashboardLink); - data.setPolicy(policy); - data.setBuildNumber(Integer.toString(workflowRun.getNumber())); - - CrDraAction cda = new CrDraAction(data); - workflowRun.addAction(cda); - - cloudCause.setDRAData(data); - } - } - - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/notification/Proxy.java b/src/main/java/com/ibm/devops/connect/Proxy.javaBOGUS similarity index 100% rename from src/main/java/com/ibm/devops/notification/Proxy.java rename to src/main/java/com/ibm/devops/connect/Proxy.javaBOGUS diff --git a/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java b/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java index f4010b0..dacf25f 100644 --- a/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java +++ b/src/main/java/com/ibm/devops/connect/SecuredActions/AbstractSecuredAction.java @@ -4,7 +4,7 @@ import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import hudson.security.SecurityRealm; import org.acegisecurity.userdetails.UserDetails; -import com.ibm.devops.dra.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.DevOpsGlobalConfiguration; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; diff --git a/src/main/java/com/ibm/devops/connect/SourceData.java b/src/main/java/com/ibm/devops/connect/SourceData.java deleted file mode 100644 index 06bc110..0000000 --- a/src/main/java/com/ibm/devops/connect/SourceData.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.ibm.devops.connect; - -import net.sf.json.JSONObject; -import java.util.Set; - -public class SourceData { - - private String branch; - private String revision; - private String scmName; - private String type; - private Set remoteUrls; - - public SourceData(String branch, String revision, String type) { - this.branch = branch; - this.revision = revision; - this.type = type; - } - - public void setBranch (String branch) { - this.branch = branch; - } - - public void setRevision (String revision) { - this.revision = revision; - } - - public void setScmName(String scmName) { - this.scmName = scmName; - } - - public void setType (String type) { - this.type = type; - } - - public void setRemoteUrls (Set remoteUrls) { - this.remoteUrls = remoteUrls; - } - - public JSONObject toJson() { - JSONObject result = new JSONObject(); - - result.put("branch", branch); - result.put("revision", revision); - result.put("scmName", scmName); - result.put("type", type); - if(remoteUrls != null) { - result.put("remoteUrls", remoteUrls.toArray()); - } - - return result; - } -} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/Status/AbstractJenkinsStatus.java b/src/main/java/com/ibm/devops/connect/Status/AbstractJenkinsStatus.java new file mode 100644 index 0000000..a2f793a --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/Status/AbstractJenkinsStatus.java @@ -0,0 +1,266 @@ +package com.ibm.devops.connect.Status; + +import hudson.model.Run; +import hudson.model.AbstractItem; +import hudson.model.AbstractBuild; +import hudson.model.TaskListener; +import hudson.model.Action; +import hudson.model.Describable; +import hudson.EnvVars; +import hudson.plugins.git.util.BuildData; +import hudson.plugins.git.util.Build; +import hudson.tasks.BuildStep; +import hudson.FilePath; +import hudson.model.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.uniqueid.IdStore; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +import com.ibm.devops.connect.DevOpsGlobalConfiguration; +import com.ibm.devops.connect.CloudCause.JobStatus; +import com.ibm.devops.connect.CloudCause; + +import com.ibm.devops.dra.EvaluateGate; +import com.ibm.devops.dra.GatePublisherAction; + +import net.sf.json.JSONObject; + +import java.io.IOException; +import java.lang.InterruptedException; +import java.util.List; +import java.util.Map; + +abstract class AbstractJenkinsStatus { + public static final Logger log = LoggerFactory.getLogger(AbstractJenkinsStatus.class); + // Run + protected Run run; + + protected CloudCause cloudCause; + + protected BuildStep buildStep; + protected FlowNode node; + + protected Boolean newStep; + protected Boolean isFatal; + + protected TaskListener taskListener; + + protected EnvVars envVars; + protected CrAction crAction; + + protected Boolean isPipeline; + protected Boolean isPaused; + + protected void getOrCreateCrAction() { + // Get CrAction + List actions = run.getActions(); + for(Action action : actions) { + if (action instanceof CrAction) { + crAction = (CrAction)action; + } + } + + // If not, create crAction + if (crAction == null) { + crAction = new CrAction(); + run.addAction(crAction); + } + } + + protected void getEnvVars() { + try { + if(run != null && taskListener != null) { + this.envVars = run.getEnvironment(taskListener); + } + } catch (IOException ioEx) { + log.warn("IOException thrown while trying to retrieve EnvVars in constructor: " + ioEx); + } catch (InterruptedException intEx) { + log.warn("InterruptedException thrown while trying to retrieve EnvVars in constructor: " + intEx); + } + } + + public JSONObject generateErrorStatus(String errorMessage) { + JSONObject result = new JSONObject(); + + cloudCause.addStep("Error: " + errorMessage, JobStatus.failure.toString(), "Failed due to error", true); + + result.put("status", JobStatus.failure.toString()); + result.put("timestamp", System.currentTimeMillis()); + result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + result.put("steps", cloudCause.getStepsArray()); + result.put("returnProps", cloudCause.getReturnProps()); + + if(run != null) { + result.put("url", Jenkins.getInstance().getRootUrl() + run.getUrl()); + result.put("jobExternalId", getJobUniqueIdFromBuild()); + result.put("name", run.getDisplayName()); + } else { + result.put("url", Jenkins.getInstance().getRootUrl()); + result.put("name", "Job Error"); + } + + return result; + } + + private String getJobUniqueIdFromBuild() { + AbstractItem project = run.getParent(); + + String projectId; + + if (IdStore.getId(project) != null) { + projectId = IdStore.getId(project); + } else { + IdStore.makeId(project); + projectId = IdStore.getId(project); + } + + return projectId; + } + + protected void evaluateSourceData() { + List actions = run.getActions(); + + // Try to get from the crAction + SourceData sd = crAction.getSourceData(); + if(sd != null) { + cloudCause.setSourceData(sd); + } + + if (envVars != null) { + for(Action action : actions) { + // If using Hudson Git Plugin + if (action instanceof BuildData) { + Map branchMap = ((BuildData)action).getBuildsByBranchName(); + + for(String branchName : branchMap.keySet()) { + Build gitBuild = branchMap.get(branchName); + + if (gitBuild.getBuildNumber() == run.getNumber()) { + SourceData sourceData = new SourceData(branchName, gitBuild.getSHA1().getName(), "GIT"); + sourceData.populateCommitMessage(taskListener, envVars, getWorkspaceFilePath(), gitBuild); + + cloudCause.setSourceData(sourceData); + crAction.setSourceData(sourceData); + } + } + } + } + } + } + + protected void evaluateDRAData() { + DRAData data = cloudCause.getDRAData(); + + List actions = run.getActions(); + if(data == null) { + data = crAction.getDRAData(); + cloudCause.setDRAData(data); + } + + if(data == null) { + data = new DRAData(); + } + + if (Jenkins.getInstance().getPlugin("ibm-cloud-devops") != null) { + + //This block if for non-pipeline jobs to set additional data that we have access to + if (this.buildStep != null && this.buildStep instanceof EvaluateGate) { + + EvaluateGate egs = (EvaluateGate)buildStep; + + String environment = egs.getEnvName(); + String applicationName = egs.getApplicationName(); + String orgName = egs.getOrgName(); + String toolchainName = egs.getToolchainName(); + + data.setApplicationName(applicationName); + data.setOrgName(orgName); + data.setToolchainName(toolchainName); + data.setEnvironment(environment); + } + + for(Action action : actions) { + if (action instanceof GatePublisherAction) { + GatePublisherAction gpa = (GatePublisherAction)action; + + String gateText = gpa.getText(); + String riskDashboardLink = gpa.getRiskDashboardLink(); + String decision = gpa.getDecision(); + String policy = gpa.getPolicyName(); + + data.setGateText(gateText); + data.setDecision(decision); + data.setRiskDahboardLink(riskDashboardLink); + data.setPolicy(policy); + data.setBuildNumber(Integer.toString(run.getNumber())); + + crAction.setDRAData(data); + cloudCause.setDRAData(data); + } + } + } + } + + private void evaluateEnvironment() { + if( envVars != null ) { + crAction.updateEnvProperties(envVars); + } + } + + public JSONObject generate() { + JSONObject result = new JSONObject(); + + evaluateSourceData(); + evaluateDRAData(); + evaluateEnvironment(); + + if(isPipeline) { + evaluatePipelineStep(); + } else { + evaluateBuildStep(); + } + + if (run.getResult() == null) { + if(run.isBuilding()) { + result.put("status", JobStatus.started.toString()); + } else { + result.put("status", JobStatus.unstarted.toString()); + } + } else { + if(run.getResult() == Result.SUCCESS) { + result.put("status", JobStatus.success.toString()); + } else { + result.put("status", JobStatus.failure.toString()); + } + } + + result.put("timestamp", System.currentTimeMillis()); + result.put("syncId", Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getSyncId()); + result.put("name", run.getDisplayName()); + result.put("steps", cloudCause.getStepsArray()); + result.put("url", Jenkins.getInstance().getRootUrl() + run.getUrl()); + result.put("returnProps", cloudCause.getReturnProps()); + result.put("isPipeline", isPipeline); + result.put("isPaused", isPaused); + result.put("jobName", run.getParent().getName()); + result.put("jobExternalId", getJobUniqueIdFromBuild()); + result.put("sourceData", cloudCause.getSourceDataJson()); + result.put("draData", cloudCause.getDRADataJson()); + result.put("crProperties", crAction.getCrProperties()); + result.put("envProperties", crAction.getEnvProperties()); + + return result; + } + + abstract protected FilePath getWorkspaceFilePath(); + + abstract protected void evaluatePipelineStep(); + + abstract protected void evaluateBuildStep(); + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/Status/CrAction.java b/src/main/java/com/ibm/devops/connect/Status/CrAction.java new file mode 100644 index 0000000..878ae72 --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/Status/CrAction.java @@ -0,0 +1,63 @@ +package com.ibm.devops.connect.Status; + +import hudson.model.Action; +import java.util.Map; +import java.util.TreeMap; + +public class CrAction implements Action { + + private DRAData draData; + private SourceData sourceData; + private Map crProperties = new TreeMap(); + private Map envProperties = new TreeMap(); + + public CrAction() { + } + + public DRAData getDRAData() { + return draData; + } + + public void setDRAData (DRAData draData) { + this.draData = draData; + } + + public SourceData getSourceData() { + return sourceData; + } + + public void setSourceData (SourceData sourceData) { + this.sourceData = sourceData; + } + + public void updateCrProperties (Map properties) { + this.crProperties.putAll(properties); + } + + public Map getCrProperties () { + return crProperties; + } + + public void updateEnvProperties (Map properties) { + this.envProperties.putAll(properties); + } + + public Map getEnvProperties () { + return envProperties; + } + + @Override + public String getIconFileName() { + return null; + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public String getUrlName() { + return null; + } +} diff --git a/src/main/java/com/ibm/devops/connect/DRAData.java b/src/main/java/com/ibm/devops/connect/Status/DRAData.java similarity index 97% rename from src/main/java/com/ibm/devops/connect/DRAData.java rename to src/main/java/com/ibm/devops/connect/Status/DRAData.java index 2ff1b2c..0b5ba2f 100644 --- a/src/main/java/com/ibm/devops/connect/DRAData.java +++ b/src/main/java/com/ibm/devops/connect/Status/DRAData.java @@ -1,4 +1,4 @@ -package com.ibm.devops.connect; +package com.ibm.devops.connect.Status; import net.sf.json.JSONObject; import java.util.Set; diff --git a/src/main/java/com/ibm/devops/connect/Status/JenkinsJobStatus.java b/src/main/java/com/ibm/devops/connect/Status/JenkinsJobStatus.java new file mode 100644 index 0000000..a78230d --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/Status/JenkinsJobStatus.java @@ -0,0 +1,87 @@ +/* + + + Copyright 2018 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect.Status; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import hudson.model.TaskListener; +import hudson.FilePath; +import hudson.model.Describable; +import hudson.tasks.BuildStep; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; + +import java.io.File; + +import com.ibm.devops.connect.CloudCause.JobStatus; +import com.ibm.devops.connect.CloudCause; + +import org.jenkinsci.plugins.workflow.actions.WorkspaceAction; + +/** + * Jenkins server + */ + +public class JenkinsJobStatus extends AbstractJenkinsStatus { + + public static final Logger log = LoggerFactory.getLogger(JenkinsJobStatus.class); + + public JenkinsJobStatus(AbstractBuild build, CloudCause cloudCause, BuildStep buildStep, BuildListener buildListener, Boolean newStep, Boolean isFatal) { + this.run = build; + this.cloudCause = cloudCause; + this.buildStep = buildStep; + this.newStep = newStep; + this.isFatal = isFatal; + this.taskListener = buildListener; + this.isPaused = false; + this.isPipeline = false; + + getEnvVars(); + getOrCreateCrAction(); + } + + protected FilePath getWorkspaceFilePath() { + return ((AbstractBuild)run).getWorkspace(); + } + + + protected void evaluateBuildStep() { + if(!(buildStep instanceof hudson.model.ParametersDefinitionProperty)) { + if (newStep) { + cloudCause.addStep(((Describable)buildStep).getDescriptor().getDisplayName(), JobStatus.started.toString(), "Started a build step", false); + } else { + String newStatus; + String message; + if (!isFatal) { + newStatus = JobStatus.success.toString(); + message = "The build step finished and the job will continue."; + } else { + newStatus = JobStatus.failure.toString(); + message = "The build step failed and the job can not continue."; + } + + if (cloudCause.isCreatedByCR()) { + cloudCause.updateLastStep(((Describable)buildStep).getDescriptor().getDisplayName(), newStatus, message, isFatal); + } + } + } + } + + protected void evaluatePipelineStep() { + // No Op + } + +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java b/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java new file mode 100644 index 0000000..3f4877c --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java @@ -0,0 +1,104 @@ +/* + + + Copyright 2018 IBM Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + */ + +package com.ibm.devops.connect.Status; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import hudson.model.TaskListener; +import hudson.FilePath; + +import java.io.File; + +import com.ibm.devops.connect.CloudCause.JobStatus; +import com.ibm.devops.connect.CloudCause; + +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; + +import org.jenkinsci.plugins.workflow.actions.WorkspaceAction; + +/** + * Jenkins server + */ + +public class JenkinsPipelineStatus extends AbstractJenkinsStatus { + + private boolean shouldCheckSCM = false; + + public static final Logger log = LoggerFactory.getLogger(JenkinsPipelineStatus.class); + + public JenkinsPipelineStatus(WorkflowRun workflowRun, CloudCause cloudCause, FlowNode node, TaskListener listener, boolean newStep, boolean isPaused) { + this.run = workflowRun; + this.cloudCause = cloudCause; + this.node = node; + this.newStep = newStep; + this.isPaused = isPaused; + this.isPipeline = true; + this.taskListener = listener; + + getEnvVars(); + getOrCreateCrAction(); + + if (envVars != null) { + shouldCheckSCM = true; + } + } + + protected FilePath getWorkspaceFilePath() { + FlowExecution exec = ((WorkflowRun)run).getExecution(); + if(exec == null) + return null; + + FlowGraphWalker w = new FlowGraphWalker(exec); + for (FlowNode n : w) { + if (n.getClass().getName().equals("org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode")) { + WorkspaceAction action = n.getAction(WorkspaceAction.class); + if(action != null) { + String node = action.getNode().toString(); + String workspace = action.getPath().toString(); + FilePath result = new FilePath(new File(workspace)); + + return result; + } + } + } + + return null; + } + + protected void evaluatePipelineStep() { + if(newStep && node == null) { + cloudCause.addStep("Starting Jenkins Pipeline", JobStatus.success.toString(), "Successfully started pipeline...", false); + } else if(newStep && node != null) { + cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Started stage", false); + } else if (isPaused && node != null) { + cloudCause.addStep(node.getDisplayName(), JobStatus.started.toString(), "Please acknowledge the Jenkins Pipeline input", false); + } else if(!newStep && node != null) { + + if(node.getError() == null) { + cloudCause.updateLastStep(null, JobStatus.success.toString(), "Stage is successful", false); + } else { + cloudCause.updateLastStep(null, JobStatus.failure.toString(), node.getError().getDisplayName(), false); + } + } + } + + protected void evaluateBuildStep() { + // No Op + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/connect/Status/SourceData.java b/src/main/java/com/ibm/devops/connect/Status/SourceData.java new file mode 100644 index 0000000..937079a --- /dev/null +++ b/src/main/java/com/ibm/devops/connect/Status/SourceData.java @@ -0,0 +1,95 @@ +package com.ibm.devops.connect.Status; + +import net.sf.json.JSONObject; +import java.util.Set; +import hudson.EnvVars; +import hudson.model.TaskListener; +import hudson.FilePath; +import hudson.plugins.git.util.Build; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jenkinsci.plugins.gitclient.Git; +import org.jenkinsci.plugins.gitclient.GitClient; +import hudson.plugins.git.Revision; +import org.eclipse.jgit.revwalk.RevCommit; +import hudson.plugins.git.util.RevCommitRepositoryCallback; + +import java.io.IOException; +import java.lang.InterruptedException; + +public class SourceData { + public static final Logger log = LoggerFactory.getLogger(SourceData.class); + + private String branch; + private String revision; + private String scmName; + private String type; + private String shortMessage; + private String fullMessage; + private Set remoteUrls; + + public SourceData(String branch, String revision, String type) { + this.branch = branch; + this.revision = revision; + this.type = type; + } + + public void setBranch (String branch) { + this.branch = branch; + } + + public void setRevision (String revision) { + this.revision = revision; + } + + public void setScmName(String scmName) { + this.scmName = scmName; + } + + public void setType (String type) { + this.type = type; + } + + public void setRemoteUrls (Set remoteUrls) { + this.remoteUrls = remoteUrls; + } + + public JSONObject toJson() { + JSONObject result = new JSONObject(); + + result.put("branch", branch); + result.put("revision", revision); + result.put("scmName", scmName); + result.put("type", type); + result.put("fullMessage", fullMessage); + result.put("shortMessage", shortMessage); + result.put("type", type); + if(remoteUrls != null) { + result.put("remoteUrls", remoteUrls.toArray()); + } + + return result; + } + + public void populateCommitMessage(TaskListener listener, EnvVars envVars, FilePath workspace, Build gitBuild) { + try { + Git git = Git.with(listener, envVars); + if (workspace != null) { + git = git.in(workspace); + } + + GitClient gitClient = git.getClient(); + RevCommit commit = gitClient.withRepository(new RevCommitRepositoryCallback(gitBuild)); + + this.shortMessage = commit.getShortMessage(); + this.fullMessage = commit.getFullMessage(); + + } catch (IOException ioEx) { + log.warn("IOException thrown while trying to retrieve commit message in populateCommitMessage: " + ioEx); + } catch (InterruptedException intEx) { + log.warn("InterruptException thrown while trying to retrieve commit message in populateCommitMessage: " + intEx); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java b/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java deleted file mode 100644 index 787dece..0000000 --- a/src/main/java/com/ibm/devops/dra/AbstractDevOpsAction.java +++ /dev/null @@ -1,985 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.ibm.devops.dra.steps.AbstractDevOpsStep; -import hudson.EnvVars; -import hudson.ProxyConfiguration; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.Recorder; -import hudson.util.ListBoxModel; -import hudson.util.Secret; -import jenkins.model.Jenkins; -import org.apache.http.HttpHost; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.cloudfoundry.client.lib.CloudCredentials; -import org.cloudfoundry.client.lib.CloudFoundryClient; -import org.cloudfoundry.client.lib.CloudFoundryException; -import org.cloudfoundry.client.lib.HttpProxyConfiguration; - -import java.io.IOException; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * Abstract DRA Builder to share common method between two different post-build actions - */ -public abstract class AbstractDevOpsAction extends Recorder { - - public final static Logger LOGGER = Logger.getLogger(AbstractDevOpsAction.class.getName()); - public final static String ORG_NAME = "IBM_CLOUD_DEVOPS_ORG"; - public final static String APP_NAME = "IBM_CLOUD_DEVOPS_APP_NAME"; - public final static String TOOLCHAIN_ID = "IBM_CLOUD_DEVOPS_TOOLCHAIN_ID"; - public final static String USERNAME = "IBM_CLOUD_DEVOPS_CREDS_USR"; - public final static String PASSWORD = "IBM_CLOUD_DEVOPS_CREDS_PSW"; - public final static String RESULT_SUCCESS = "SUCCESS"; - public final static String RESULT_FAIL = "FAIL"; - - private final static String ORG= "&&organization_guid:"; - private final static String SPACE= "&&space_guid:"; - - // ImmutableMap accepts at most 5 items - https://www.lewuathe.com/guava-immutablemap-limitation.html - private static Map TARGET_API_MAP = ImmutableMap.builder() - .put("production", "https://api.ng.bluemix.net") - .put("dev", "https://api.stage1.ng.bluemix.net") - .put("new", "https://api.stage1.ng.bluemix.net") - .put("stage1", "https://api.stage1.ng.bluemix.net") - .put("eu-de", "https://api.eu-de.bluemix.net") - .put("eu-gb", "https://api.eu-gb.bluemix.net") - .build(); - - private static Map ORGANIZATIONS_URL_MAP = ImmutableMap.builder() - .put("production", "https://api.ng.bluemix.net/v2/organizations?q=name:") - .put("dev", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") - .put("new", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") - .put("stage1", "https://api.stage1.ng.bluemix.net/v2/organizations?q=name:") - .put("eu-de", "https://api.eu-de.bluemix.net/v2/organizations?q=name:") - .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/organizations?q=name:") - .build(); - - private static Map SPACES_URL_MAP = ImmutableMap.builder() - .put("production", "https://api.ng.bluemix.net/v2/spaces?q=name:") - .put("dev", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") - .put("new", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") - .put("stage1", "https://api.stage1.ng.bluemix.net/v2/spaces?q=name:") - .put("eu-de", "https://api.eu-de.bluemix.net/v2/spaces?q=name:") - .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/spaces?q=name:") - .build(); - - private static Map APPS_URL_MAP = ImmutableMap.builder() - .put("production", "https://api.ng.bluemix.net/v2/apps?q=name:") - .put("dev", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") - .put("new", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") - .put("stage1", "https://api.stage1.ng.bluemix.net/v2/apps?q=name:") - .put("eu-de", "https://api.eu-de.bluemix.net/v2/apps?q=name:") - .put("eu-gb", "https://api.eu-gb.bluemix.net/v2/apps?q=name:") - .build(); - - private static Map TOOLCHAINS_URL_MAP = ImmutableMap.of( - "production", "https://otc-api.ng.bluemix.net/api/v1/toolchains?organization_guid=", - "dev", "https://otc-api.stage1.ng.bluemix.net/api/v1/toolchains?organization_guid=", - "stage1", "https://otc-api-integration.stage1.ng.bluemix.net/api/v1/toolchains?organization_guid=", - "new", "https://otc-api.stage1.ng.bluemix.net/api/v1/toolchains?organization_guid=" - ); - - private static Map POLICIES_URL_MAP = ImmutableMap.of( - "production", "https://dra.ng.bluemix.net/api/v4/organizations/{org_name}/toolchainids/{toolchain_name}/policies", - "dev", "https://dev-dra.stage1.ng.bluemix.net/api/v4/organizations/{org_name}/toolchainids/{toolchain_name}/policies", - "new", "https://new-dra.stage1.ng.bluemix.net/api/v4/organizations/{org_name}/toolchainids/{toolchain_name}/policies", - "stage1", "https://dra.stage1.ng.bluemix.net/api/v4/organizations/{org_name}/toolchainids/{toolchain_name}/policies" - ); - - private static Map DLMS_ENV_MAP = ImmutableMap.of( - "production", "https://dlms.ng.bluemix.net/v2", - "dev", "https://dev-dlms.stage1.ng.bluemix.net/v2", - "new", "https://new-dlms.stage1.ng.bluemix.net/v2", - "stage1", "https://dlms.stage1.ng.bluemix.net/v2" - ); - - private static Map GATE_DECISION_ENV_MAP = ImmutableMap.of( - "production", "https://dra.ng.bluemix.net/api/v4", - "dev", "https://dev-dra.stage1.ng.bluemix.net/api/v4", - "new", "https://new-dra.stage1.ng.bluemix.net/api/v4", - "stage1", "https://dra.stage1.ng.bluemix.net/api/v4" - ); - - // Todo: need to get rid of ng and add env_id - private static Map CONTROL_CENTER_ENV_MAP = ImmutableMap.of( - "production", "https://console.ng.bluemix.net/devops/insights/#!/", - "dev", "https://dev-console.stage1.ng.bluemix.net/devops/insights/#!/", - "new", "https://new-console.stage1.ng.bluemix.net/devops/insights/#!/", - "stage1", "https://console.stage1.ng.bluemix.net/devops/insights/#!/" - ); - - public static void printPluginVersion(ClassLoader loader, PrintStream printStream) { - final Properties properties = new Properties(); - try { - properties.load(loader.getResourceAsStream("plugin.properties")); - printStream.println("[IBM Cloud DevOps] version: " + properties.getProperty("version")); - } catch (IOException e) { - e.printStackTrace(); - } - - } - - /** - * get the environment based on the console - * @param consoleUrl - */ - public static String getEnv(String consoleUrl) { - - if (Util.isNullOrEmpty(consoleUrl)) { - return "production"; - } else if (consoleUrl.contains("dev-console.stage1.bluemix.net") || consoleUrl.contains("dev-console.stage1.ng.bluemix.net")) { - return "dev"; - } else if (consoleUrl.contains("new-console.stage1.bluemix.net") || consoleUrl.contains("new-console.stage1.ng.bluemix.net")) { - return "new"; - } else if (consoleUrl.contains("console.stage1.bluemix.net") || consoleUrl.contains("console.stage1.ng.bluemix.net")) { - return "stage1"; - } else if (consoleUrl.contains("console.bluemix.net") || consoleUrl.contains("console.ng.bluemix.net")){ - return "production"; - } else { - int start = consoleUrl.indexOf("console") + 8; - int end = consoleUrl.indexOf("bluemix.net") - 1; - String local = consoleUrl.substring(start, end); - return local; - } - } - - /** - * set the required env variables' HashMap for all steps - * @param step - * @param envVars - * @return - */ - public static HashMap setRequiredEnvVars(AbstractDevOpsStep step, EnvVars envVars) { - HashMap requiredEnvVars = new HashMap<>(); - requiredEnvVars.put(ORG_NAME, Util.isNullOrEmpty(step.getOrgName()) ? envVars.get("IBM_CLOUD_DEVOPS_ORG") : step.getOrgName()); - requiredEnvVars.put(APP_NAME, Util.isNullOrEmpty(step.getApplicationName()) ? envVars.get("IBM_CLOUD_DEVOPS_APP_NAME") : step.getApplicationName()); - requiredEnvVars.put(TOOLCHAIN_ID, Util.isNullOrEmpty(step.getToolchainId()) ? envVars.get("IBM_CLOUD_DEVOPS_TOOLCHAIN_ID") : step.getToolchainId()); - requiredEnvVars.put(USERNAME, envVars.get("IBM_CLOUD_DEVOPS_CREDS_USR")); - requiredEnvVars.put(PASSWORD, envVars.get("IBM_CLOUD_DEVOPS_CREDS_PSW")); - return requiredEnvVars; - } - - public static String chooseTargetAPI(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (TARGET_API_MAP.keySet().contains(environment)) { - return TARGET_API_MAP.get(environment); - } else { - String api = TARGET_API_MAP.get("production").replace("ng", environment); - return api; - } - } - - return TARGET_API_MAP.get("production"); - } - - public static String chooseToolchainsUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (TOOLCHAINS_URL_MAP.keySet().contains(environment)) { - return TOOLCHAINS_URL_MAP.get(environment); - } else { - String api = TOOLCHAINS_URL_MAP.get("production").replace("ng", environment); - return api; - } - } - - return TOOLCHAINS_URL_MAP.get("production"); - } - - public static String chooseOrganizationsUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (ORGANIZATIONS_URL_MAP.keySet().contains(environment)) { - return ORGANIZATIONS_URL_MAP.get(environment); - } else { - String api = ORGANIZATIONS_URL_MAP.get("production").replace("ng", environment); - return api; - } - } - return ORGANIZATIONS_URL_MAP.get("production"); - } - - public static String chooseSpacesUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (SPACES_URL_MAP.keySet().contains(environment)) { - return SPACES_URL_MAP.get(environment); - } else { - String api = SPACES_URL_MAP.get("production").replace("ng", environment); - return api; - } - } - - return SPACES_URL_MAP.get("production"); - } - - public static String chooseAppsUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (APPS_URL_MAP.keySet().contains(environment)) { - return APPS_URL_MAP.get(environment); - } else { - String api = APPS_URL_MAP.get("production").replace("ng", environment); - return api; - } - } - - return APPS_URL_MAP.get("production"); - } - - public static String choosePoliciesUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (POLICIES_URL_MAP.keySet().contains(environment)) { - return POLICIES_URL_MAP.get(environment); - } else { - String api = POLICIES_URL_MAP.get("production").replace("ng", environment); - return api; - } - } - - return POLICIES_URL_MAP.get("production"); - } - - - /** - * choose DLMS Url for different environment (production, stage1, new, dev) - * @param environment - * @return - */ - - public static String chooseDLMSUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (DLMS_ENV_MAP.keySet().contains(environment)) { - return DLMS_ENV_MAP.get(environment); - } else { - String api = DLMS_ENV_MAP.get("production").replace("ng", environment); - return api; - } - } - - return DLMS_ENV_MAP.get("production"); - } - - /** - * choose DRA Url for different environment (production, stage1, new, dev) - * @param environment - * @return - */ - public static String chooseDRAUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (GATE_DECISION_ENV_MAP.keySet().contains(environment)) { - return GATE_DECISION_ENV_MAP.get(environment); - } else { - String api = GATE_DECISION_ENV_MAP.get("production").replace("ng", environment); - return api; - } - } - - return GATE_DECISION_ENV_MAP.get("production"); - } - - /** - * choose control center Url for different environment (production, stage1, new, dev) - * @param environment - * @return - */ - public static String chooseControlCenterUrl(String environment) { - if (!Util.isNullOrEmpty(environment)) { - if (CONTROL_CENTER_ENV_MAP.keySet().contains(environment)) { - return CONTROL_CENTER_ENV_MAP.get(environment); - } else { - String api = CONTROL_CENTER_ENV_MAP.get("production").replace("ng", environment); - return api; - } - } - return CONTROL_CENTER_ENV_MAP.get("production"); - } - - /** - * check if the root url in the jenkins is set correctly - * @param printStream - * @return - */ - public static boolean checkRootUrl(PrintStream printStream) { - - if (Util.isNullOrEmpty(Jenkins.getInstance().getRootUrl())) { - printStream.println( - "[IBM Cloud DevOps] The Jenkins global root url is not set. Please set it to use this postbuild Action. \"Manage Jenkins > Configure System > Jenkins URL\""); - printStream.println("[IBM Cloud DevOps] Warning: You would not get the correct project url"); - return false; - } - return true; - } - - /** - * Get the Bluemix Token using Cloud Foundry as the authentication with DLMS and DRA backend - * @param context - the current job - * @param credentialsId - the credential id in Jenkins - * @param targetAPI - the target api that used for logging in to the Bluemix - * @return the bearer token - */ - public static String getBluemixToken(Job context, String credentialsId, String targetAPI) throws Exception { - - try { - List standardCredentials = CredentialsProvider.lookupCredentials( - StandardUsernamePasswordCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(targetAPI).build()); - - StandardUsernamePasswordCredentials credentials = - CredentialsMatchers.firstOrNull(standardCredentials, CredentialsMatchers.withId(credentialsId)); - - if (credentials == null || credentials.getUsername() == null || credentials.getPassword() == null) { - throw new Exception("Failed to get Credentials"); - } - CloudCredentials cloudCredentials = new CloudCredentials(credentials.getUsername(), Secret.toString(credentials.getPassword())); - if (cloudCredentials == null) { - throw new Exception("Failed to get Cloud Credentials"); - } - - URL url = new URL(targetAPI); - HttpProxyConfiguration configuration = buildProxyConfiguration(url); - - CloudFoundryClient client = new CloudFoundryClient(cloudCredentials, url, configuration, true); - return "bearer " + client.login().toString(); - - } catch (MalformedURLException e) { - throw e; - } catch (CloudFoundryException e) { - throw e; - } - } - - public static String getBluemixToken(ItemGroup context, String credentialsId, String targetAPI) throws Exception { - - try { - List standardCredentials = CredentialsProvider.lookupCredentials( - StandardUsernamePasswordCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(targetAPI).build()); - - StandardUsernamePasswordCredentials credentials = - CredentialsMatchers.firstOrNull(standardCredentials, CredentialsMatchers.withId(credentialsId)); - - if (credentials == null || credentials.getUsername() == null || credentials.getPassword() == null) { - throw new Exception("Failed to get Credentials"); - } - CloudCredentials cloudCredentials = new CloudCredentials(credentials.getUsername(), Secret.toString(credentials.getPassword())); - if (cloudCredentials == null) { - throw new Exception("Failed to get Cloud Credentials"); - } - - URL url = new URL(targetAPI); - HttpProxyConfiguration configuration = buildProxyConfiguration(url); - - CloudFoundryClient client = new CloudFoundryClient(cloudCredentials, url, configuration, true); - return "bearer " + client.login().toString(); - - } catch (MalformedURLException e) { - throw e; - } catch (CloudFoundryException e) { - throw e; - } - } - - public static String getBluemixToken(String username, String password, String targetAPI) throws MalformedURLException, CloudFoundryException { - try { - - CloudCredentials cloudCredentials = new CloudCredentials(username, password); - - URL url = new URL(targetAPI); - HttpProxyConfiguration configuration = buildProxyConfiguration(url); - - CloudFoundryClient client = new CloudFoundryClient(cloudCredentials, url, configuration, true); - return "bearer " + client.login().toString(); - - } catch (MalformedURLException e) { - throw e; - } catch (CloudFoundryException e) { - throw e; - } - } - - /** - * build proxy for cloud foundry http connection - * @param targetURL - target API URL - * @return the full target URL - */ - private static HttpProxyConfiguration buildProxyConfiguration(URL targetURL) { - ProxyConfiguration proxyConfig = Jenkins.getInstance().proxy; - if (proxyConfig == null) { - return null; - } - - String host = targetURL.getHost(); - for (Pattern p : proxyConfig.getNoProxyHostPatterns()) { - if (p.matcher(host).matches()) { - return null; - } - } - - return new HttpProxyConfiguration(proxyConfig.name, proxyConfig.port); - } - - /** - * get the root project - * @param job - the source job - * @return the root project - */ - private static Job getRootProject(Job job) { - if (job instanceof AbstractProject) { - return ((AbstractProject)job).getRootProject(); - } else { - return job; - } - } - - // retrieve the "folder" (jenkins root if no folder used) for this build - private static ItemGroup getItemGroup(Run build) { - return getRootProject(build.getParent()).getParent(); - } - - - /** - * Recursive function to locate the triggered build - * @param job - the target job - * @param parent - the current job - * @return the specific build of the target job - */ - private static Run getBuild(Job job, Run parent) { - Run result = null; - - // Upstream job for matrix will be parent project, not only individual configuration: - List jobNames = new ArrayList<>(); - jobNames.add(job.getFullName()); - if ((job instanceof AbstractProject) && ((AbstractProject)job).getRootProject() != job) { - jobNames.add(((AbstractProject)job).getRootProject().getFullName()); - } - - List> upstreamBuilds = new ArrayList<>(); - - for (Cause cause: parent.getCauses()) { - if (cause instanceof Cause.UpstreamCause) { - Cause.UpstreamCause upstream = (Cause.UpstreamCause) cause; - Run upstreamRun = upstream.getUpstreamRun(); - if (upstreamRun != null) { - upstreamBuilds.add(upstreamRun); - } - } - } - - if (parent instanceof AbstractBuild) { - AbstractBuild parentBuild = (AbstractBuild)parent; - - Map parentUpstreamBuilds = parentBuild.getUpstreamBuilds(); - for (Map.Entry buildEntry : parentUpstreamBuilds.entrySet()) { - upstreamBuilds.add(buildEntry.getKey().getBuildByNumber(buildEntry.getValue())); - } - } - - for (Run upstreamBuild : upstreamBuilds) { - Run run; - - if(upstreamBuild == null) { - continue; - } - if (jobNames.contains(upstreamBuild.getParent().getFullName())) { - // Use the 'job' parameter instead of directly the 'upstreamBuild', because of Matrix jobs. - run = job.getBuildByNumber(upstreamBuild.getNumber()); - } else { - // Figure out the parent job and do a recursive call to getBuild - run = getBuild(job, upstreamBuild); - } - - if (run != null){ - if ((result == null) || (result.getNumber() > run.getNumber())) { - result = run; - } - } - - } - - return result; - } - - /** - * locate triggered build - * @param build - the current running build of this job - * @param name - the build job name that you are going to locate - * @param printStream - logger - * @return - */ - public static Run getTriggeredBuild(Run build, String name, EnvVars envVars, PrintStream printStream) { - // if user specify the build job as current job or leave it empty - if (name == null || name.isEmpty() || name.equals(build.getParent().getName())) { - printStream.println("[IBM Cloud DevOps] Current job is the build job"); - return build; - } else { - name = envVars.expand(name); - Job job = Jenkins.getInstance().getItem(name, getItemGroup(build), Job.class); - if (job != null) { - Run src = getBuild(job, build); - if (src == null) { - // if user runs the test job independently - printStream.println("[IBM Cloud DevOps] Are you running the test job independently? Use the last successful build of the build job"); - src = job.getLastSuccessfulBuild(); - } - - return src; - } else { - // if user does not specify the build job or can not find the build job that user specifies - printStream.println("[IBM Cloud DevOps] ERROR: Failed to find the build job, please check the build job name"); - return null; - } - } - } - - /** - * Get the build number - * @param build - * @return - */ - public String getBuildNumber(String jobName, Run build) { - - String jName = ""; - Scanner s = new Scanner(jobName).useDelimiter("/"); - while(s.hasNext()){ // this will loop through the string until the last string(job name) is reached. - jName = s.next(); - } - s.close(); - - String buildNumber = jName + ":" + build.getNumber(); - return buildNumber; - } - - /** - * Get a list of toolchains using given token and organization name. - * @param token - * @param orgName - * @return - */ - public static ListBoxModel getToolchainList(String token, String orgName, String environment, Boolean debug_mode) { - - LOGGER.setLevel(Level.INFO); - - if(debug_mode){ - LOGGER.info("#######################"); - LOGGER.info("TOKEN:" + token); - LOGGER.info("ORG:" + orgName); - LOGGER.info("ENVIRONMENT:" + environment); - } - - String orgId = getOrgId(token, orgName, environment, debug_mode); - ListBoxModel emptybox = new ListBoxModel(); - emptybox.add("","empty"); - - if(orgId == null) { - return emptybox; - } - - CloseableHttpClient httpClient = HttpClients.createDefault(); - String toolchains_url = chooseToolchainsUrl(environment); - if(debug_mode){ - LOGGER.info("GET TOOLCHAIN LIST URL:" + toolchains_url + orgId); - } - - HttpGet httpGet = new HttpGet(toolchains_url + orgId); - - httpGet = addProxyInformation(httpGet); - - httpGet.setHeader("Authorization", token); - CloseableHttpResponse response = null; - - try { - response = httpClient.execute(httpGet); - String resStr = EntityUtils.toString(response.getEntity()); - - if(debug_mode){ - LOGGER.info("RESPONSE FROM TOOLCHAINS API:" + response.getStatusLine().toString()); - } - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject obj = element.getAsJsonObject(); - JsonArray items = obj.getAsJsonArray("items"); - ListBoxModel toolchainList = new ListBoxModel(); - - for (int i = 0; i < items.size(); i++) { - JsonObject toolchainObj = items.get(i).getAsJsonObject(); - String toolchainName = String.valueOf(toolchainObj.get("name")).replaceAll("\"", ""); - String toolchainID = String.valueOf(toolchainObj.get("toolchain_guid")).replaceAll("\"", ""); - toolchainList.add(toolchainName,toolchainID); - } - if(debug_mode){ - LOGGER.info("TOOLCHAIN LIST:" + toolchainList); - LOGGER.info("#######################"); - } - if(toolchainList.isEmpty()) { - if(debug_mode){ - LOGGER.info("RETURNED NO TOOLCHAINS."); - } - return emptybox; - } - return toolchainList; - } else { - LOGGER.info("RETURNED STATUS CODE OTHER THAN 200. RESPONSE: " + response.getStatusLine().toString()); - return emptybox; - } - - } catch (Exception e) { - e.printStackTrace(); - } - - return emptybox; - } - - public static String getOrgId(String token, String orgName, String environment, Boolean debug_mode) { - CloseableHttpClient httpClient = HttpClients.createDefault(); - String organizations_url = chooseOrganizationsUrl(environment); - if(debug_mode){ - LOGGER.info("GET ORG_GUID URL:" + organizations_url + orgName); - } - - try { - HttpGet httpGet = new HttpGet(organizations_url + URLEncoder.encode(orgName, "UTF-8").replaceAll("\\+", "%20")); - - httpGet = addProxyInformation(httpGet); - - httpGet.setHeader("Authorization", token); - CloseableHttpResponse response = null; - - response = httpClient.execute(httpGet); - String resStr = EntityUtils.toString(response.getEntity()); - - if(debug_mode){ - LOGGER.info("RESPONSE FROM ORGANIZATIONS API:" + response.getStatusLine().toString()); - } - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject obj = element.getAsJsonObject(); - JsonArray resources = obj.getAsJsonArray("resources"); - - if(resources.size() > 0) { - JsonObject resource = resources.get(0).getAsJsonObject(); - JsonObject metadata = resource.getAsJsonObject("metadata"); - if(debug_mode){ - LOGGER.info("ORG_ID:" + String.valueOf(metadata.get("guid")).replaceAll("\"", "")); - } - return String.valueOf(metadata.get("guid")).replaceAll("\"", ""); - } - else { - if(debug_mode){ - LOGGER.info("RETURNED NO ORGANIZATIONS."); - } - return null; - } - - } else { - if(debug_mode){ - LOGGER.info("RETURNED STATUS CODE OTHER THAN 200. RESPONSE: " + response.getStatusLine().toString()); - } - return null; - } - - } catch (Exception e) { - e.printStackTrace(); - } - - return null; - } - - public static String getSpaceId(String token, String spaceName, String environment, Boolean debug_mode) { - CloseableHttpClient httpClient = HttpClients.createDefault(); - String spaces_url = chooseSpacesUrl(environment); - if(debug_mode){ - LOGGER.info("GET SPACE_GUID URL:" + spaces_url + spaceName); - } - - try { - HttpGet httpGet = new HttpGet(spaces_url + URLEncoder.encode(spaceName, "UTF-8").replaceAll("\\+", "%20")); - - httpGet = addProxyInformation(httpGet); - - httpGet.setHeader("Authorization", token); - CloseableHttpResponse response = null; - - response = httpClient.execute(httpGet); - String resStr = EntityUtils.toString(response.getEntity()); - - if(debug_mode){ - LOGGER.info("RESPONSE FROM SPACES API:" + response.getStatusLine().toString()); - } - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject obj = element.getAsJsonObject(); - JsonArray resources = obj.getAsJsonArray("resources"); - - if(resources.size() > 0) { - JsonObject resource = resources.get(0).getAsJsonObject(); - JsonObject metadata = resource.getAsJsonObject("metadata"); - if(debug_mode){ - LOGGER.info("SPACE_ID:" + String.valueOf(metadata.get("guid")).replaceAll("\"", "")); - } - return String.valueOf(metadata.get("guid")).replaceAll("\"", ""); - } - else { - if(debug_mode){ - LOGGER.info("RETURNED NO SPACES."); - } - return null; - } - - } else { - if(debug_mode){ - LOGGER.info("RETURNED STATUS CODE OTHER THAN 200. RESPONSE: " + response.getStatusLine().toString()); - } - return null; - } - - } catch (Exception e) { - e.printStackTrace(); - } - - return null; - } - - public static String getAppId(String token, String appName, String orgName, String spaceName, String environment, Boolean debug_mode) { - CloseableHttpClient httpClient = HttpClients.createDefault(); - String apps_url = chooseAppsUrl(environment); - if(debug_mode){ - LOGGER.info("GET APPS_GUID URL:" + apps_url + appName + ORG + orgName + SPACE + spaceName); - } - - try { - HttpGet httpGet = new HttpGet(apps_url + URLEncoder.encode(appName, "UTF-8").replaceAll("\\+", "%20") + ORG + URLEncoder.encode(orgName, "UTF-8").replaceAll("\\+", "%20") + SPACE + URLEncoder.encode(spaceName, "UTF-8").replaceAll("\\+", "%20")); - - httpGet = addProxyInformation(httpGet); - - httpGet.setHeader("Authorization", token); - CloseableHttpResponse response = null; - - response = httpClient.execute(httpGet); - String resStr = EntityUtils.toString(response.getEntity()); - - if(debug_mode){ - LOGGER.info("RESPONSE FROM APPS API:" + response.getStatusLine().toString()); - } - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject obj = element.getAsJsonObject(); - JsonArray resources = obj.getAsJsonArray("resources"); - - if(resources.size() > 0) { - JsonObject resource = resources.get(0).getAsJsonObject(); - JsonObject metadata = resource.getAsJsonObject("metadata"); - if(debug_mode){ - LOGGER.info("APP_ID:" + String.valueOf(metadata.get("guid")).replaceAll("\"", "")); - } - return String.valueOf(metadata.get("guid")).replaceAll("\"", ""); - } - else { - if(debug_mode){ - LOGGER.info("RETURNED NO APPS."); - } - return null; - } - - } else { - if(debug_mode){ - LOGGER.info("RETURNED STATUS CODE OTHER THAN 200. RESPONSE: " + response.getStatusLine().toString()); - } - return null; - } - - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - /** - * Get a list of policies that belong to an org - * @param token - * @param orgName - * @return - */ - - public static ListBoxModel getPolicyList(String token, String orgName, String toolchainName, String environment, Boolean debug_mode) { - - // get all jenkins job - ListBoxModel emptybox = new ListBoxModel(); - emptybox.add("","empty"); - - String url = choosePoliciesUrl(environment); - - - try { - url = url.replace("{org_name}", URLEncoder.encode(orgName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{toolchain_name}", URLEncoder.encode(toolchainName, "UTF-8").replaceAll("\\+", "%20")); - if(debug_mode){ - LOGGER.info("GET POLICIES URL:" + url); - } - - CloseableHttpClient httpClient = HttpClients.createDefault(); - HttpGet httpGet = new HttpGet(url); - - httpGet = addProxyInformation(httpGet); - - httpGet.setHeader("Authorization", token); - CloseableHttpResponse response = null; - response = httpClient.execute(httpGet); - String resStr = EntityUtils.toString(response.getEntity()); - - if(debug_mode){ - LOGGER.info("RESPONSE FROM GET POLICIES URL:" + response.getStatusLine().toString()); - } - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonArray jsonArray = element.getAsJsonArray(); - - ListBoxModel model = new ListBoxModel(); - - for (int i = 0; i < jsonArray.size(); i++) { - JsonObject obj = jsonArray.get(i).getAsJsonObject(); - String name = String.valueOf(obj.get("name")).replaceAll("\"", ""); - model.add(name, name); - } - if(debug_mode){ - LOGGER.info("POLICY LIST:" + model); - LOGGER.info("#######################"); - } - return model; - } else { - if(debug_mode){ - LOGGER.info("RETURNED STATUS CODE OTHER THAN 200. RESPONSE: " + response.getStatusLine().toString()); - } - return emptybox; - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return emptybox; - } - - /** - * write to the environment variables to pass to next build step - * @param build - the current build - * @param bluemixToken - the Bluemix Token - * @param buildId - the build number of the build job in the Jenkins - */ - public static void passEnvToNextBuildStep (Run build, final String bluemixToken, final String buildId) { - - build.addAction(new EnvironmentContributingAction() { - @Override - public String getIconFileName() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public String getUrlName() { - return null; - } - - public void buildEnvVars(AbstractBuild build, EnvVars envVars) { - if (envVars != null) { - if (!Util.isNullOrEmpty(bluemixToken)) { - envVars.put("DI_BM_TOKEN", bluemixToken); - } - - if (!Util.isNullOrEmpty(buildId)) { - envVars.put("DI_BUILD_ID", buildId); - } - } - } - } - ); - } - - public static HttpGet addProxyInformation (HttpGet instance) { - /* Add proxy to request if proxy settings in Jenkins UI are set. */ - ProxyConfiguration proxyConfig = Jenkins.getInstance().proxy; - if(proxyConfig != null){ - if((!Util.isNullOrEmpty(proxyConfig.name)) && proxyConfig.port != 0) { - HttpHost proxy = new HttpHost(proxyConfig.name, proxyConfig.port, "http"); - RequestConfig config = RequestConfig.custom().setProxy(proxy).build(); - instance.setConfig(config); - } - } - return instance; - } - - public static HttpPost addProxyInformation (HttpPost instance) { - /* Add proxy to request if proxy settings in Jenkins UI are set. */ - ProxyConfiguration proxyConfig = Jenkins.getInstance().proxy; - if(proxyConfig != null){ - if((!Util.isNullOrEmpty(proxyConfig.name)) && proxyConfig.port != 0) { - HttpHost proxy = new HttpHost(proxyConfig.name, proxyConfig.port, "http"); - RequestConfig config = RequestConfig.custom().setProxy(proxy).build(); - instance.setConfig(config); - } - } - return instance; - } - -} diff --git a/src/main/java/com/ibm/devops/dra/AbstractGateAction.java b/src/main/java/com/ibm/devops/dra/AbstractGateAction.java deleted file mode 100644 index 702aeab..0000000 --- a/src/main/java/com/ibm/devops/dra/AbstractGateAction.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import hudson.model.AbstractBuild; -import hudson.model.Action; - -/** - * Abstract Decision Text Action - */ -public abstract class AbstractGateAction implements Action { - public abstract Boolean getShowHeader(); - - public abstract String getText(); - - public abstract AbstractBuild getBuild(); - - @Override - public String getIconFileName() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public String getUrlName() { - return null; - } - -} diff --git a/src/main/java/com/ibm/devops/dra/BuildInfoModel.java b/src/main/java/com/ibm/devops/dra/BuildInfoModel.java deleted file mode 100644 index 58fc40f..0000000 --- a/src/main/java/com/ibm/devops/dra/BuildInfoModel.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -public class BuildInfoModel { - private String build_id; - private String job_url; - private String status; - private String timestamp; - private Repo repository; - - public BuildInfoModel(String build_id, String job_url, String status, String timestamp, Repo repository) { - this.build_id = build_id; - this.job_url = job_url; - this.status = status; - this.timestamp = timestamp; - this.repository = repository; - } - - public String getBuild_id() { - return build_id; - } - - public String getJob_url() { - return job_url; - } - - public String getStatus() { - return status; - } - - public String getTimestamp() { - return timestamp; - } - - public Repo getRepository() { - return repository; - } - - public static class Repo { - private String repository_url; - private String branch; - private String commit_id; - - public Repo(String repository_url, String branch, String commit_id) { - this.repository_url = repository_url; - this.branch = branch; - this.commit_id = commit_id; - } - - public String getRepository_url() { - return repository_url; - } - - public String getBranch() { - return branch; - } - - public String getCommit_id() { - return commit_id; - } - } - - -} diff --git a/src/main/java/com/ibm/devops/dra/BuildPublisherAction.java b/src/main/java/com/ibm/devops/dra/BuildPublisherAction.java deleted file mode 100644 index 6f20075..0000000 --- a/src/main/java/com/ibm/devops/dra/BuildPublisherAction.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import hudson.model.Action; - -public class BuildPublisherAction implements Action { - - private final String link; - - public BuildPublisherAction(String link) { - this.link = link; - } - - public String getLink() { - return link; - } - - @Override - public String getIconFileName() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public String getUrlName() { - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/DeploymentInfoModel.java b/src/main/java/com/ibm/devops/dra/DeploymentInfoModel.java deleted file mode 100644 index e4aacc2..0000000 --- a/src/main/java/com/ibm/devops/dra/DeploymentInfoModel.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -public class DeploymentInfoModel { - private String app_url; - private String environment_name; - private String job_url; - private String status; - private String timestamp; - - public DeploymentInfoModel(String app_url, String environment_name, String job_url, String status, String timestamp) { - this.app_url = app_url; - this.environment_name = environment_name; - this.job_url = job_url; - this.status = status; - this.timestamp = timestamp; - } - - public String getApp_url() { - return app_url; - } - - public String getEnvironment_name() { - return environment_name; - } - - public String getJob_url() { - return job_url; - } - - public String getStatus() { - return status; - } - - public String getTimestamp() { - return timestamp; - } -} diff --git a/src/main/java/com/ibm/devops/dra/EnvironmentScope.java b/src/main/java/com/ibm/devops/dra/EnvironmentScope.java deleted file mode 100644 index e92d190..0000000 --- a/src/main/java/com/ibm/devops/dra/EnvironmentScope.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import hudson.Extension; -import hudson.model.AbstractDescribableImpl; -import hudson.model.Descriptor; -import org.kohsuke.stapler.DataBoundConstructor; - -public class EnvironmentScope extends AbstractDescribableImpl { - private boolean isBuild; - private boolean isAll; - private boolean isDeploy; - private String branchName; - private String envName; - - @DataBoundConstructor - public EnvironmentScope(String value, String branchName, String envName) { - switch (value) { - case "build": - this.isBuild = true; - this.isDeploy = false; - this.isAll = false; - break; - case "deploy": - this.isDeploy = true; - this.isBuild = false; - this.isAll = false; - break; - default: - this.isAll = true; - this.isBuild = false; - this.isDeploy = false; - break; - } - - this.branchName = branchName; - this.envName = envName; - } - - public boolean isBuild() { - return isBuild; - } - - public boolean isAll() { - return isAll; - } - - public boolean isDeploy() { - return isDeploy; - } - - public String getBranchName() { - return branchName; - } - - public void setBranchName(String branchName) { - this.branchName = branchName; - } - - public String getEnvName() { - return envName; - } - - public void setEnvName(String envName) { - this.envName = envName; - } - - - @Extension - public static class DescriptorImpl extends Descriptor { - @Override - public String getDisplayName() { - return "DevOps Insight Test Environment Scope"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/EvaluateGate.java b/src/main/java/com/ibm/devops/dra/EvaluateGate.java deleted file mode 100644 index d4a23ce..0000000 --- a/src/main/java/com/ibm/devops/dra/EvaluateGate.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import hudson.*; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Publisher; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.tasks.SimpleBuildStep; -import net.sf.json.JSONObject; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.kohsuke.stapler.AncestorInPath; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; - -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.IOException; -import java.io.PrintStream; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.List; - - -/** - * Customized build step to get a gate decision from DRA backend - */ - -public class EvaluateGate extends AbstractDevOpsAction implements SimpleBuildStep{ - - private final static String CONTENT_TYPE = "application/json"; - - // form fields from UI - private final String policyName; - private String orgName; - private String buildJobName; - private String applicationName; - private String toolchainName; - private String environmentName; - private String credentialsId; - private boolean willDisrupt; - - private EnvironmentScope scope; - private String envName; - private boolean isDeploy; - - private String draUrl; - private PrintStream printStream; - private static String bluemixToken; - private static String preCredentials; - - //fields to support jenkins pipeline - private String username; - private String password; - // optional customized build number - private String buildNumber; - - // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" - @DataBoundConstructor - public EvaluateGate(String policyName, - String orgName, - String applicationName, - String toolchainName, - String environmentName, - String buildJobName, - String credentialsId, - boolean willDisrupt, - EnvironmentScope scope, - OptionalBuildInfo additionalBuildInfo) { - this.policyName = policyName; - this.orgName = orgName; - this.applicationName = applicationName; - this.toolchainName = toolchainName; - this.environmentName = environmentName; - this.buildJobName = buildJobName; - this.credentialsId = credentialsId; - this.willDisrupt = willDisrupt; - this.scope = scope; - this.envName = scope.getEnvName(); - this.isDeploy = scope.isDeploy(); - if (additionalBuildInfo == null) { - this.buildNumber = null; - } else { - this.buildNumber = additionalBuildInfo.buildNumber; - } - } - - public EvaluateGate(HashMap envVarsMap, - String policyName, - String environmentName, - boolean willDisrupt) { - - this.applicationName = envVarsMap.get(APP_NAME); - this.orgName = envVarsMap.get(ORG_NAME); - this.toolchainName = envVarsMap.get(TOOLCHAIN_ID); - this.username = envVarsMap.get(USERNAME); - this.password = envVarsMap.get(PASSWORD); - this.envName = environmentName; - this.willDisrupt = willDisrupt; - this.policyName = policyName; - } - - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - /** - * We'll use this from the config.jelly. - */ - - public String getPolicyName() { - return policyName; - } - - public String getOrgName() { - return orgName; - } - - public String getBuildJobName() { - return buildJobName; - } - - public String getApplicationName() { - return applicationName; - } - - public String getToolchainName() { - return toolchainName; - } - - public String getEnvironmentName() { - return environmentName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public boolean isWillDisrupt() { - return willDisrupt; - } - - public EnvironmentScope getScope() { - return scope; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getEnvName() { - return envName; - } - - public boolean isDeploy() { - return isDeploy; - } - - public static class OptionalBuildInfo { - private String buildNumber; - - @DataBoundConstructor - public OptionalBuildInfo(String buildNumber, String buildUrl) { - this.buildNumber = buildNumber; - } - } - - /** - * Override this method to get your operation done in the build step. When invoked, it is up to you, as a plugin developer - * to add your actions, and/or perform the operations required by your plugin in this build step. Equally, it is up - * to the developer to make the code run on the slave(master or an actual remote). This must be done given the builds - * workspace, as in build.getWorkspace(). The workspace is the link to the slave, as it is the representation of the - * remote file system. - * - * Build steps as you add them to your job configuration are executed sequentially, and the return value for your - * builder should indicate whether to execute the next build step in the list. - * @param build - the current build - * @param launcher - the launcher - * @param listener - the build listener - * @throws InterruptedException - * @throws IOException - */ - @Override - public void perform(@Nonnull Run build, @Nonnull FilePath filePath, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { - // This is where you 'build' the project. - printStream = listener.getLogger(); - printPluginVersion(this.getClass().getClassLoader(), printStream); - - // Get the project name and build id from environment - EnvVars envVars = build.getEnvironment(listener); - this.orgName = envVars.expand(this.orgName); - this.applicationName = envVars.expand(this.applicationName); - this.toolchainName = envVars.expand(this.toolchainName); - - if (this.isDeploy || !Util.isNullOrEmpty(this.envName)) { - this.environmentName = envVars.expand(this.envName); - } - - // verify if user chooses advanced option to input customized DRA - String env = getDescriptor().getEnvironment(); - this.draUrl = chooseDRAUrl(env); - String targetAPI = chooseTargetAPI(env); - - String buildNumber; - if (Util.isNullOrEmpty(this.buildNumber)) { - // locate the build job that triggers current build - Run triggeredBuild = getTriggeredBuild(build, buildJobName, envVars, printStream); - if (triggeredBuild == null) { - //failed to find the build job - return; - } else { - if (Util.isNullOrEmpty(this.buildJobName)) { - // handle the case which the build job name left empty, and the pipeline case - this.buildJobName = envVars.get("JOB_NAME"); - } - buildNumber = getBuildNumber(buildJobName, triggeredBuild); - } - } else { - buildNumber = envVars.expand(this.buildNumber); - } - - String bluemixToken; - // get the Bluemix token - try { - if (Util.isNullOrEmpty(this.credentialsId)) { - bluemixToken = getBluemixToken(username, password, targetAPI); - } else { - bluemixToken = getBluemixToken(build.getParent(), this.credentialsId, targetAPI); - } - - printStream.println("[IBM Cloud DevOps] Log in successfully, get the Bluemix token"); - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Username/Password is not correct, fail to authenticate with Bluemix"); - printStream.println("[IBM Cloud DevOps]" + e.toString()); - return; - } - - // get decision response from DRA - try { - JsonObject decisionJson = getDecisionFromDRA(bluemixToken, buildNumber); - if (decisionJson == null) { - printStream.println("[IBM Cloud DevOps] get empty decision"); - return; - } - - // retrieve the decision id to compose the report link - String decisionId = String.valueOf(decisionJson.get("decision_id")); - // remove the double quotes - decisionId = decisionId.replace("\"",""); - - // Show Proceed or Failed based on the decision - String decision = String.valueOf(decisionJson.get("contents").getAsJsonObject().get("proceed")); - if (decision.equals("true")) { - decision = "Succeed"; - } else { - decision = "Failed"; - } - - String cclink = chooseControlCenterUrl(env) + "deploymentrisk?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" + this.toolchainName; - String reportUrl = chooseControlCenterUrl(env) + "decisionreport?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" - + URLEncoder.encode(toolchainName, "UTF-8") + "&reportId=" + decisionId; - - GatePublisherAction action = new GatePublisherAction(reportUrl, cclink, decision, this.policyName, build); - build.addAction(action); - - printStream.println("************************************"); - printStream.println("Check IBM Cloud DevOps Gate Evaluation report here -" + reportUrl); - // console output for a "fail" decision - if (decision.equals("Failed")) { - printStream.println("IBM Cloud DevOps decision to proceed is: false"); - printStream.println("************************************"); - if (willDisrupt) { - Result result = Result.FAILURE; - build.setResult(result); - throw new AbortException("Decision is fail"); - } - return; - } - - // console output for a "proceed" decision - printStream.println("IBM Cloud DevOps decision to proceed is: true"); - printStream.println("************************************"); - return; - - } catch (IOException e) { - if (e instanceof AbortException) { - throw new AbortException("Decision is fail"); - } else { - printStream.print("[IBM Cloud DevOps] Error: " + e.getMessage()); - } - } - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - /** - * Send a request to DRA backend to get a decision - * @param buildId - build ID, get from Jenkins environment - * @return - the response decision Json file - */ - private JsonObject getDecisionFromDRA(String bluemixToken, String buildId) throws IOException { - // create http client and post method - CloseableHttpClient httpClient = HttpClients.createDefault(); - String url = this.draUrl; - url = url + "/organizations/" + URLEncoder.encode(orgName, "UTF-8").replaceAll("\\+", "%20") + - "/toolchainids/" + URLEncoder.encode(toolchainName, "UTF-8").replaceAll("\\+", "%20") + - "/buildartifacts/" + URLEncoder.encode(applicationName, "UTF-8").replaceAll("\\+", "%20") + - "/builds/" + URLEncoder.encode(buildId, "UTF-8").replaceAll("\\+", "%20") + - "/policies/" + URLEncoder.encode(policyName, "UTF-8").replaceAll("\\+", "%20") + - "/decisions"; - if (!Util.isNullOrEmpty(this.environmentName)) { - url = url.concat("?environment_name=" + environmentName); - } - - HttpPost postMethod = new HttpPost(url); - - postMethod = addProxyInformation(postMethod); - postMethod.setHeader("Authorization", bluemixToken); - postMethod.setHeader("Content-Type", CONTENT_TYPE); - - CloseableHttpResponse response = httpClient.execute(postMethod); - String resStr = EntityUtils.toString(response.getEntity()); - - try { - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - printStream.println("[IBM Cloud DevOps] Get decision successfully"); - return resJson; - } else { - // if gets error status - printStream.println("[IBM Cloud DevOps] Error: Failed to get a decision, response status " + response.getStatusLine()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("message")) { - printStream.println("[IBM Cloud DevOps] Reason: " + resJson.get("message")); - } - } - } catch (JsonSyntaxException e) { - printStream.println("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); - } - - return null; - } - - - @Override - public EvaluateGateImpl getDescriptor() { - return (EvaluateGateImpl)super.getDescriptor(); - } - - /** - * Descriptor for {@link EvaluateGate}. Used as a singleton. - * The class is marked as public so that it can be accessed from views. - * - *

- * See src/main/resources/hudson/plugins/hello_world/HelloWorldBuilder/*.jelly - * for the actual HTML fragment for the configuration screen. - */ - @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class EvaluateGateImpl extends BuildStepDescriptor { - - /** - * In order to load the persisted global configuration, you have to - * call load() in the constructor. - */ - public EvaluateGateImpl() { - load(); - } - - /** - * Performs on-the-fly validation of the form field 'name'. - * - * @param value - * This parameter receives the value that the user has typed. - * @return - * Indicates the outcome of the validation. This is sent to the browser. - *

- * Note that returning {@link FormValidation#error(String)} does not - * prevent the form from being saved. It just means that a message - * will be displayed to the user. - */ - public FormValidation doCheckOrgName(@QueryParameter String value) - throws IOException, ServletException { - - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckApplicationName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckEnvironmentName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckToolchainName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Could not retrieve list of toolchains. Please check your username and password. If you have not created a toolchain, create one here."); - } - return FormValidation.ok(); - } - - public FormValidation doCheckPolicyName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Fail to get the policies, please check your username/password or org name and make sure you have created policies for this org and toolchain."); - } - return FormValidation.ok(); - } - - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - preCredentials = credentialsId; - try { - String bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - if (Util.isNullOrEmpty(bluemixToken)) { - EvaluateGate.bluemixToken = bluemixToken; - return FormValidation.warning("Got empty token"); - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } catch (Exception e) { - return FormValidation.error("Failed to log in to Bluemix, please check your username/password"); - } - } else { - - return FormValidation.okWithMarkup("Connection successful"); - } - } - - /** - * This method is called to populate the credentials list on the Jenkins config page. - */ - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, - @QueryParameter("target") final String target) { - StandardListBoxModel result = new StandardListBoxModel(); - result.includeEmptyValue(); - result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(target).build() - ) - ); - return result; - } - - /** - * Autocompletion for build job name field - * @param value - * @return - */ - public AutoCompletionCandidates doAutoCompleteBuildJobName(@QueryParameter String value) { - AutoCompletionCandidates auto = new AutoCompletionCandidates(); - - // get all jenkins job - List jobs = Jenkins.getInstance().getAllItems(Job.class); - for (int i = 0; i < jobs.size(); i++) { - String jobName = jobs.get(i).getName(); - - if (jobName.toLowerCase().startsWith(value.toLowerCase())) { - auto.add(jobName); - } - } - - return auto; - } - - /** - * This method is called to populate the policy list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillPolicyNameItems(@AncestorInPath ItemGroup context, - @QueryParameter final String orgName, - @QueryParameter final String toolchainName, - @QueryParameter final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - // if user changes to a different credential, need to get a new token - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - preCredentials = credentialsId; - } - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######GATE : calling getPolicyList#######"); - } - return getPolicyList(bluemixToken, orgName, toolchainName, environment, isDebug_mode()); - - } - - /** - * This method is called to populate the toolchain list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillToolchainNameItems(@AncestorInPath ItemGroup context, - @QueryParameter final String orgName, - @QueryParameter final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######GATE : calling getToolchainList#######"); - } - return getToolchainList(bluemixToken, orgName, environment, isDebug_mode()); - } - - - /** - * Required Method - * This is used to determine if this build step is applicable for your chosen project type. (FreeStyle, MultiConfiguration, Maven) - * Some plugin build steps might be made to be only available to MultiConfiguration projects. - * - * @param aClass The current project - * @return a boolean indicating whether this build step can be chose given the project type - */ - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project types - // return FreeStyleProject.class.isAssignableFrom(aClass); - return true; - } - - /** - * Required Method - * @return The text to be displayed when selecting your build in the project - */ - public String getDisplayName() { - return "IBM Cloud DevOps Gate"; - } - - public String getEnvironment() { - return getEnv(Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getConsoleUrl()); - } - - public boolean isDebug_mode() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).isDebug_mode(); - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/GatePublisherAction.java b/src/main/java/com/ibm/devops/dra/GatePublisherAction.java deleted file mode 100644 index d4293e7..0000000 --- a/src/main/java/com/ibm/devops/dra/GatePublisherAction.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import hudson.model.Action; -import hudson.model.Run; - -/** - * DRA action for builds, show the decision and report link in the build status page - */ -public class GatePublisherAction implements Action { - - private final String text; - private final String riskDashboardLink; - private final String decision; - private final String policyName; - private final Run build; - - public GatePublisherAction(String text, String riskDashboardLink, String decision, String policyName, Run build) { - this.text = text; - this.riskDashboardLink = riskDashboardLink; - this.decision = decision; - this.policyName = policyName; - this.build = build; - } - - public String getText() { - return text; - } - - public String getRiskDashboardLink() { - return riskDashboardLink; - } - - public String getDecision() { - return decision; - } - - public String getPolicyName() { - return policyName; - } - - public Run getBuild() { - return build; - } - - @Override - public String getIconFileName() { - return null; - } - - @Override - public String getDisplayName() { - return null; - } - - @Override - public String getUrlName() { - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/PublishBuild.java b/src/main/java/com/ibm/devops/dra/PublishBuild.java deleted file mode 100644 index 7c2006a..0000000 --- a/src/main/java/com/ibm/devops/dra/PublishBuild.java +++ /dev/null @@ -1,502 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.gson.*; -import hudson.*; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Publisher; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.tasks.SimpleBuildStep; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.kohsuke.stapler.*; - -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.*; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.TimeZone; -import java.net.URLEncoder; - -public class PublishBuild extends AbstractDevOpsAction implements SimpleBuildStep { - - private static String BUILD_API_URL = "/organizations/{org_name}/toolchainids/{toolchain_id}/buildartifacts/{build_artifact}/builds"; - private final static String CONTENT_TYPE_JSON = "application/json"; - private final static String CONTENT_TYPE_XML = "application/xml"; - - // form fields from UI - private String applicationName; - private String orgName; - private String credentialsId; - private String toolchainName; - - private String dlmsUrl; - private PrintStream printStream; - private File root; - private static String bluemixToken; - private static String preCredentials; - - // fields to support jenkins pipeline - private String result; - private String gitRepo; - private String gitBranch; - private String gitCommit; - private String username; - private String password; - // optional customized build number - private String buildNumber; - - - @DataBoundConstructor - public PublishBuild(String applicationName, String orgName, String credentialsId, String toolchainName, OptionalBuildInfo additionalBuildInfo) { - this.credentialsId = credentialsId; - this.applicationName = applicationName; - this.orgName = orgName; - this.toolchainName = toolchainName; - if (additionalBuildInfo == null) { - this.buildNumber = null; - } else { - this.buildNumber = additionalBuildInfo.buildNumber; - } - } - - public PublishBuild(HashMap envVarsMap, HashMap paramsMap) { - this.gitRepo = paramsMap.get("gitRepo"); - this.gitBranch = paramsMap.get("gitBranch"); - this.gitCommit = paramsMap.get("gitCommit"); - this.result = paramsMap.get("result"); - this.applicationName = envVarsMap.get(APP_NAME); - this.orgName = envVarsMap.get(ORG_NAME); - this.toolchainName = envVarsMap.get(TOOLCHAIN_ID); - this.username = envVarsMap.get(USERNAME); - this.password = envVarsMap.get(PASSWORD); - } - - @DataBoundSetter - public void setApplicationName(String applicationName) { - this.applicationName = applicationName; - } - - @DataBoundSetter - public void setOrgName(String orgName) { - this.orgName = orgName; - } - - @DataBoundSetter - public void setCredentialsId(String credentialsId) { - this.credentialsId = credentialsId; - } - - @DataBoundSetter - public void setToolchainName(String toolchainName) { - this.toolchainName = toolchainName; - } - - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - /** - * We'll use this from the config.jelly. - */ - public String getApplicationName() { - return applicationName; - } - - public String getOrgName() { - return orgName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getToolchainName() { - return toolchainName; - } - - public String getBuildNumber() { - return buildNumber; - } - - public static class OptionalBuildInfo { - private String buildNumber; - - @DataBoundConstructor - public OptionalBuildInfo(String buildNumber, String buildUrl) { - this.buildNumber = buildNumber; - } - } - - @Override - public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { - - printStream = listener.getLogger(); - printPluginVersion(this.getClass().getClassLoader(), printStream); - - // create root dir for storing test result - root = new File(build.getRootDir(), "DRA_TestResults"); - - // Get the project name and build id from environment - EnvVars envVars = build.getEnvironment(listener); - - // verify if user chooses advanced option to input customized DLMS - String env = getDescriptor().getEnvironment(); - this.dlmsUrl = chooseDLMSUrl(env) + BUILD_API_URL; - String targetAPI = chooseTargetAPI(env); - - //expand the variables - this.orgName = envVars.expand(this.orgName); - this.applicationName = envVars.expand(this.applicationName); - this.toolchainName = envVars.expand(this.toolchainName); - - // Check required parameters - if (Util.isNullOrEmpty(orgName) || Util.isNullOrEmpty(applicationName) || Util.isNullOrEmpty(toolchainName)) { - printStream.println("[IBM Cloud DevOps] Missing few required configurations"); - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Build Info."); - return; - } - - String bluemixToken; - // get the Bluemix token - try { - if (Util.isNullOrEmpty(this.credentialsId)) { - bluemixToken = getBluemixToken(username, password, targetAPI); - } else { - bluemixToken = getBluemixToken(build.getParent(), this.credentialsId, targetAPI); - } - - printStream.println("[IBM Cloud DevOps] Log in successfully, get the Bluemix token"); - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Username/Password is not correct, fail to authenticate with Bluemix"); - printStream.println("[IBM Cloud DevOps]" + e.toString()); - return; - } - - String link = chooseControlCenterUrl(env) + "deploymentrisk?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" + this.toolchainName; - if (uploadBuildInfo(bluemixToken, build, envVars)) { - printStream.println("[IBM Cloud DevOps] Go to Control Center (" + link + ") to check your build status"); - BuildPublisherAction action = new BuildPublisherAction(link); - build.addAction(action); - } - } - - /** - * Construct the Git data model - * @param envVars - * @return - */ - public BuildInfoModel.Repo buildGitRepo(EnvVars envVars) { - String repoUrl = envVars.get("GIT_URL"); - String branch = envVars.get("GIT_BRANCH"); - String commitId = envVars.get("GIT_COMMIT"); - - repoUrl = Util.isNullOrEmpty(repoUrl) ? this.gitRepo : repoUrl; - branch = Util.isNullOrEmpty(branch) ? this.gitBranch : branch; - commitId = Util.isNullOrEmpty(commitId) ? this.gitCommit : commitId; - if (!Util.isNullOrEmpty(branch)) { - String[] parts = branch.split("/"); - branch = parts[parts.length - 1]; - } - - BuildInfoModel.Repo repo = new BuildInfoModel.Repo(repoUrl, branch, commitId); - return repo; - } - - /** - * Upload the build information to DLMS - API V2. - * @param bluemixToken - * @param build - * @param envVars - * @throws IOException - */ - private boolean uploadBuildInfo(String bluemixToken, Run build, EnvVars envVars) { - String resStr = ""; - - try { - CloseableHttpClient httpClient = HttpClients.createDefault(); - String url = this.dlmsUrl; - url = url.replace("{org_name}", URLEncoder.encode(this.orgName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{toolchain_id}", URLEncoder.encode(this.toolchainName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{build_artifact}", URLEncoder.encode(this.applicationName, "UTF-8").replaceAll("\\+", "%20")); - - String buildNumber; - if (Util.isNullOrEmpty(this.buildNumber)) { - buildNumber = getBuildNumber(envVars.get("JOB_NAME"), build); - } else { - buildNumber = envVars.expand(this.buildNumber); - } - - String buildUrl; - if (checkRootUrl(printStream)) { - buildUrl = Jenkins.getInstance().getRootUrl() + build.getUrl(); - } else { - buildUrl = build.getAbsoluteUrl(); - } - HttpPost postMethod = new HttpPost(url); - postMethod = addProxyInformation(postMethod); - postMethod.setHeader("Authorization", bluemixToken); - postMethod.setHeader("Content-Type", CONTENT_TYPE_JSON); - - String buildStatus; - Result result = build.getResult(); - if ((result != null && result.equals(Result.SUCCESS)) - || (this.result != null && this.result.equals(RESULT_SUCCESS))) { - buildStatus = "pass"; - } else { - buildStatus = "fail"; - } - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - TimeZone utc = TimeZone.getTimeZone("UTC"); - dateFormat.setTimeZone(utc); - String timestamp = dateFormat.format(System.currentTimeMillis()); - - // build up the json body - Gson gson = new Gson(); - BuildInfoModel.Repo repo = buildGitRepo(envVars); - BuildInfoModel buildInfo = new BuildInfoModel(buildNumber, buildUrl, buildStatus, timestamp, repo); - - String json = gson.toJson(buildInfo); - StringEntity data = new StringEntity(json); - postMethod.setEntity(data); - CloseableHttpResponse response = httpClient.execute(postMethod); - resStr = EntityUtils.toString(response.getEntity()); - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - printStream.println("[IBM Cloud DevOps] Upload Build Information successfully"); - return true; - - } else { - // if gets error status - printStream.println("[IBM Cloud DevOps] Error: Failed to upload, response status " + response.getStatusLine()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("user_error")) { - printStream.println("[IBM Cloud DevOps] Reason: " + resJson.get("user_error")); - } - } - } catch (JsonSyntaxException e) { - printStream.println("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); - } catch (IllegalStateException e) { - // will be triggered when 403 Forbidden - try { - printStream.println("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); - } catch (UnsupportedEncodingException e1) { - e1.printStackTrace(); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - - // Overridden for better type safety. - // If your plugin doesn't really define any property on Descriptor, - // you don't have to do this. - @Override - public PublishBuildActionImpl getDescriptor() { - return (PublishBuildActionImpl)super.getDescriptor(); - } - - /** - * Descriptor for {@link PublishBuild}. Used as a singleton. - * The class is marked as public so that it can be accessed from views. - * - *

- * See src/main/resources/com/ibm/devops/dra/PublishBuild/*.jelly - * for the actual HTML fragment for the configuration screen. - */ - @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class PublishBuildActionImpl extends BuildStepDescriptor { - /** - * To persist global configuration information, - * simply store it in a field and call save(). - * - *

- * If you don't want fields to be persisted, use transient. - */ - - /** - * In order to load the persisted global configuration, you have to - * call load() in the constructor. - */ - public PublishBuildActionImpl() { - super(PublishBuild.class); - load(); - } - - /** - * Performs on-the-fly validation of the form field 'credentialId'. - * - * @param value - * This parameter receives the value that the user has typed. - * @return - * Indicates the outcome of the validation. This is sent to the browser. - *

- * Note that returning {@link FormValidation#error(String)} does not - * prevent the form from being saved. It just means that a message - * will be displayed to the user. - */ - - public FormValidation doCheckOrgName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckToolchainName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Could not retrieve list of toolchains. Please check your username and password. If you have not created a toolchain, create one here."); - } - return FormValidation.ok(); - } - - public FormValidation doCheckApplicationName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckEnvironmentName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - preCredentials = credentialsId; - try { - String newToken = getBluemixToken(context, credentialsId, targetAPI); - if (Util.isNullOrEmpty(newToken)) { - bluemixToken = newToken; - return FormValidation.warning("Got empty token"); - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } catch (Exception e) { - return FormValidation.error("Failed to log in to Bluemix, please check your username/password"); - } - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } - - /** - * This method is called to populate the credentials list on the Jenkins config page. - */ - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, - @QueryParameter("target") final String target) { - StandardListBoxModel result = new StandardListBoxModel(); - result.includeEmptyValue(); - result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(target).build() - ) - ); - return result; - } - - /** - * This method is called to populate the toolchain list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillToolchainNameItems(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("orgName") final String orgName) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######UPLOAD BUILD INFO : calling getToolchainList#######"); - } - ListBoxModel toolChainListBox = getToolchainList(bluemixToken, orgName, environment, isDebug_mode()); - return toolChainListBox; - - } - - /** - * Required Method - * This is used to determine if this build step is applicable for your chosen project type. (FreeStyle, MultiConfiguration, Maven) - * Some plugin build steps might be made to be only available to MultiConfiguration projects. - * - * @param aClass The current project - * @return a boolean indicating whether this build step can be chose given the project type - */ - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project types - // return FreeStyleProject.class.isAssignableFrom(aClass); - return true; - } - - /** - * Required Method - * @return The text to be displayed when selecting your build in the project - */ - public String getDisplayName() { - return "Publish build information to IBM Cloud DevOps"; - } - - public String getEnvironment() { - return getEnv(Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getConsoleUrl()); - } - - public boolean isDebug_mode() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).isDebug_mode(); - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/PublishDeploy.java b/src/main/java/com/ibm/devops/dra/PublishDeploy.java deleted file mode 100644 index 7f53ec9..0000000 --- a/src/main/java/com/ibm/devops/dra/PublishDeploy.java +++ /dev/null @@ -1,517 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.gson.*; -import hudson.EnvVars; -import hudson.Extension; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Publisher; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.tasks.SimpleBuildStep; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.kohsuke.stapler.*; - -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.IOException; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.List; -import java.util.TimeZone; - -public class PublishDeploy extends AbstractDevOpsAction implements SimpleBuildStep { - - private static String DEPLOYMENT_API_URL = "/organizations/{org_name}/toolchainids/{toolchain_id}/buildartifacts/{build_artifact}/builds/{build_id}/deployments"; - private final static String CONTENT_TYPE_JSON = "application/json"; - - private PrintStream printStream; - - // form fields from UI - private String applicationName; - private String toolchainName; - private String orgName; - private String buildJobName; - private String environmentName; - private String credentialsId; - private String applicationUrl; - private String buildNumber; - private static String bluemixToken; - private static String preCredentials; - - //fields to support jenkins pipeline - private String result; - private String username; - private String password; - - @DataBoundConstructor - public PublishDeploy(String applicationName, - String toolchainName, - String orgName, - String buildJobName, - String environmentName, - String credentialsId, - String applicationUrl, - OptionalBuildInfo additionalBuildInfo) { - this.applicationName = applicationName; - this.toolchainName = toolchainName; - this.orgName = orgName; - this.buildJobName = buildJobName; - this.environmentName = environmentName; - this.credentialsId = credentialsId; - this.applicationUrl = applicationUrl; - - if (additionalBuildInfo == null) { - this.buildNumber = null; - } else { - this.buildNumber = additionalBuildInfo.buildNumber; - } - } - - public PublishDeploy(HashMap envVarsMap, HashMap paramsMap) { - this.environmentName = paramsMap.get("environment"); - this.result = paramsMap.get("result"); - this.applicationName = envVarsMap.get(APP_NAME); - this.orgName = envVarsMap.get(ORG_NAME); - this.toolchainName = envVarsMap.get(TOOLCHAIN_ID); - this.username = envVarsMap.get(USERNAME); - this.password = envVarsMap.get(PASSWORD); - } - - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - /** - * We'll use this from the config.jelly. - */ - public String getApplicationName() { - return applicationName; - } - - public String getToolchainName() { - return toolchainName; - } - - public String getOrgName() { - return orgName; - } - - public String getBuildJobName() { - return buildJobName; - } - - public String getEnvironmentName() { - return environmentName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getApplicationUrl() { - return applicationUrl; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getResult() { - return result; - } - - public static class OptionalBuildInfo { - private String buildNumber; - - @DataBoundConstructor - public OptionalBuildInfo(String buildNumber) { - this.buildNumber = buildNumber; - } - } - - @Override - public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, - @Nonnull TaskListener listener) throws InterruptedException, IOException { - - printStream = listener.getLogger(); - printPluginVersion(this.getClass().getClassLoader(), printStream); - - // Get the project name and build id from environment - EnvVars envVars = build.getEnvironment(listener); - - // verify if user chooses advanced option to input customized DLMS - String env = getDescriptor().getEnvironment(); - String targetAPI = chooseTargetAPI(env); - String dlmsUrl = chooseDLMSUrl(env) + DEPLOYMENT_API_URL; - - // expand to support env vars - this.orgName = envVars.expand(this.orgName); - this.toolchainName = envVars.expand(this.toolchainName); - this.applicationName = envVars.expand(this.applicationName); - this.environmentName = envVars.expand(this.environmentName); - this.applicationUrl = envVars.expand(this.applicationUrl); - - if (Util.isNullOrEmpty(orgName) || Util.isNullOrEmpty(applicationName) || Util.isNullOrEmpty(environmentName) || Util.isNullOrEmpty(toolchainName)) { - printStream.println("[IBM Cloud DevOps] Missing few required configurations"); - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Deployment Info."); - return; - } - - String buildNumber; - // if user does not specify the build number - if (Util.isNullOrEmpty(this.buildNumber)) { - // locate the build job that triggers current build - Run triggeredBuild = getTriggeredBuild(build, buildJobName, envVars, printStream); - if (triggeredBuild == null) { - //failed to find the build job - return; - } else { - if (Util.isNullOrEmpty(this.buildJobName)) { - // handle the case which the build job name left empty, and the pipeline case - this.buildJobName = envVars.get("JOB_NAME"); - } - buildNumber = getBuildNumber(buildJobName, triggeredBuild); - } - } else { - buildNumber = envVars.expand(this.buildNumber); - } - - dlmsUrl = dlmsUrl.replace("{org_name}", URLEncoder.encode(this.orgName, "UTF-8").replaceAll("\\+", "%20")); - dlmsUrl = dlmsUrl.replace("{toolchain_id}", URLEncoder.encode(toolchainName, "UTF-8").replaceAll("\\+", "%20")); - dlmsUrl = dlmsUrl.replace("{build_artifact}", URLEncoder.encode(applicationName, "UTF-8").replaceAll("\\+", "%20")); - dlmsUrl = dlmsUrl.replace("{build_id}", URLEncoder.encode(buildNumber, "UTF-8").replaceAll("\\+", "%20")); - String link = chooseControlCenterUrl(env) + "deploymentrisk?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" + this.toolchainName; - String jobUrl; - if (checkRootUrl(printStream)) { - jobUrl = Jenkins.getInstance().getRootUrl() + build.getUrl(); - } else { - jobUrl = build.getAbsoluteUrl(); - } - - String bluemixToken; - // get the Bluemix token - try { - if (Util.isNullOrEmpty(this.credentialsId)) { - bluemixToken = getBluemixToken(username, password, targetAPI); - } else { - bluemixToken = getBluemixToken(build.getParent(), this.credentialsId, targetAPI); - } - - printStream.println("[IBM Cloud DevOps] Log in successfully, get the Bluemix token"); - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Username/Password is not correct, fail to authenticate with Bluemix"); - printStream.println("[IBM Cloud DevOps]" + e.toString()); - return; - } - - if (uploadDeploymentInfo(bluemixToken, dlmsUrl, build, jobUrl)) { - printStream.println("[IBM Cloud DevOps] Go to Control Center (" + link + ") to check your deployment status"); - } - } - - private boolean uploadDeploymentInfo(String token, String dlmsUrl, Run build, String jobUrl) { - - String resStr = ""; - - try { - CloseableHttpClient httpClient = HttpClients.createDefault(); - HttpPost postMethod = new HttpPost(dlmsUrl); - postMethod = addProxyInformation(postMethod); - postMethod.setHeader("Authorization", token); - postMethod.setHeader("Content-Type", CONTENT_TYPE_JSON); - - String buildStatus; - Result result = build.getResult(); - if ((result != null && result.equals(Result.SUCCESS)) - || (this.result != null && this.result.equals(RESULT_SUCCESS))) { - buildStatus = "pass"; - } else { - buildStatus = "fail"; - } - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - TimeZone utc = TimeZone.getTimeZone("UTC"); - dateFormat.setTimeZone(utc); - String timestamp = dateFormat.format(System.currentTimeMillis()); - - // build up the json body - Gson gson = new Gson(); - DeploymentInfoModel deploymentInfo = new DeploymentInfoModel(applicationUrl, environmentName, jobUrl, buildStatus, - timestamp); - - String json = gson.toJson(deploymentInfo); - StringEntity data = new StringEntity(json); - postMethod.setEntity(data); - CloseableHttpResponse response = httpClient.execute(postMethod); - resStr = EntityUtils.toString(response.getEntity()); - - - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - printStream.println("[IBM Cloud DevOps] Deployment Info uploaded successfully"); - return true; - - } else { - // if gets error status - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Deployment Info, response status " - + response.getStatusLine()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("user_error")) { - printStream.println("[IBM Cloud DevOps] Reason: " + resJson.get("user_error")); - } - } - } catch (JsonSyntaxException e) { - printStream.println("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); - } catch (IllegalStateException e) { - // will be triggered when 403 Forbidden - try { - printStream.println("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); - } catch (UnsupportedEncodingException e1) { - e1.printStackTrace(); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - return false; - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - // Overridden for better type safety. - // If your plugin doesn't really define any property on Descriptor, - // you don't have to do this. - @Override - public PublishDeployImpl getDescriptor() { - return (PublishDeployImpl) super.getDescriptor(); - } - - /** - * Descriptor for {@link PublishBuild}. Used as a singleton. The - * class is marked as public so that it can be accessed from views. - * - *

- * See - * src/main/resources/com/ibm/devops/dra/PublishBuild/*.jelly - * for the actual HTML fragment for the configuration screen. - */ - @Extension // This indicates to Jenkins that this is an implementation of an - // extension point. - public static final class PublishDeployImpl extends BuildStepDescriptor { - /** - * To persist global configuration information, simply store it in a - * field and call save(). - * - *

- * If you don't want fields to be persisted, use transient. - */ - - /** - * In order to load the persisted global configuration, you have to call - * load() in the constructor. - */ - public PublishDeployImpl() { - super(PublishDeploy.class); - load(); - } - - /** - * Performs on-the-fly validation of the form field 'credentialId'. - * - * @param value - * This parameter receives the value that the user has typed. - * @return Indicates the outcome of the validation. This is sent to the - * browser. - *

- * Note that returning {@link FormValidation#error(String)} does - * not prevent the form from being saved. It just means that a - * message will be displayed to the user. - */ - - public FormValidation doCheckOrgName(@QueryParameter String value) throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckApplicationName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckToolchainName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Could not retrieve list of toolchains. Please check your username and password. If you have not created a toolchain, create one here."); - } - return FormValidation.ok(); - } - - public FormValidation doCheckEnvironmentName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - preCredentials = credentialsId; - try { - String newToken = getBluemixToken(context, credentialsId, targetAPI); - if (Util.isNullOrEmpty(newToken)) { - bluemixToken = newToken; - return FormValidation.warning("Got empty token"); - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } catch (Exception e) { - return FormValidation.error("Failed to log in to Bluemix, please check your username/password"); - } - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } - - /** - * Autocompletion for build job name field - * - * @param value - * - user input for the build job name field - * @return - */ - public AutoCompletionCandidates doAutoCompleteBuildJobName(@QueryParameter String value) { - AutoCompletionCandidates auto = new AutoCompletionCandidates(); - - // get all jenkins job - List jobs = Jenkins.getInstance().getAllItems(Job.class); - for (int i = 0; i < jobs.size(); i++) { - String jobName = jobs.get(i).getName(); - - if (jobName.toLowerCase().startsWith(value.toLowerCase())) { - auto.add(jobName); - } - } - - return auto; - } - - /** - * This method is called to populate the credentials list on the Jenkins - * config page. - */ - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, - @QueryParameter("target") final String target) { - StandardListBoxModel result = new StandardListBoxModel(); - result.includeEmptyValue(); - result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, context, ACL.SYSTEM, - URIRequirementBuilder.fromUri(target).build())); - return result; - } - - /** - * This method is called to populate the toolchain list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillToolchainNameItems(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("orgName") final String orgName) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######UPLOAD DEPLOYMENT INFO : calling getToolchainList#######"); - } - ListBoxModel toolChainListBox = getToolchainList(bluemixToken, orgName, environment, isDebug_mode()); - return toolChainListBox; - } - - /** - * Required Method This is used to determine if this build step is - * applicable for your chosen project type. (FreeStyle, - * MultiConfiguration, Maven) Some plugin build steps might be made to - * be only available to MultiConfiguration projects. - * - * @param aClass - * The current project - * @return a boolean indicating whether this build step can be chose - * given the project type - */ - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project - // types - // return FreeStyleProject.class.isAssignableFrom(aClass); - return true; - } - - /** - * Required Method - * - * @return The text to be displayed when selecting your build in the - * project - */ - public String getDisplayName() { - return "Publish deployment information to IBM Cloud DevOps"; - } - - public String getEnvironment() { - return getEnv(Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getConsoleUrl()); - } - - public boolean isDebug_mode() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).isDebug_mode(); - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/PublishSQ.java b/src/main/java/com/ibm/devops/dra/PublishSQ.java deleted file mode 100644 index 25089c4..0000000 --- a/src/main/java/com/ibm/devops/dra/PublishSQ.java +++ /dev/null @@ -1,617 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.gson.*; -import hudson.*; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Publisher; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.tasks.SimpleBuildStep; - -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.entity.StringEntity; -import org.kohsuke.stapler.*; - -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.*; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.Map; -import java.util.TimeZone; - - -import javax.xml.bind.DatatypeConverter; - -/** - * Authenticate with Bluemix and then upload the result file to DRA - */ -public class PublishSQ extends AbstractDevOpsAction implements SimpleBuildStep { - - private final static String API_PART = "/organizations/{org_name}/toolchainids/{toolchain_id}/buildartifacts/{build_artifact}/builds/{build_id}/results"; - private final static String CONTENT_TYPE_JSON = "application/json"; - - // form fields from UI - private String applicationName; - private String buildJobName; - private String orgName; - private String toolchainName; - private String environmentName; - private String credentialsId; - private String buildNumber; - - private String SQProjectKey; - private String SQHostName; - private String SQAuthToken; - private String IBMusername; - private String IBMpassword; - - private String envName; - private boolean isDeploy; - - private PrintStream printStream; - private String dlmsUrl; - private static String bluemixToken; - private static String preCredentials; - - @DataBoundConstructor - public PublishSQ(String credentialsId, - String orgName, - String toolchainName, - String buildJobName, - String applicationName, - String SQHostName, - String SQAuthToken, - String SQProjectKey, - OptionalBuildInfo additionalBuildInfo) { - this.credentialsId = credentialsId; - this.orgName = orgName; - this.toolchainName = toolchainName; - this.buildJobName = buildJobName; - this.applicationName = applicationName; - this.SQHostName = SQHostName; - this.SQAuthToken = SQAuthToken; - this.SQProjectKey = SQProjectKey; - - if (additionalBuildInfo == null) { - this.buildNumber = null; - } else { - this.buildNumber = additionalBuildInfo.buildNumber; - } - } - - - public PublishSQ(HashMap envVarsMap, HashMap paramsMap) { - this.SQProjectKey = paramsMap.get("SQProjectKey"); - this.SQHostName = paramsMap.get("SQHostName"); - this.SQAuthToken = paramsMap.get("SQAuthToken"); - - this.applicationName = envVarsMap.get(APP_NAME); - this.orgName = envVarsMap.get(ORG_NAME); - this.toolchainName = envVarsMap.get(TOOLCHAIN_ID); - this.IBMusername = envVarsMap.get(USERNAME); - this.IBMpassword = envVarsMap.get(PASSWORD); - } - - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - /** - * We'll use this from the config.jelly. - */ - public String getApplicationName() { - return applicationName; - } - - public String getToolchainName() { - return toolchainName; - } - - public String getOrgName() { - return orgName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getBuildJobName() { - return buildJobName; - } - - public String getSQHostName() { - return this.SQHostName; - } - - public String getSQAuthToken() { - return this.SQAuthToken; - } - - public String getSQProjectKey() { - return this.SQProjectKey; - } - - public String getBuildNumber() { - return buildNumber; - } - - public boolean isDeploy() { - return isDeploy; - } - - public static class OptionalBuildInfo { - private String buildNumber; - - @DataBoundConstructor - public OptionalBuildInfo(String buildNumber) { - this.buildNumber = buildNumber; - } - } - - - @Override - public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { - - printStream = listener.getLogger(); - printPluginVersion(this.getClass().getClassLoader(), printStream); - - // Get the project name and build id from environment - EnvVars envVars = build.getEnvironment(listener); - - // verify if user chooses advanced option to input customized DLMS - String env = getDescriptor().getEnvironment(); - String targetAPI = chooseTargetAPI(env); - String url = chooseDLMSUrl(env) + API_PART; - // expand to support env vars - this.orgName = envVars.expand(this.orgName); - this.applicationName = envVars.expand(this.applicationName); - this.toolchainName = envVars.expand(this.toolchainName); - if (this.isDeploy || !Util.isNullOrEmpty(this.envName)) { - this.environmentName = envVars.expand(this.envName); - } - - String buildNumber; - // if user does not specify the build number - if (Util.isNullOrEmpty(this.buildNumber)) { - // locate the build job that triggers current build - Run triggeredBuild = getTriggeredBuild(build, buildJobName, envVars, printStream); - if (triggeredBuild == null) { - //failed to find the build job - return; - } else { - if (Util.isNullOrEmpty(this.buildJobName)) { - // handle the case which the build job name left empty, and the pipeline case - this.buildJobName = envVars.get("JOB_NAME"); - } - buildNumber = getBuildNumber(buildJobName, triggeredBuild); - } - } else { - buildNumber = envVars.expand(this.buildNumber); - } - - url = url.replace("{org_name}", URLEncoder.encode(this.orgName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{toolchain_id}", URLEncoder.encode(this.toolchainName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{build_artifact}", URLEncoder.encode(this.applicationName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{build_id}", URLEncoder.encode(buildNumber, "UTF-8").replaceAll("\\+", "%20")); - this.dlmsUrl = url; - - String bluemixToken; - // get the Bluemix token - try { - if (Util.isNullOrEmpty(this.credentialsId)) { - bluemixToken = getBluemixToken(IBMusername, IBMpassword, targetAPI); - } else { - bluemixToken = getBluemixToken(build.getParent(), this.credentialsId, targetAPI); - } - printStream.println("[IBM Cloud DevOps] Log in successfully, got the Bluemix token"); - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Username/Password is not correct, fail to authenticate with Bluemix"); - printStream.println("[IBM Cloud DevOps]" + e.toString()); - return; - } - - Map headers = new HashMap(); - // ':' needs to be added so the SQ api knows an auth token is being used - String SQAuthToken = DatatypeConverter.printBase64Binary((this.SQAuthToken + ":").getBytes("UTF-8")); - headers.put("Authorization", "Basic " + SQAuthToken); - try { - JsonObject SQqualityGate = sendGETRequest(this.SQHostName + "/api/qualitygates/project_status?projectKey=" + this.SQProjectKey, headers); - printStream.println("[IBM Cloud DevOps] Successfully queried SonarQube for quality gate information"); - JsonObject SQissues = getFullResponse(this.SQHostName + "/api/issues/search?statuses=OPEN&projectKeys=" + this.SQProjectKey, headers); - printStream.println("[IBM Cloud DevOps] Successfully queried SonarQube for issue information"); - JsonObject SQratings = sendGETRequest(this.SQHostName + "/api/measures/component?metricKeys=reliability_rating,security_rating,sqale_rating&componentKey=" + this.SQProjectKey, headers); - printStream.println("[IBM Cloud DevOps] Successfully queried SonarQube for metric information"); - - JsonObject payload = createDLMSPayload(SQqualityGate, SQissues, SQratings); - JsonArray urls = createPayloadUrls(this.SQHostName, this.SQProjectKey); - sendPayloadToDLMS(bluemixToken, payload, urls); - - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Error: Unable to upload results. Please make sure all parameters are valid"); - e.printStackTrace(); - } - } - - /** - * Constructs the urls that should be sent with the DLMS message - * - * @param SQHostname hostname of the SQ instance - * @param SQKey project key of the SQ instance - * @return an array of URLs that should be sent to dlms along with the payload - */ - public JsonArray createPayloadUrls(String SQHostname, String SQKey) { - - JsonArray urls = new JsonArray(); - String url = SQHostname + "/dashboard/index/" + SQKey; - urls.add(url); - return urls; - } - - /** - * Combines all SQ information into one gson that can be sent to DLMS - * - * @param qualityGateData information pertaining to SQ gate status - * @param issuesData information pertaining to SQ issues raised - * @param ratingsData information pertaining to SQ ratings - * @return combined gson object - */ - public JsonObject createDLMSPayload(JsonObject qualityGateData, JsonObject issuesData, JsonObject ratingsData) { - - JsonObject payload = new JsonObject(); - - payload.add("qualityGate", qualityGateData.get("projectStatus")); - payload.add("issues", issuesData.get("issues")); - - JsonParser parser = new JsonParser(); - JsonObject component = (JsonObject)parser.parse(ratingsData.get("component").toString()); - payload.add("ratings", component.get("measures")); - - return payload; - } - - /** - * Sends a GET request to the provided url - * - * @param url the endpoint of the request - * @param headers a map of headers where key is the header name and the map value is the header value - * @return a JSON parsed representation of the payload returneds - * @throws Exception - */ - private JsonObject sendGETRequest(String url, Map headers) throws Exception { - - String resStr; - CloseableHttpClient httpClient = HttpClients.createDefault(); - - HttpGet getMethod = new HttpGet(url); - getMethod = addProxyInformation(getMethod); - - //add request headers - for(Map.Entry entry: headers.entrySet()) { - getMethod.setHeader(entry.getKey(), entry.getValue()); - } - - CloseableHttpResponse response = httpClient.execute(getMethod); - resStr = EntityUtils.toString(response.getEntity()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - - return resJson; - } - - /** - * Get all the pages of response, a call to the api returns a single page response, this function will iterate thru all the - * pages to get a full response. Gets 250 records per page at a time. - * - * @param url the endpoint of the request - * @param headers a map of headers where key is the header name and the map value is the header value - * @return a JSON parsed representation of the payload returned - * @throws Exception - */ - private JsonObject getFullResponse(String url, Map headers) throws Exception { - JsonArray finalArray = new JsonArray(); - int recordCount = 0; - int page = 1; - - do { - String uurl = url + "&ps=250&p=" + page; - JsonObject partResponse = sendGETRequest(uurl, headers); - JsonArray issues = partResponse.getAsJsonArray("issues"); - finalArray.addAll(issues); - recordCount = issues.size(); - page += 1; - } while(recordCount > 0); - - JsonObject payload = new JsonObject(); - payload.add("issues", finalArray); - return payload; - } - - /** - * Sends POST method to DLMS to upload SQ results - *F - * @param bluemixToken the bluemix auth header that allows us to talk to dlms - * @param payload the content part of the payload to send to dlms - * @param urls a json array that holds the urls for a payload - * @return boolean based on if the request was successful or not - */ - private boolean sendPayloadToDLMS(String bluemixToken, JsonObject payload, JsonArray urls) { - String resStr = ""; - printStream.println("[IBM Cloud DevOps] Uploading SonarQube results..."); - try { - CloseableHttpClient httpClient = HttpClients.createDefault(); - - HttpPost postMethod = new HttpPost(this.dlmsUrl); - postMethod = addProxyInformation(postMethod); - postMethod.setHeader("Authorization", bluemixToken); - postMethod.setHeader("Content-Type", CONTENT_TYPE_JSON); - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - TimeZone utc = TimeZone.getTimeZone("UTC"); - dateFormat.setTimeZone(utc); - String timestamp = dateFormat.format(System.currentTimeMillis()); - - JsonObject body = new JsonObject(); - - body.addProperty("contents", DatatypeConverter.printBase64Binary(payload.toString().getBytes("UTF-8"))); - body.addProperty("contents_type", CONTENT_TYPE_JSON); - body.addProperty("timestamp", timestamp); - body.addProperty("tool_name", "sonarqube"); - body.addProperty("lifecycle_stage", "sonarqube"); - body.add("url", urls); - - StringEntity data = new StringEntity(body.toString()); - postMethod.setEntity(data); - CloseableHttpResponse response = httpClient.execute(postMethod); - resStr = EntityUtils.toString(response.getEntity()); - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - printStream.println("[IBM Cloud DevOps] Upload Build Information successfully"); - return true; - - } else { - // if gets error status - printStream.println("[IBM Cloud DevOps] Error: Failed to upload, response status " + response.getStatusLine()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("user_error")) { - printStream.println("[IBM Cloud DevOps] Reason: " + resJson.get("user_error")); - } - } - } catch (JsonSyntaxException e) { - printStream.println("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); - } catch (IllegalStateException e) { - // will be triggered when 403 Forbidden - try { - printStream.println("[IBM Cloud DevOps] Please check if you have the access to " + URLEncoder.encode(this.orgName, "UTF-8") + " org"); - } catch (UnsupportedEncodingException e1) { - e1.printStackTrace(); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return false; - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - // Overridden for better type safety. - // If your plugin doesn't really define any property on Descriptor, - // you don't have to do this. - @Override - public PublishSQImpl getDescriptor() { - return (PublishSQImpl)super.getDescriptor(); - } - - /** - * Descriptor for {@link PublishSQ}. Used as a singleton. - * The class is marked as public so that it can be accessed from views. - * - *

- * See src/main/resources/com/ibm/devops/dra/PublishTest/*.jelly - * for the actual HTML fragment for the configuration screen. - */ - @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class PublishSQImpl extends BuildStepDescriptor { - /** - * To persist global configuration information, - * simply store it in a field and call save(). - * - *

- * If you don't want fields to be persisted, use transient. - */ - - /** - * In order to load the persisted global configuration, you have to - * call load() in the constructor. - */ - public PublishSQImpl() { - super(PublishSQ.class); - load(); - } - - /** - * Performs on-the-fly validation of the form field 'credentialId'. - * - * @param value - * This parameter receives the value that the user has typed. - * @return - * Indicates the outcome of the validation. This is sent to the browser. - *

- * Note that returning {@link FormValidation#error(String)} does not - * prevent the form from being saved. It just means that a message - * will be displayed to the user. - */ - - public FormValidation doCheckOrgName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckToolchainName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Could not retrieve list of toolchains. Please check your username and password. If you have not created a toolchain, create one here."); - } - return FormValidation.ok(); - } - - public FormValidation doCheckApplicationName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckSQHostName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckSQAuthToken(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckSQProjectKey(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - preCredentials = credentialsId; - try { - String newToken = getBluemixToken(context, credentialsId, targetAPI); - if (Util.isNullOrEmpty(newToken)) { - bluemixToken = newToken; - return FormValidation.warning("Got empty token"); - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } catch (Exception e) { - return FormValidation.error("Failed to log in to Bluemix, please check your username/password"); - } - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } - - /** - * This method is called to populate the credentials list on the Jenkins config page. - */ - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, - @QueryParameter("target") final String target) { - StandardListBoxModel result = new StandardListBoxModel(); - result.includeEmptyValue(); - result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(target).build() - ) - ); - return result; - } - - /** - * This method is called to populate the toolchain list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillToolchainNameItems(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("orgName") final String orgName) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######UPLOAD BUILD INFO : calling getToolchainList#######"); - } - ListBoxModel toolChainListBox = getToolchainList(bluemixToken, orgName, environment, isDebug_mode()); - return toolChainListBox; - - } - - /** - * Required Method - * This is used to determine if this build step is applicable for your chosen project type. (FreeStyle, MultiConfiguration, Maven) - * Some plugin build steps might be made to be only available to MultiConfiguration projects. - * - * @param aClass The current project - * @return a boolean indicating whether this build step can be chose given the project type - */ - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project types - // return FreeStyleProject.class.isAssignableFrom(aClass); - return true; - } - - /** - * Required Method - * @return The text to be displayed when selecting your build in the project - */ - public String getDisplayName() { - return "Publish SonarQube test result to IBM Cloud DevOps"; - } - - public String getEnvironment() { - return getEnv(Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getConsoleUrl()); - } - - public boolean isDebug_mode() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).isDebug_mode(); - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/PublishTest.java b/src/main/java/com/ibm/devops/dra/PublishTest.java deleted file mode 100644 index 65620cc..0000000 --- a/src/main/java/com/ibm/devops/dra/PublishTest.java +++ /dev/null @@ -1,961 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardListBoxModel; -import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import com.google.gson.*; -import hudson.*; -import hudson.model.*; -import hudson.security.ACL; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Publisher; -import hudson.util.FormValidation; -import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; -import jenkins.tasks.SimpleBuildStep; -import org.apache.commons.io.FilenameUtils; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.mime.HttpMultipartMode; -import org.apache.http.entity.mime.MultipartEntityBuilder; -import org.apache.http.entity.mime.content.FileBody; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.kohsuke.stapler.*; - -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.HashMap; -import java.util.List; -import java.util.TimeZone; -import java.util.HashSet; - -/** - * Authenticate with Bluemix and then upload the result file to DRA - */ -public class PublishTest extends AbstractDevOpsAction implements SimpleBuildStep { - - private final static String API_PART = "/organizations/{org_name}/toolchainids/{toolchain_id}/buildartifacts/{build_artifact}/builds/{build_id}/results_multipart"; - private final static String CONTENT_TYPE_JSON = "application/json"; - private final static String CONTENT_TYPE_XML = "application/xml"; - - // form fields from UI - private final String lifecycleStage; - private String contents; - private String additionalLifecycleStage; - private String additionalContents; - private String buildNumber; - private String applicationName; - private String buildJobName; - private String orgName; - private String toolchainName; - private String environmentName; - private String credentialsId; - private String policyName; - private boolean willDisrupt; - - private EnvironmentScope testEnv; - private String envName; - private boolean isDeploy; - - private PrintStream printStream; - private File root; - private String dlmsUrl; - private String draUrl; - private static String bluemixToken; - private static String preCredentials; - - //fields to support jenkins pipeline - private String username; - private String password; - - @DataBoundConstructor - public PublishTest(String lifecycleStage, - String contents, - String applicationName, - String orgName, - String toolchainName, - String buildJobName, - String credentialsId, - OptionalUploadBlock additionalUpload, - OptionalBuildInfo additionalBuildInfo, - OptionalGate additionalGate, - EnvironmentScope testEnv) { - this.lifecycleStage = lifecycleStage; - this.contents = contents; - this.credentialsId = credentialsId; - this.applicationName = applicationName; - this.orgName = orgName; - this.toolchainName = toolchainName; - this.buildJobName = buildJobName; - this.testEnv = testEnv; - this.envName = testEnv.getEnvName(); - this.isDeploy = testEnv.isDeploy(); - - if (additionalUpload == null) { - this.additionalContents = null; - this.additionalLifecycleStage = null; - } else { - this.additionalLifecycleStage = additionalUpload.additionalLifecycleStage; - this.additionalContents = additionalUpload.additionalContents; - } - - if (additionalBuildInfo == null) { - this.buildNumber = null; - } else { - this.buildNumber = additionalBuildInfo.buildNumber; - } - - if (additionalGate == null) { - this.policyName = null; - this.willDisrupt = false; - } else { - this.policyName = additionalGate.getPolicyName(); - this.willDisrupt = additionalGate.isWillDisrupt(); - } - } - - public PublishTest(HashMap envVarsMap, HashMap paramsMap) { - this.lifecycleStage = paramsMap.get("type"); - this.contents = paramsMap.get("fileLocation"); - - this.applicationName = envVarsMap.get(APP_NAME); - this.orgName = envVarsMap.get(ORG_NAME); - this.toolchainName = envVarsMap.get(TOOLCHAIN_ID); - this.username = envVarsMap.get(USERNAME); - this.password = envVarsMap.get(PASSWORD); - } - - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - /** - * We'll use this from the config.jelly. - */ - public String getApplicationName() { - return applicationName; - } - - public String getToolchainName() { - return toolchainName; - } - - public String getOrgName() { - return orgName; - } - - public String getEnvironmentName() { - return environmentName; - } - - public String getBuildJobName() { - return buildJobName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getLifecycleStage() { - return lifecycleStage; - } - - public String getContents() { - return contents; - } - - public String getAdditionalLifecycleStage() { - return additionalLifecycleStage; - } - - public String getAdditionalContents() { - return additionalContents; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getPolicyName() { - return policyName; - } - - public boolean isWillDisrupt() { - return willDisrupt; - } - - public EnvironmentScope getTestEnv() { - return testEnv; - } - - public String getEnvName() { - return envName; - } - - public void setEnvName(String envName) { - this.envName = envName; - } - - public boolean isDeploy() { - return isDeploy; - } - - /** - * Sub class for Optional Upload Block - */ - public static class OptionalUploadBlock { - private String additionalLifecycleStage; - private String additionalContents; - - @DataBoundConstructor - public OptionalUploadBlock(String additionalLifecycleStage, String additionalContents) { - this.additionalLifecycleStage = additionalLifecycleStage; - this.additionalContents = additionalContents; - } - } - - public static class OptionalBuildInfo { - private String buildNumber; - - @DataBoundConstructor - public OptionalBuildInfo(String buildNumber) { - this.buildNumber = buildNumber; - } - } - - public static class OptionalGate { - private String policyName; - private boolean willDisrupt; - - @DataBoundConstructor - public OptionalGate(String policyName, boolean willDisrupt) { - this.policyName = policyName; - setWillDisrupt(willDisrupt); - } - - public String getPolicyName() { - return policyName; - } - - public boolean isWillDisrupt() { - return willDisrupt; - } - - @DataBoundSetter - public void setWillDisrupt(boolean willDisrupt) { - this.willDisrupt = willDisrupt; - } - } - - - @Override - public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { - - printStream = listener.getLogger(); - printPluginVersion(this.getClass().getClassLoader(), printStream); - - // create root dir for storing test result - root = new File(build.getRootDir(), "DRA_TestResults"); - - // Get the project name and build id from environment - EnvVars envVars = build.getEnvironment(listener); - - // verify if user chooses advanced option to input customized DLMS - String env = getDescriptor().getEnvironment(); - String targetAPI = chooseTargetAPI(env); - String url = chooseDLMSUrl(env) + API_PART; - // expand to support env vars - this.orgName = envVars.expand(this.orgName); - this.applicationName = envVars.expand(this.applicationName); - this.toolchainName = envVars.expand(this.toolchainName); - this.contents = envVars.expand(this.contents); - if (this.isDeploy || !Util.isNullOrEmpty(this.envName)) { - this.environmentName = envVars.expand(this.envName); - } - - String buildNumber; - // if user does not specify the build number - if (Util.isNullOrEmpty(this.buildNumber)) { - // locate the build job that triggers current build - Run triggeredBuild = getTriggeredBuild(build, buildJobName, envVars, printStream); - if (triggeredBuild == null) { - //failed to find the build job - return; - } else { - if (Util.isNullOrEmpty(this.buildJobName)) { - // handle the case which the build job name left empty, and the pipeline case - this.buildJobName = envVars.get("JOB_NAME"); - } - buildNumber = getBuildNumber(buildJobName, triggeredBuild); - } - } else { - buildNumber = envVars.expand(this.buildNumber); - } - - url = url.replace("{org_name}", URLEncoder.encode(this.orgName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{toolchain_id}", URLEncoder.encode(this.toolchainName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{build_artifact}", URLEncoder.encode(this.applicationName, "UTF-8").replaceAll("\\+", "%20")); - url = url.replace("{build_id}", URLEncoder.encode(buildNumber, "UTF-8").replaceAll("\\+", "%20")); - this.dlmsUrl = url; - - String link = chooseControlCenterUrl(env) + "deploymentrisk?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" + this.toolchainName; - - String bluemixToken; - // get the Bluemix token - try { - if (Util.isNullOrEmpty(this.credentialsId)) { - bluemixToken = getBluemixToken(username, password, targetAPI); - } else { - bluemixToken = getBluemixToken(build.getParent(), this.credentialsId, targetAPI); - } - - printStream.println("[IBM Cloud DevOps] Log in successfully, get the Bluemix token"); - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Username/Password is not correct, fail to authenticate with Bluemix"); - printStream.println("[IBM Cloud DevOps]" + e.toString()); - return; - } - - // parse the wildcard result files - try { - if(!scanAndUpload(build, workspace, contents, lifecycleStage, bluemixToken)){ - // if there is any error when scanning and uploading - return; - } - - // check to see if we need to upload additional result file - if (!Util.isNullOrEmpty(additionalContents) && !Util.isNullOrEmpty(additionalLifecycleStage)) { - if(!scanAndUpload(build, workspace, additionalContents, additionalLifecycleStage, bluemixToken)) { - return; - } - } - } catch (Exception e) { - printStream.print("[IBM Cloud DevOps] Got Exception: " + e.getMessage()); - e.printStackTrace(); - return; - } - - printStream.println("[IBM Cloud DevOps] Go to Control Center (" + link + ") to check your build status"); - - // Gate - // verify if user chooses advanced option to input customized DRA - if (Util.isNullOrEmpty(policyName)) { - return; - } - - this.draUrl = chooseDRAUrl(env); - - // get decision response from DRA - try { - JsonObject decisionJson = getDecisionFromDRA(bluemixToken, buildNumber); - if (decisionJson == null) { - printStream.println("[IBM Cloud DevOps] get empty decision"); - return; - } - - // retrieve the decision id to compose the report link - String decisionId = String.valueOf(decisionJson.get("decision_id")); - // remove the double quotes - decisionId = decisionId.replace("\"",""); - - // Show Proceed or Failed based on the decision - String decision = String.valueOf(decisionJson.get("contents").getAsJsonObject().get("proceed")); - if (decision.equals("true")) { - decision = "Succeed"; - } else { - decision = "Failed"; - } - - String cclink = chooseControlCenterUrl(env) + "deploymentrisk?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" + this.toolchainName; - - String reportUrl = chooseControlCenterUrl(env) + "decisionreport?orgName=" + URLEncoder.encode(this.orgName, "UTF-8") + "&toolchainId=" - + URLEncoder.encode(toolchainName, "UTF-8") + "&reportId=" + decisionId; - GatePublisherAction action = new GatePublisherAction(reportUrl, cclink, decision, this.policyName, build); - build.addAction(action); - - printStream.println("************************************"); - printStream.println("Check IBM Cloud DevOps Gate Evaluation report here -" + reportUrl); - printStream.println("Check IBM Cloud DevOps Deployment Risk Dashboard here -" + cclink); - // console output for a "fail" decision - if (decision.equals("Failed")) { - printStream.println("IBM Cloud DevOps decision to proceed is: false"); - printStream.println("************************************"); - if (willDisrupt) { - Result result = Result.FAILURE; - build.setResult(result); - } - return; - } - - // console output for a "proceed" decision - printStream.println("IBM Cloud DevOps decision to proceed is: true"); - printStream.println("************************************"); - return; - - } catch (IOException e) { - printStream.print("[IBM Cloud DevOps] Error: " + e.getMessage()); - } - - } - - @Override - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - /** - * Support wildcard for the result file path, scan the path and upload each matching result file to the DLMS - * @param build - the current build - * @param bluemixToken - the Bluemix toekn - * @return false if there is any error when scan and upload the file - */ - public boolean scanAndUpload(Run build, FilePath workspace, String path, String lifecycleStage, String bluemixToken) throws Exception { - boolean errorFlag = true; - FilePath[] filePaths = null; - - if (Util.isNullOrEmpty(path)) { - // if no result file specified, create dummy result based on the build status - filePaths = new FilePath[]{createDummyFile(build, workspace)}; - } else { - - // remove "./" prefix of the path if it exists - if (path.startsWith("./")) { - path = path.substring(2); - } - - try { - filePaths = workspace.list(path); - } catch(InterruptedException ie) { - printStream.println("[IBM Cloud DevOps] catching interrupt" + ie.getMessage()); - ie.printStackTrace(); - throw ie; - } catch (IOException e) { - printStream.println("[IBM Cloud DevOps] catching act" + e.getMessage()); - e.printStackTrace(); - throw e; - } - } - - if (filePaths == null || filePaths.length < 1) { - printStream.println("[IBM Cloud DevOps] Error: Fail to find the file, please check the path"); - return false; - } else { - - for (FilePath fp : filePaths) { - - // make sure the file path is for file, and copy to the master build folder - if (!fp.isDirectory()) { - FilePath resultFileLocation = new FilePath(new File(root, fp.getName())); - fp.copyTo(resultFileLocation); - } - - //get timestamp - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - TimeZone utc = TimeZone.getTimeZone("UTC"); - dateFormat.setTimeZone(utc); - String timestamp = dateFormat.format(System.currentTimeMillis()); - - String jobUrl; - if (checkRootUrl(printStream)) { - jobUrl = Jenkins.getInstance().getRootUrl() + build.getUrl(); - } else { - jobUrl = build.getAbsoluteUrl(); - } - - // upload the result file to DLMS - String res = sendFormToDLMS(bluemixToken, fp, lifecycleStage, jobUrl, timestamp); - if(!printUploadMessage(res, fp.getName())) { - errorFlag = false; - } - } - } - - return errorFlag; - } - - /** - * create a dummy result file following mocha format for some testing which does not generate test report - * @param build - current build - * @param workspace - current workspace, if it runs on slave, then it will be the path on slave - * @return simple test result file - */ - private FilePath createDummyFile(Run build, FilePath workspace) throws Exception { - - // if user did not specify the result file location, upload the dummy json file - Gson gson = new Gson(); - - //set the passes and failures based on the test status - int passes, failures; - Result result = build.getResult(); - if (result != null) { - if (!result.equals(Result.SUCCESS)) { - passes = 0; - failures = 1; - } else { - passes = 1; - failures = 0; - } - } else { - throw new Exception("Failed to get build result"); - } - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - TimeZone utc = TimeZone.getTimeZone("UTC"); - dateFormat.setTimeZone(utc); - String start = dateFormat.format(build.getStartTimeInMillis()); - long duration = build.getDuration(); - String end = dateFormat.format(build.getStartTimeInMillis() + duration); - - TestResultModel.Stats stats = new TestResultModel.Stats(1, 1, passes, 0, failures, start, end, duration); - TestResultModel.Test test = new TestResultModel.Test("unknown test", "unknown test", duration, 0, null); - TestResultModel.Test[] tests = {test}; - String[] emptyArray = {}; - TestResultModel testResultModel = new TestResultModel(stats, tests, emptyArray, emptyArray, emptyArray); - - // create new dummy file - try { - FilePath filePath = workspace.child("simpleTest.json"); - filePath.write(gson.toJson(testResultModel), "UTF8"); - return filePath; - } catch (IOException e) { - printStream.println("[IBM Cloud DevOps] Failed to create dummy file in current workspace, Exception: " + e.getMessage()); - } - - return null; - } - - /** - * print out the response message from DLMS to the console log - * @param response - response from DLMS - * @param fileName - uploaded filename - * @return true if upload succeed, otherwise return false - */ - private boolean printUploadMessage(String response, String fileName) { - if (response.contains("Error")) { - printStream.println("[IBM Cloud DevOps] " + response); - } else if (response.contains("200")) { - printStream.println("[IBM Cloud DevOps] Upload [" + fileName + "] SUCCESSFUL"); - return true; - } else { - printStream.println("[IBM Cloud DevOps]" + response + ", Upload [" + fileName + "] FAILED"); - } - - return false; - } - - /** - * * Send POST request to DLMS back end with the result file - * @param bluemixToken - the Bluemix token - * @param contents - the result file - * @param jobUrl - the build url of the build job in Jenkins - * @param timestamp - * @return - response/error message from DLMS - */ - public String sendFormToDLMS(String bluemixToken, FilePath contents, String lifecycleStage, String jobUrl, String timestamp) throws IOException { - - // create http client and post method - CloseableHttpClient httpClient = HttpClients.createDefault(); - HttpPost postMethod = new HttpPost(this.dlmsUrl); - - postMethod = addProxyInformation(postMethod); - // build up multi-part forms - MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); - if (contents != null) { - - File file = new File(root, contents.getName()); - FileBody fileBody = new FileBody(file); - builder.addPart("contents", fileBody); - - - builder.addTextBody("test_artifact", file.getName()); - if (this.isDeploy) { - builder.addTextBody("environment_name", environmentName); - } - //Todo check the value of lifecycleStage - builder.addTextBody("lifecycle_stage", lifecycleStage); - builder.addTextBody("url", jobUrl); - builder.addTextBody("timestamp", timestamp); - - String fileExt = FilenameUtils.getExtension(contents.getName()); - String contentType; - switch (fileExt) { - case "json": - contentType = CONTENT_TYPE_JSON; - break; - case "xml": - contentType = CONTENT_TYPE_XML; - break; - default: - return "Error: " + contents.getName() + " is an invalid result file type"; - } - - builder.addTextBody("contents_type", contentType); - HttpEntity entity = builder.build(); - postMethod.setEntity(entity); - postMethod.setHeader("Authorization", bluemixToken); - } else { - return "Error: File is null"; - } - - - CloseableHttpResponse response = null; - try { - response = httpClient.execute(postMethod); - // parse the response json body to display detailed info - String resStr = EntityUtils.toString(response.getEntity()); - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - - if (!element.isJsonObject()) { - // 401 Forbidden - return "Error: Upload is Forbidden, please check your org name. Error message: " + element.toString(); - } else { - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("status")) { - return String.valueOf(response.getStatusLine()) + "\n" + resJson.get("status"); - } else { - // other cases - return String.valueOf(response.getStatusLine()); - } - } - } catch (IOException e) { - e.printStackTrace(); - throw e; - } - } - - - /** - * Send a request to DRA backend to get a decision - * @param buildId - build ID, get from Jenkins environment - * @return - the response decision Json file - */ - private JsonObject getDecisionFromDRA(String bluemixToken, String buildId) throws IOException { - // create http client and post method - CloseableHttpClient httpClient = HttpClients.createDefault(); - - String url = this.draUrl; - url = url + "/organizations/" + orgName + - "/toolchainids/" + toolchainName + - "/buildartifacts/" + URLEncoder.encode(applicationName, "UTF-8").replaceAll("\\+", "%20") + - "/builds/" + buildId + - "/policies/" + URLEncoder.encode(policyName, "UTF-8").replaceAll("\\+", "%20") + - "/decisions"; - if (this.isDeploy) { - url = url.concat("?environment_name=" + environmentName); - } - - HttpPost postMethod = new HttpPost(url); - - postMethod = addProxyInformation(postMethod); - postMethod.setHeader("Authorization", bluemixToken); - postMethod.setHeader("Content-Type", CONTENT_TYPE_JSON); - - CloseableHttpResponse response = httpClient.execute(postMethod); - String resStr = EntityUtils.toString(response.getEntity()); - - try { - if (response.getStatusLine().toString().contains("200")) { - // get 200 response - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - printStream.println("[IBM Cloud DevOps] Get decision successfully"); - return resJson; - } else { - // if gets error status - printStream.println("[IBM Cloud DevOps] Error: Failed to get a decision, response status " + response.getStatusLine()); - - JsonParser parser = new JsonParser(); - JsonElement element = parser.parse(resStr); - JsonObject resJson = element.getAsJsonObject(); - if (resJson != null && resJson.has("message")) { - printStream.println("[IBM Cloud DevOps] Reason: " + resJson.get("message")); - } - } - } catch (JsonSyntaxException e) { - printStream.println("[IBM Cloud DevOps] Invalid Json response, response: " + resStr); - } - - return null; - - } - - // Overridden for better type safety. - // If your plugin doesn't really define any property on Descriptor, - // you don't have to do this. - @Override - public PublishTestImpl getDescriptor() { - return (PublishTestImpl)super.getDescriptor(); - } - - /** - * Descriptor for {@link PublishTest}. Used as a singleton. - * The class is marked as public so that it can be accessed from views. - * - *

- * See src/main/resources/com/ibm/devops/dra/PublishTest/*.jelly - * for the actual HTML fragment for the configuration screen. - */ - @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class PublishTestImpl extends BuildStepDescriptor { - /** - * To persist global configuration information, - * simply store it in a field and call save(). - * - *

- * If you don't want fields to be persisted, use transient. - */ - - /** - * In order to load the persisted global configuration, you have to - * call load() in the constructor. - */ - public PublishTestImpl() { - super(PublishTest.class); - load(); - } - - /** - * Performs on-the-fly validation of the form field 'credentialId'. - * - * @param value - * This parameter receives the value that the user has typed. - * @return - * Indicates the outcome of the validation. This is sent to the browser. - *

- * Note that returning {@link FormValidation#error(String)} does not - * prevent the form from being saved. It just means that a message - * will be displayed to the user. - */ - - public FormValidation doCheckOrgName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckApplicationName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckToolchainName(@QueryParameter String value) - throws IOException, ServletException { - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Could not retrieve list of toolchains. Please check your username and password. If you have not created a toolchain, create one here."); - } - return FormValidation.ok(); - } - - public FormValidation doCheckEnvironmentName(@QueryParameter String value) - throws IOException, ServletException { - return FormValidation.validateRequired(value); - } - - public FormValidation doCheckPolicyName(@QueryParameter String value) { - - if (value == null || value.equals("empty")) { - return FormValidation.errorWithMarkup("Fail to get the policies, please check your username/password or org name and make sure you have created policies for this org and toolchain."); - } - return FormValidation.ok(); - } - - public FormValidation doTestConnection(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - preCredentials = credentialsId; - try { - String bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - if (Util.isNullOrEmpty(bluemixToken)) { - PublishTest.bluemixToken = bluemixToken; - return FormValidation.warning("Got empty token"); - } else { - return FormValidation.okWithMarkup("Connection successful"); - } - } catch (Exception e) { - return FormValidation.error("Failed to log in to Bluemix, please check your username/password"); - } - } else { - - return FormValidation.okWithMarkup("Connection successful"); - } - } - - /** - * Autocompletion for build job name field - * @param value - user input for the build job name field - * @return - */ - public AutoCompletionCandidates doAutoCompleteBuildJobName(@QueryParameter String value) { - AutoCompletionCandidates auto = new AutoCompletionCandidates(); - - // get all jenkins job - List jobs = Jenkins.getInstance().getAllItems(Job.class); - HashSet jobSet = new HashSet<>(); - for (int i = 0; i < jobs.size(); i++) { - String jobName = jobs.get(i).getName(); - - if (jobName.toLowerCase().startsWith(value.toLowerCase())) { - jobSet.add(jobName); - } - } - - for (String s : jobSet) { - auto.add(s); - } - - return auto; - } - - /** - * This method is called to populate the credentials list on the Jenkins config page. - */ - public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context, - @QueryParameter("target") final String target) { - StandardListBoxModel result = new StandardListBoxModel(); - result.includeEmptyValue(); - result.withMatching(CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), - CredentialsProvider.lookupCredentials( - StandardUsernameCredentials.class, - context, - ACL.SYSTEM, - URIRequirementBuilder.fromUri(target).build() - ) - ); - return result; - } - - /** - * This method is called to populate the policy list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillPolicyNameItems(@AncestorInPath ItemGroup context, - @RelativePath("..") @QueryParameter final String orgName, - @RelativePath("..") @QueryParameter final String toolchainName, - @RelativePath("..") @QueryParameter final String credentialsId) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - // if user changes to a different credential, need to get a new token - if (!credentialsId.equals(preCredentials) || Util.isNullOrEmpty(bluemixToken)) { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - preCredentials = credentialsId; - } - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######UPLOAD TEST RESULTS : calling getPolicyList#######"); - } - return getPolicyList(bluemixToken, orgName, toolchainName, environment, isDebug_mode()); - } - - /** - * This method is called to populate the toolchain list on the Jenkins config page. - * @param context - * @param orgName - * @param credentialsId - * @return - */ - public ListBoxModel doFillToolchainNameItems(@AncestorInPath ItemGroup context, - @QueryParameter("credentialsId") final String credentialsId, - @QueryParameter("orgName") final String orgName) { - String environment = getEnvironment(); - String targetAPI = chooseTargetAPI(environment); - try { - bluemixToken = getBluemixToken(context, credentialsId, targetAPI); - } catch (Exception e) { - return new ListBoxModel(); - } - if(isDebug_mode()){ - LOGGER.info("#######UPLOAD TEST RESULTS : calling getToolchainList#######"); - } - ListBoxModel toolChainListBox = getToolchainList(bluemixToken, orgName, environment, isDebug_mode()); - return toolChainListBox; - - } - - /** - * Required Method - * This is used to determine if this build step is applicable for your chosen project type. (FreeStyle, MultiConfiguration, Maven) - * Some plugin build steps might be made to be only available to MultiConfiguration projects. - * - * @param aClass The current project - * @return a boolean indicating whether this build step can be chose given the project type - */ - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project types - // return FreeStyleProject.class.isAssignableFrom(aClass); - return true; - } - - public ListBoxModel doFillLifecycleStageItems(@QueryParameter("lifecycleStage") final String selection) { - return fillTestType(); - } - - public ListBoxModel doFillAdditionalLifecycleStageItems(@QueryParameter("additionalLifecycleStage") final String selection) { - return fillTestType(); - } - - /** - * fill the dropdown list of rule type - * @return the dropdown list model - */ - public ListBoxModel fillTestType() { - ListBoxModel model = new ListBoxModel(); - - model.add("Unit Test", "unittest"); - model.add("Functional Verification Test", "fvt"); - model.add("Code Coverage", "code"); - return model; - } - - /** - * Required Method - * @return The text to be displayed when selecting your build in the project - */ - public String getDisplayName() { - return "Publish test result to IBM Cloud DevOps"; - } - - public String getEnvironment() { - return getEnv(Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).getConsoleUrl()); - } - - public boolean isDebug_mode() { - return Jenkins.getInstance().getDescriptorByType(DevOpsGlobalConfiguration.class).isDebug_mode(); - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/TestResultModel.java b/src/main/java/com/ibm/devops/dra/TestResultModel.java deleted file mode 100644 index 2116651..0000000 --- a/src/main/java/com/ibm/devops/dra/TestResultModel.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - -/** - * Object for creating dummy test result file following mocha format - */ -public class TestResultModel{ - - // sub-model for stats - public static class Stats { - private Integer suites; - private Integer tests; - private Integer passes; - private Integer pending; - private Integer failures; - private String start; - private String end; - private Long duration; - - public Stats(Integer suites, - Integer tests, - Integer passes, - Integer pending, - Integer failures, - String start, - String end, - Long duration) { - this.suites = suites; - this.tests = tests; - this.passes = passes; - this.pending = pending; - this.failures = failures; - this.start = start; - this.end = end; - this.duration = duration; - } - - public Integer getSuites() { - return suites; - } - - public Integer getTests() { - return tests; - } - - public Integer getPasses() { - return passes; - } - - public Integer getPending() { - return pending; - } - - public Integer getFailures() { - return failures; - } - - public String getStart() { - return start; - } - - public String getEnd() { - return end; - } - - public Long getDuration() { - return duration; - } - } - - // sub-model for test - public static class Test { - private String title; - private String fullTitle; - private Long duration; - private Integer currentRetry; - private Object err; - - public Test(String title, - String fullTitle, - Long duration, - Integer currentRetry, - Object err) { - this.title = title; - this.fullTitle = fullTitle; - this.duration = duration; - this.currentRetry = currentRetry; - this.err = err; - } - - public String getTitle() { - return title; - } - - public String getFullTitle() { - return fullTitle; - } - - public Long getDuration() { - return duration; - } - - public Integer getCurrentRetry() { - return currentRetry; - } - - public Object getErr() { - return err; - } - } - - private Stats stats; - private Test[] tests; - private String[] pending; - private String[] failures; - private String[] passes; - - public TestResultModel(Stats stats, - Test[] tests, - String[] pending, - String[] failures, - String[] passes) { - this.stats = stats; - this.tests = tests.clone(); - this.pending = pending.clone(); - this.failures = failures.clone(); - this.passes = passes.clone(); - } - - public Stats getStats() { - return stats; - } - - public Test[] getTests() { - return tests.clone(); - } - - public String[] getPending() { - return pending.clone(); - } - - public String[] getFailures() { - return failures.clone(); - } - - public String[] getPasses() { - return passes.clone(); - } -} diff --git a/src/main/java/com/ibm/devops/dra/Util.java b/src/main/java/com/ibm/devops/dra/Util.java deleted file mode 100644 index e242781..0000000 --- a/src/main/java/com/ibm/devops/dra/Util.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra; - - -import com.google.common.collect.ImmutableMap; -import hudson.EnvVars; -import java.io.PrintStream; -import java.util.HashMap; -import java.util.Map; - -/** - * Utilities functions - */ - -public class Util { - - private static Map TARGET_API_MAP = ImmutableMap.of( - "devops-api.ng.bluemix.net", "production", - "devops-api.stage1.ng.bluemix.net", "stage1", - "devops-api.eu-de.bluemix.net", "eu-de", - "devops-api.eu-gb.bluemix.net", "eu-gb" - ); - /** - * check if the str is null or empty - * @param str - * @return true if it is null or empty - */ - public static boolean isNullOrEmpty(String str) { - if (str == null || str.isEmpty()) { - return true; - } - return false; - } - - public static boolean allNotNullOrEmpty(String... strs) { - for (String str : strs) { - if (isNullOrEmpty(str)) { - return false; - } - } - return true; - } - - public static boolean allNotNullOrEmpty(HashMap vars, PrintStream printStream) { - for (Map.Entry e : vars.entrySet()) { - if (isNullOrEmpty(e.getValue())) { - if (e.getKey().contains("IBM")) - printStream.println("[IBM Cloud DevOps] Missing environment variables \"" + e.getKey() + "\" configurations"); - else - printStream.println("[IBM Cloud DevOps] Missing required parameters, \"" + e.getKey() + "\""); - return false; - } - } - return true; - } - - public static String getTargetEnv(String webHookUrl, PrintStream printStream) { - if (!isNullOrEmpty(webHookUrl)) { - String baseUrl = webHookUrl.split("@")[1]; - String environment = baseUrl.split("/")[0]; - if (TARGET_API_MAP.keySet().contains(environment)) { - return TARGET_API_MAP.get(environment); - } else { - printStream.println("[IBM Cloud DevOps] WARNING - environment not found: " + environment); - } - } - // default to production - return TARGET_API_MAP.get("production"); - } - - public static boolean validateEnvVariables(EnvVars envVars, PrintStream printStream) { - Boolean valid = true; - if(envVars != null) { - String org = getOrg(envVars); - String space = getSpace(envVars); - String appName = getAppName(envVars); - String user = getUser(envVars); - String pwd = getPassword(envVars); - String webhook = getWebhookUrl(envVars); - - // perform validation and warn for each missing required property - if (isNullOrEmpty(org)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_ORG"); - valid = false; - } - if (isNullOrEmpty(space)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_SPACE"); - valid = false; - } - if (isNullOrEmpty(appName)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_APP_NAME"); - valid = false; - } - if (isNullOrEmpty(user)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_CREDS_USR"); - valid = false; - } - if (isNullOrEmpty(pwd)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_CREDS_PSW"); - valid = false; - } - if (isNullOrEmpty(webhook)) { - printStream.println("[IBM Cloud DevOps] Missing required property IBM_CLOUD_DEVOPS_WEBHOOK_URL"); - valid = false; - } - } - return valid; - } - - public static String getWebhookUrl(EnvVars envVars) { - String webhook = envVars.get("IBM_CLOUD_DEVOPS_WEBHOOK_URL"); - //backward compatibility - if (isNullOrEmpty(webhook)) { - webhook = envVars.get("ICD_WEBHOOK_URL"); - } - return webhook; - } - - public static String getOrg(EnvVars envVars) { - String org = envVars.get("IBM_CLOUD_DEVOPS_ORG"); - //backward compatibility - if (isNullOrEmpty(org)) { - org = envVars.get("CF_ORG"); - } - return org; - } - - public static String getSpace(EnvVars envVars) { - String space = envVars.get("IBM_CLOUD_DEVOPS_SPACE"); - //backward compatibility - if (isNullOrEmpty(space)) { - space = envVars.get("CF_SPACE"); - } - return space; - } - - public static String getAppName(EnvVars envVars) { - String appName = envVars.get("IBM_CLOUD_DEVOPS_APP_NAME"); - //backward compatibility - if (isNullOrEmpty(appName)) { - appName = envVars.get("CF_APP"); - } - return appName; - } - - public static String getUser(EnvVars envVars) { - String user = envVars.get("IBM_CLOUD_DEVOPS_CREDS_USR"); - //backward compatibility - if (isNullOrEmpty(user)) { - user = envVars.get("CF_CREDS_USR"); - } - return user; - } - - public static String getPassword(EnvVars envVars) { - String pwd = envVars.get("IBM_CLOUD_DEVOPS_CREDS_PSW"); - //backward compatibility - if (isNullOrEmpty(pwd)) { - pwd = envVars.get("CF_CREDS_PSW"); - } - return pwd; - } - - public static String getGitRepoUrl(EnvVars envVars) { - String gitUrl = envVars.get("GIT_URL"); - if (isNullOrEmpty(gitUrl)) { - gitUrl = envVars.get("GIT_REPO"); // used in pipeline scripts - } - return gitUrl; - } - - public static String getGitBranch(EnvVars envVars) { - return envVars.get("GIT_BRANCH"); - } - - public static String getGitCommit(EnvVars envVars) { - return envVars.get("GIT_COMMIT"); - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/AbstractDevOpsStep.java b/src/main/java/com/ibm/devops/dra/steps/AbstractDevOpsStep.java deleted file mode 100644 index 76f0fb7..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/AbstractDevOpsStep.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ibm.devops.dra.steps; - -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundSetter; - -/** - * Created by lix on 8/8/17. - */ -abstract public class AbstractDevOpsStep extends AbstractStepImpl { - private String applicationName; - private String orgName; - private String credentialsId; - private String toolchainId; - - @DataBoundSetter - public void setApplicationName(String applicationName) { - this.applicationName = applicationName; - } - - @DataBoundSetter - public void setOrgName(String orgName) { - this.orgName = orgName; - } - - @DataBoundSetter - public void setCredentialsId(String credentialsId) { - this.credentialsId = credentialsId; - } - - @DataBoundSetter - public void setToolchainId(String toolchainId) { - this.toolchainId = toolchainId; - } - - public String getApplicationName() { - return applicationName; - } - - public String getOrgName() { - return orgName; - } - - public String getCredentialsId() { - return credentialsId; - } - - public String getToolchainId() { - return toolchainId; - } - -} diff --git a/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStep.java b/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStep.java deleted file mode 100644 index f8ca38d..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStep.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import hudson.Extension; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class EvaluateGateStep extends AbstractDevOpsStep { - - // required parameters to support pipeline script - private String policy; - - // optional gate parameters - private String forceDecision; - private String environment; - private String buildNumber; - - @DataBoundConstructor - public EvaluateGateStep(String policy) { - this.policy = policy; - } - - @DataBoundSetter - public void setEnvironment(String environment) { - this.environment = environment; - } - - @DataBoundSetter - public void setForceDecision(String forceDecision) { - this.forceDecision = forceDecision; - } - - @DataBoundSetter - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getEnvironment() { - return environment; - } - - public String getPolicy() { - return policy; - } - - public String getForceDecision() { - return forceDecision; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { super(EvaluateGateStepExecution.class); } - - @Override - public String getFunctionName() { - return "evaluateGate"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "IBM Cloud DevOps Gate"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStepExecution.java b/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStepExecution.java deleted file mode 100644 index f025a8f..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/EvaluateGateStepExecution.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import com.ibm.devops.dra.EvaluateGate; -import com.ibm.devops.dra.Util; -import hudson.AbortException; -import hudson.EnvVars; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; - -import javax.inject.Inject; -import java.io.PrintStream; -import java.util.HashMap; - -import static com.ibm.devops.dra.AbstractDevOpsAction.setRequiredEnvVars; - -public class EvaluateGateStepExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient EvaluateGateStep step; - - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient FilePath ws; - @StepContextParameter - private transient Launcher launcher; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - - PrintStream printStream = listener.getLogger(); - HashMap requiredEnvVars = setRequiredEnvVars(step, envVars); - - - //check all the required env vars - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to get Gate decision."); - return null; - } - - String policy = step.getPolicy(); - if (Util.isNullOrEmpty(policy)) { - printStream.println("[IBM Cloud DevOps] evaluateGate is missing required parameters, " + - "please make sure you specify \"policy\""); - printStream.println("[IBM Cloud DevOps] Error: Failed to run evaluate Gate."); - return null; - } - - Boolean willDisrupt = false; - if (!Util.isNullOrEmpty(step.getForceDecision()) && step.getForceDecision().toLowerCase().equals("true")) { - willDisrupt = true; - } - - // optional build number, if user wants to set their own build number - String buildNumber = step.getBuildNumber(); - EvaluateGate evaluateGate = new EvaluateGate(requiredEnvVars, policy, step.getEnvironment(), willDisrupt); - try { - if (!Util.isNullOrEmpty(buildNumber)) { - evaluateGate.setBuildNumber(buildNumber); - } - evaluateGate.perform(build, ws, launcher, listener); - } catch (AbortException e) { - throw new AbortException("Decision is fail"); - } - - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishBuildStep.java b/src/main/java/com/ibm/devops/dra/steps/PublishBuildStep.java deleted file mode 100644 index bc3847a..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishBuildStep.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import hudson.Extension; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class PublishBuildStep extends AbstractDevOpsStep { - - // required parameters to support pipeline script - private String result; - private String gitRepo; - private String gitBranch; - private String gitCommit; - - // custom build number, optional - private String buildNumber; - - @DataBoundConstructor - public PublishBuildStep(String result, String gitRepo, String gitBranch, String gitCommit) { - this.gitRepo = gitRepo; - this.gitBranch = gitBranch; - this.gitCommit = gitCommit; - this.result = result; - } - - @DataBoundSetter - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - public String getGitRepo() { - return gitRepo; - } - - public String getGitBranch() { - return gitBranch; - } - - public String getGitCommit() { - return gitCommit; - } - - public String getResult() { - return result; - } - - public String getBuildNumber() { - return buildNumber; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { super(PublishBuildStepExecution.class); } - - @Override - public String getFunctionName() { - return "publishBuildRecord"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Publish build record to IBM Cloud DevOps"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishBuildStepExecution.java b/src/main/java/com/ibm/devops/dra/steps/PublishBuildStepExecution.java deleted file mode 100644 index 4151932..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishBuildStepExecution.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import com.ibm.devops.dra.PublishBuild; -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; - -import javax.inject.Inject; -import java.io.PrintStream; -import java.util.HashMap; - -import static com.ibm.devops.dra.AbstractDevOpsAction.*; - -public class PublishBuildStepExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient PublishBuildStep step; - - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient FilePath ws; - @StepContextParameter - private transient Launcher launcher; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - - PrintStream printStream = listener.getLogger(); - HashMap requiredEnvVars = setRequiredEnvVars(step, envVars); - - //check all the required env vars - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Build Record."); - return null; - } - - //check all the required parameters - HashMap requiredParams = new HashMap<>(); - String result = step.getResult(); - requiredParams.put("result", result); - requiredParams.put("gitRepo", step.getGitRepo()); - requiredParams.put("gitBranch", step.getGitBranch()); - requiredParams.put("gitCommit", step.getGitCommit()); - - // optional build number, if user wants to set their own build number - String buildNumber = step.getBuildNumber(); - - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Build Record."); - return null; - } - - if (result.equals(RESULT_SUCCESS) || result.equals(RESULT_FAIL)) { - PublishBuild publishBuild = new PublishBuild(requiredEnvVars, requiredParams); - - if (!Util.isNullOrEmpty(buildNumber)) { - publishBuild.setBuildNumber(buildNumber); - } - publishBuild.perform(build, ws, launcher, listener); - } else { - printStream.println("[IBM Cloud DevOps] the \"result\" in the publishBuildRecord should be either \"" - + RESULT_SUCCESS + "\" or \"" + RESULT_FAIL + "\""); - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Build Record."); - } - - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishDeployStep.java b/src/main/java/com/ibm/devops/dra/steps/PublishDeployStep.java deleted file mode 100644 index 5292ec2..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishDeployStep.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import hudson.Extension; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class PublishDeployStep extends AbstractDevOpsStep { - - // required parameters to support pipeline script - private String result; - private String environment; - private String appUrl; - - // custom build number, optional - private String buildNumber; - - @DataBoundConstructor - public PublishDeployStep(String result, String environment, String appUrl) { - this.environment = environment; - this.appUrl = appUrl; - this.result = result; - } - - @DataBoundSetter - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getEnvironment() { - return environment; - } - - public String getAppUrl() { - return appUrl; - } - - public String getResult() { - return result; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { super(PublishDeployStepExecution.class); } - - @Override - public String getFunctionName() { - return "publishDeployRecord"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Publish deploy record to IBM Cloud DevOps"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishDeployStepExecution.java b/src/main/java/com/ibm/devops/dra/steps/PublishDeployStepExecution.java deleted file mode 100644 index 8369223..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishDeployStepExecution.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import com.ibm.devops.dra.PublishDeploy; -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; - -import javax.inject.Inject; -import java.io.PrintStream; -import java.util.HashMap; - -import static com.ibm.devops.dra.AbstractDevOpsAction.RESULT_FAIL; -import static com.ibm.devops.dra.AbstractDevOpsAction.RESULT_SUCCESS; -import static com.ibm.devops.dra.AbstractDevOpsAction.setRequiredEnvVars; - -public class PublishDeployStepExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient PublishDeployStep step; - - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient FilePath ws; - @StepContextParameter - private transient Launcher launcher; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - - @Override - protected Void run() throws Exception { - - PrintStream printStream = listener.getLogger(); - HashMap requiredEnvVars = setRequiredEnvVars(step, envVars); - - //check all the required env vars - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Test Result."); - return null; - } - - //check all the required parameters - HashMap requiredParams = new HashMap<>(); - String result = step.getResult(); - requiredParams.put("environment", step.getEnvironment()); - requiredParams.put("result", result); - - // optional build number, if user wants to set their own build number - String buildNumber = step.getBuildNumber(); - if (!Util.allNotNullOrEmpty(requiredParams, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Deploy Record."); - return null; - } - - if (result.equals(RESULT_SUCCESS) || result.equals(RESULT_FAIL)) { - PublishDeploy publishDeploy = new PublishDeploy(requiredEnvVars, requiredParams); - if (!Util.isNullOrEmpty(buildNumber)) { - publishDeploy.setBuildNumber(buildNumber); - } - publishDeploy.perform(build, ws, launcher, listener); - } else { - printStream.println("[IBM Cloud DevOps] the \"result\" in the publishDeployRecord should be either \"" - + RESULT_SUCCESS + "\" or \"" + RESULT_FAIL + "\""); - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Deploy Record."); - } - return null; - } -} - diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishSQStep.java b/src/main/java/com/ibm/devops/dra/steps/PublishSQStep.java deleted file mode 100644 index 995c668..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishSQStep.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import hudson.Extension; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class PublishSQStep extends AbstractDevOpsStep { - // required parameters - private String SQHostURL; - private String SQAuthToken; - private String SQProjectKey; - - // custom build number, optional - private String buildNumber; - private String environment; - - @DataBoundConstructor - public PublishSQStep(String SQHostURL, String SQAuthToken, String SQProjectKey) { - - this.SQHostURL = SQHostURL; - this.SQAuthToken = SQAuthToken; - this.SQProjectKey = SQProjectKey; - } - - @DataBoundSetter - public void setEnvironment(String environment) { - this.environment = environment; - } - - @DataBoundSetter - public void setSQHostURL(String SQHostURL) { - this.SQHostURL = SQHostURL; - } - - @DataBoundSetter - public void setSQAuthToken(String SQAuthToken) { - this.SQAuthToken = SQAuthToken; - } - - @DataBoundSetter - public void setSQProjectKey(String SQProjectKey) { - this.SQProjectKey = SQProjectKey; - } - - @DataBoundSetter - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getEnvironment() { - return environment; - } - - public String getSQHostURL() { - return SQHostURL; - } - - public String getSQAuthToken() { - return SQAuthToken; - } - - public String getSQProjectKey() { - return SQProjectKey; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { super(PublishSQStepExecution.class); } - - @Override - public String getFunctionName() { - return "publishSQResults"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Publish SonarQube test results to IBM Cloud DevOps"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishSQStepExecution.java b/src/main/java/com/ibm/devops/dra/steps/PublishSQStepExecution.java deleted file mode 100644 index 9804123..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishSQStepExecution.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import com.ibm.devops.dra.PublishSQ; -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; - -import javax.inject.Inject; -import java.io.PrintStream; -import java.util.HashMap; - -import static com.ibm.devops.dra.AbstractDevOpsAction.setRequiredEnvVars; - -public class PublishSQStepExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient PublishSQStep step; - - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient FilePath ws; - @StepContextParameter - private transient Launcher launcher; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - - PrintStream printStream = listener.getLogger(); - - HashMap requiredEnvVars = setRequiredEnvVars(step, envVars); - - //check all the required env vars - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Test Result."); - return null; - } - - //check all the required parameters - HashMap requiredParams = new HashMap<>(); - requiredParams.put("SQProjectKey", step.getSQProjectKey()); - requiredParams.put("SQHostURL", step.getSQHostURL()); - requiredParams.put("SQAuthToken", step.getSQAuthToken()); - - if (!Util.allNotNullOrEmpty(requiredParams, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload SonarQube Test Result."); - return null; - } - - // optional build number, if user wants to set their own build number - String buildNumber = step.getBuildNumber(); - - PublishSQ publisher = new PublishSQ(requiredEnvVars, requiredEnvVars); - - if (!Util.isNullOrEmpty(buildNumber)) { - publisher.setBuildNumber(buildNumber); - } - publisher.perform(build, ws, launcher, listener); - - return null; - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishTestStep.java b/src/main/java/com/ibm/devops/dra/steps/PublishTestStep.java deleted file mode 100644 index 6aa7948..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishTestStep.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import hudson.Extension; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class PublishTestStep extends AbstractDevOpsStep { - - // required parameters - private String type; - private String fileLocation; - // custom build number, optional - private String buildNumber; - - // optional form fields for fvt - private String environment; - - @DataBoundConstructor - public PublishTestStep(String type, String fileLocation) { - this.type = type; - this.fileLocation = fileLocation; - } - - @DataBoundSetter - public void setEnvironment(String environment) { - this.environment = environment; - } - - @DataBoundSetter - public void setBuildNumber(String buildNumber) { - this.buildNumber = buildNumber; - } - - public String getBuildNumber() { - return buildNumber; - } - - public String getEnvironment() { - return environment; - } - - public String getType() { - return type; - } - - public String getFileLocation() { - return fileLocation; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { super(PublishTestStepExecution.class); } - - @Override - public String getFunctionName() { - return "publishTestResult"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Publish test result to IBM Cloud DevOps"; - } - } -} diff --git a/src/main/java/com/ibm/devops/dra/steps/PublishTestStepExecution.java b/src/main/java/com/ibm/devops/dra/steps/PublishTestStepExecution.java deleted file mode 100644 index 02d43d5..0000000 --- a/src/main/java/com/ibm/devops/dra/steps/PublishTestStepExecution.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.dra.steps; - -import com.ibm.devops.dra.PublishTest; -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.FilePath; -import hudson.Launcher; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; - -import javax.inject.Inject; -import java.io.PrintStream; -import java.util.HashMap; - -import static com.ibm.devops.dra.AbstractDevOpsAction.setRequiredEnvVars; - -public class PublishTestStepExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient PublishTestStep step; - - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient FilePath ws; - @StepContextParameter - private transient Launcher launcher; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - - PrintStream printStream = listener.getLogger(); - - HashMap requiredEnvVars = setRequiredEnvVars(step, envVars); - - //check all the required env vars - if (!Util.allNotNullOrEmpty(requiredEnvVars, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Test Result."); - return null; - } - - //check all the required parameters - HashMap requiredParams = new HashMap<>(); - String type = step.getType(); - requiredParams.put("type", type); - requiredParams.put("fileLocation", step.getFileLocation()); - - // optional build number, if user wants to set their own build number - String buildNumber = step.getBuildNumber(); - String envName = step.getEnvironment(); - - if (!Util.allNotNullOrEmpty(requiredParams, printStream)) { - printStream.println("[IBM Cloud DevOps] Error: Failed to upload Test Result."); - return null; - } - - if (type.equals("unittest") || type.equals("code") || type.equals("fvt")) { - PublishTest publishTest = new PublishTest(requiredEnvVars, requiredParams); - - if (!Util.isNullOrEmpty(envName)) publishTest.setEnvName(envName); - if (!Util.isNullOrEmpty(buildNumber)) publishTest.setBuildNumber(buildNumber); - - publishTest.perform(build, ws, launcher, listener); - } else { - printStream.println("[IBM Cloud DevOps] the \"type\" in the publishTestResult should be either \"unittest\", \"code\" or \"fvt\""); - } - - return null; - } -} diff --git a/src/main/java/com/ibm/devops/notification/BuildListener.java b/src/main/java/com/ibm/devops/notification/BuildListener.java deleted file mode 100644 index 089836e..0000000 --- a/src/main/java/com/ibm/devops/notification/BuildListener.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - - - Copyright 2016, 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification; - -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.Extension; -import hudson.model.*; -import hudson.model.listeners.RunListener; -import net.sf.json.JSONObject; -import java.io.PrintStream; - -@Extension -public class BuildListener extends RunListener { - - public BuildListener(){ - super(AbstractBuild.class); - } - - @Override - public void onStarted(AbstractBuild r, TaskListener listener) { - handleEvent(r, listener, "STARTED"); - } - - @Override - public void onCompleted(AbstractBuild r, TaskListener listener) { - handleEvent(r, listener, "COMPLETED"); - } - - @Override - public void onFinalized(AbstractBuild r){ - handleEvent(r, TaskListener.NULL, "FINALIZED"); - } - - private void handleEvent(AbstractBuild r, TaskListener listener, String phase) { - OTCNotifier notifier = EventHandler.findPublisher(r); - PrintStream printStream = listener.getLogger(); - EnvVars envVars = EventHandler.getEnv(r, listener, printStream); - String webhook = Util.getWebhookUrl(envVars); - Result result = r.getResult(); - - // OTC Notifier - if(EventHandler.isRelevant(notifier, phase, result)) { - String resultString = null; - if(result != null){ - resultString = result.toString(); - } - - JSONObject message = MessageHandler.buildMessage(r, envVars, phase, resultString); - MessageHandler.postToWebhook(webhook, false, message, printStream); - } - - // deployable mapping - if(EventHandler.shouldPostDeployableMappingMessage(notifier, phase, result)) { - printStream.println("[IBM Cloud DevOps] Building Deployable Message."); - String resultString = null; - if(result != null){ - resultString = result.toString(); - } - - if (Util.validateEnvVariables(envVars, printStream)) { - JSONObject message = MessageHandler.buildDeployableMappingMessage(envVars, printStream); - printStream.println("[IBM Cloud DevOps] Sending Deployable Message."); - MessageHandler.postToWebhook(webhook, true, message, printStream); - } else { - printStream.println("[IBM Cloud DevOps] Not sending Deployable Message due to missing required property."); - } - } else { - printStream.println("[IBM Cloud DevOps] Not building Deployable Message."); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/notification/EventHandler.java b/src/main/java/com/ibm/devops/notification/EventHandler.java deleted file mode 100644 index aba80ac..0000000 --- a/src/main/java/com/ibm/devops/notification/EventHandler.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification; - -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.model.AbstractBuild; -import hudson.model.Result; -import hudson.model.TaskListener; -import hudson.tasks.Publisher; - -import java.io.IOException; -import java.io.PrintStream; -import java.util.List; - -public final class EventHandler { - /* - find OTCNotifer in the publisher list - */ - public static OTCNotifier findPublisher(AbstractBuild r){ - List publisherList = r.getProject().getPublishersList().toList(); - - //ensure that there is an OTCNotifier in the project - for(Publisher publisher: publisherList){ - if(publisher instanceof OTCNotifier){ - return (OTCNotifier) publisher; - } - } - - return null; - } - - /* - return the build env variables - */ - public static EnvVars getEnv(AbstractBuild r, TaskListener listener, PrintStream printStream){ - try { - return r.getEnvironment(listener); - } catch (IOException e) { - printStream.println("[IBM Cloud DevOps] Exception: "); - printStream.println("[IBM Cloud DevOps] Error: Failed to notify OTC."); - e.printStackTrace(printStream); - } catch (InterruptedException e) { - printStream.println("[IBM Cloud DevOps] Exception: "); - printStream.println("[IBM Cloud DevOps] Error: Failed to notify OTC."); - e.printStackTrace(printStream); - } - - return null; - } - - /* - Check job config to see if message should be sent. - */ - public static boolean isRelevant(OTCNotifier notifier, String phase, Result result){ - boolean onStarted; - boolean onCompleted; - boolean onFinalized; - boolean failureOnly; - - //Make sure OTC Notifier was found in the publisherList - if(notifier != null){ - onStarted = notifier.getOnStarted(); - onCompleted = notifier.getOnCompleted(); - onFinalized = notifier.getOnFinalized(); - failureOnly = notifier.getFailureOnly(); - - if(onStarted && "STARTED".equals(phase) || onCompleted && "COMPLETED".equals(phase) - || onFinalized && "FINALIZED".equals(phase)){//check selections - if(failureOnly && result != null && result.equals(Result.FAILURE) || !failureOnly){//check failureOnly - return true; - } - } - } - - return false; - } - - /* - Returns whether deployable mapping message should be sent. - */ - public static boolean shouldPostDeployableMappingMessage(OTCNotifier notifier, String phase, Result result){ - // publish deployable mapping message only is traceability is enabled, phase is completed and build is successful - if(notifier != null && notifier.getEnableTraceability()){ - if ("COMPLETED".equals(phase) && result.equals(Result.SUCCESS)){ - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/ibm/devops/notification/MessageHandler.java b/src/main/java/com/ibm/devops/notification/MessageHandler.java deleted file mode 100644 index 6a1d2e0..0000000 --- a/src/main/java/com/ibm/devops/notification/MessageHandler.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification; - -import com.ibm.devops.dra.AbstractDevOpsAction; -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import hudson.model.Run; -import hudson.model.Job; -import jenkins.model.Jenkins; -import net.sf.json.JSONArray; -import net.sf.json.JSONObject; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClients; -import java.io.IOException; -import java.io.PrintStream; - -//build message that will be posted to the webhook -public final class MessageHandler { - public static JSONObject buildMessage(Run r, EnvVars envVars, String phase, String result){ - JSONObject message = new JSONObject(); - JSONObject build = new JSONObject(); - JSONObject scm = new JSONObject(); - - Job job = r.getParent(); - String rootUrl = Jenkins.getInstance().getRootUrl(); - - //setup scm - if(envVars != null) { - String gitCommit = envVars.get("GIT_COMMIT"); - String gitBranch = envVars.get("GIT_BRANCH"); - String gitPreviousCommit = envVars.get("GIT_PREVIOUS_COMMIT"); - String gitPreviousSuccessfulCommit = envVars.get("GIT_PREVIOUS_SUCCESSFUL_COMMIT"); - String gitUrl = envVars.get("GIT_URL"); - String gitCommitterName = envVars.get("GIT_COMMITTER_NAME"); - String gitCommitterEmail = envVars.get("GIT_COMMITTER_EMAIL"); - String gitAuthorName = envVars.get("GIT_AUTHOR_NAME"); - String gitAuthorEmail = envVars.get("GIT_AUTHOR_EMAIL"); - - if (gitCommit != null) { - scm.put("git_commit", gitCommit); - } - - if (gitBranch != null) { - scm.put("git_branch", gitBranch); - } - - if (gitPreviousCommit != null) { - scm.put("git_previous_commit", gitPreviousCommit); - } - - if (gitPreviousSuccessfulCommit != null) { - scm.put("git_previous_successful_commit", gitPreviousSuccessfulCommit); - } - - if (gitUrl != null) { - scm.put("git_url", gitUrl); - } - - if (gitCommitterName != null) { - scm.put("git_committer_name", gitCommitterName); - } - - if (gitCommitterEmail != null) { - scm.put("git_committer_email", gitCommitterEmail); - } - - if (gitAuthorName != null) { - scm.put("git_author_name", gitAuthorName); - } - - if (gitAuthorEmail != null) { - scm.put("git_author_email", gitAuthorEmail); - } - } - - //setup the build object - build.put("number", r.getNumber()); - build.put("queue_id", r.getQueueId()); - build.put("phase", phase); - build.put("url", r.getUrl()); - - if(rootUrl != null){ - build.put("full_url", rootUrl + r.getUrl()); - } else{ - build.put("full_url", ""); - } - - if(result != null){ - build.put("status", result); - } - - if(!"STARTED".equals(phase)) { - build.put("duration", r.getDuration()); - } - - build.put("scm", scm); - - //setup the message - message.put("name", job.getName()); - message.put("url", job.getUrl()); - message.put("build", build); - - return message; - } - - //post message to webhook - public static void postToWebhook(String webhook, boolean deployableMessage, JSONObject message, PrintStream printStream){ - //check webhook - if(Util.isNullOrEmpty(webhook)){ - printStream.println("[IBM Cloud DevOps] IBM_CLOUD_DEVOPS_WEBHOOK_URL not set."); - printStream.println("[IBM Cloud DevOps] Error: Failed to notify OTC."); - } else { - // set a 5 seconds timeout - RequestConfig defaultRequestConfig = RequestConfig.custom() - .setSocketTimeout(5000) - .setConnectTimeout(5000) - .setConnectionRequestTimeout(5000) - .build(); - CloseableHttpClient httpClient = HttpClients.custom() - .setDefaultRequestConfig(defaultRequestConfig) - .build(); - HttpPost postMethod = new HttpPost(webhook); - try { - StringEntity data = new StringEntity(message.toString()); - postMethod.setEntity(data); - postMethod = Proxy.addProxyInformation(postMethod); - postMethod.addHeader("Content-Type", "application/json"); - - if (deployableMessage) { - postMethod.addHeader("x-create-connection", "true"); - printStream.println("[IBM Cloud DevOps] Sending Deployable Mapping message to webhook:"); - printStream.println(message); - } else { - printStream.println("[IBM Cloud DevOps] Sending DLMS message to webhook:"); - printStream.println(message); - } - - CloseableHttpResponse response = httpClient.execute(postMethod); - - if (response.getStatusLine().toString().matches(".*2([0-9]{2}).*")) { - printStream.println("[IBM Cloud DevOps] Message successfully posted to webhook."); - } else { - printStream.println("[IBM Cloud DevOps] Message failed, response status: " + response.getStatusLine()); - } - } catch (IOException e) { - printStream.println("[IBM Cloud DevOps] IOException, could not post to webhook:"); - e.printStackTrace(printStream); - } - } - } - - public static JSONObject buildDeployableMappingMessage(EnvVars envVars, PrintStream printStream){ - String environment = null; - // for debugging purpose only, uncomment the line below - // environment = "dev"; // to target YS1 - try { - JSONObject deployableMappingMessage; - // API - String webHookUrl= Util.getWebhookUrl(envVars); - environment= Util.getTargetEnv(webHookUrl, printStream); - String apiUrl= AbstractDevOpsAction.chooseTargetAPI(environment); - - // get bluemix token first - String userId= Util.getUser(envVars); - String pwd= Util.getPassword(envVars); - - String bluemixToken = AbstractDevOpsAction.getBluemixToken(userId, pwd, apiUrl); - - // org details - JSONObject org = new JSONObject(); - String orgName = Util.getOrg(envVars); - org.put("Name" , orgName); - String orgId= AbstractDevOpsAction.getOrgId(bluemixToken, orgName, environment, false); - org.put("Guid" , orgId); - - // space details - JSONObject space = new JSONObject(); - String spaceName = Util.getSpace(envVars); - space.put("Name" , spaceName); - String spaceId= AbstractDevOpsAction.getSpaceId(bluemixToken, spaceName, environment, false); - space.put("Guid" , spaceId); - - // app details - JSONObject app = new JSONObject(); - String appName = Util.getAppName(envVars); - app.put("Name" , appName); - String appId= AbstractDevOpsAction.getAppId(bluemixToken, appName, orgName, spaceName, environment, false); - app.put("Guid" , appId); - - // Git - JSONArray gitData= MessageUtil.buildGitData(envVars, printStream); - - // format deployable message - deployableMappingMessage = MessageUtil.formatDeployableMappingMessage(org, space, app, apiUrl, gitData, printStream); - - return deployableMappingMessage; - - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Unexpected Exception encountered while building deployable message:"); - e.printStackTrace(printStream); - } - - return new JSONObject(); - } -} diff --git a/src/main/java/com/ibm/devops/notification/MessageUtil.java b/src/main/java/com/ibm/devops/notification/MessageUtil.java deleted file mode 100644 index 8bccbcd..0000000 --- a/src/main/java/com/ibm/devops/notification/MessageUtil.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification; - -import com.ibm.devops.dra.Util; -import hudson.EnvVars; -import java.io.PrintStream; -import net.sf.json.JSONArray; -import net.sf.json.JSONObject; - -/** - * Message Utilities functions - */ - -public class MessageUtil { - - public static JSONArray buildGitData(EnvVars envVars, PrintStream printStream) { - try { - String gitUrl = Util.getGitRepoUrl(envVars); - String gitBranch = Util.getGitBranch(envVars); - String gitCommit = Util.getGitCommit(envVars); - - JSONObject gitInfo = new JSONObject(); - gitInfo.put("GitURL" , gitUrl); - gitInfo.put("GitBranch" , gitBranch); - gitInfo.put("GitCommitID" , gitCommit); - - JSONArray gitData = new JSONArray(); - gitData.add(gitInfo); - - return gitData; - - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Error: Failed to build Git data."); - e.printStackTrace(printStream); - throw e; - } - } - - public static JSONObject formatDeployableMappingMessage(JSONObject org, JSONObject space, JSONObject app, String apiUrl, JSONArray gitData, PrintStream printStream) { - try { - JSONObject deployableMappingMessage = new JSONObject(); - deployableMappingMessage.put("Org" , org); - deployableMappingMessage.put("Space" , space); - deployableMappingMessage.put("App" , app); - deployableMappingMessage.put("ApiEndpoint" , apiUrl); - deployableMappingMessage.put("Method" , "POST"); - deployableMappingMessage.put("GitData" , gitData); - return deployableMappingMessage; - - } catch (Exception e) { - printStream.println("[IBM Cloud DevOps] Error: Failed to build Deployable Mapping Message."); - e.printStackTrace(printStream); - throw e; - } - } -} diff --git a/src/main/java/com/ibm/devops/notification/OTCNotifier.java b/src/main/java/com/ibm/devops/notification/OTCNotifier.java deleted file mode 100644 index a75793e..0000000 --- a/src/main/java/com/ibm/devops/notification/OTCNotifier.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification; - -import hudson.Extension; -import hudson.Launcher; -import hudson.model.AbstractBuild; -import hudson.model.AbstractProject; -import hudson.model.BuildListener; -import hudson.tasks.BuildStepDescriptor; -import hudson.tasks.BuildStepMonitor; -import hudson.tasks.Notifier; -import hudson.tasks.Publisher; -import org.kohsuke.stapler.DataBoundConstructor; -import java.io.IOException; - -public class OTCNotifier extends Notifier { - private boolean onStarted; - private boolean onCompleted; - private boolean onFinalized; - private boolean failureOnly; - private boolean enableTraceability; - - /* - The paramater names in @DataBoundConstructor need to match the fields in config.jelly exactly - */ - @DataBoundConstructor - public OTCNotifier(boolean onStarted, - boolean onCompleted, - boolean onFinalized, - boolean failureOnly, - boolean enableTraceability - ){ - this.onStarted = onStarted; - this.onCompleted = onCompleted; - this.onFinalized = onFinalized; - this.failureOnly = failureOnly; - this.enableTraceability = enableTraceability; - } - - /* - These methods are called by jenkins to populate the per-job config fields - */ - public Boolean getOnStarted(){ - return this.onStarted; - } - - public Boolean getOnCompleted(){ - return this.onCompleted; - } - - public Boolean getOnFinalized(){ - return this.onFinalized; - } - - public Boolean getFailureOnly(){ - return this.failureOnly; - } - - public Boolean getEnableTraceability(){ - return this.enableTraceability; - } - - public BuildStepMonitor getRequiredMonitorService() { - return BuildStepMonitor.NONE; - } - - @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { - return true; - } - - @Override - public DescriptorImpl getDescriptor() { - return (DescriptorImpl)super.getDescriptor(); - } - - /* - The descriptor allows global configs for your plugin, this class will be passed to every instance of the plugin. - */ - @Extension // This indicates to Jenkins that this is an implementation of an extension point. - public static final class DescriptorImpl extends BuildStepDescriptor { - @Override - public String getDisplayName() { - return "Notify OTC";//This is the plugin name in the config - } - - public boolean isApplicable(Class aClass) { - return true; //It is always ok for someone to add this as a build step - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationExecution.java b/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationExecution.java deleted file mode 100644 index 0d38910..0000000 --- a/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationExecution.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification.steps; - -import com.ibm.devops.dra.Util; -import com.ibm.devops.notification.MessageHandler; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; -import javax.inject.Inject; -import hudson.EnvVars; -import hudson.model.Run; -import hudson.model.TaskListener; -import net.sf.json.JSONObject; - - -import java.io.PrintStream; - -public class DeployableMappingNotificationExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient DeployableMappingNotificationStep step; - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - PrintStream printStream = listener.getLogger(); - String webhookUrl; - - if(Util.isNullOrEmpty(step.getWebhookUrl())){ - webhookUrl = Util.getWebhookUrl(envVars); - } else { - webhookUrl = step.getWebhookUrl(); - } - - String status = step.getStatus().trim(); - //check all the required env vars - if (!Util.allNotNullOrEmpty(status)) { - printStream.println("[IBM Cloud DevOps] Required parameter null or empty."); - printStream.println("[IBM Cloud DevOps] Error: Failed to notify OTC."); - return null; - } - - if ("SUCCESS".equals(status)) { // send deployable mapping message on for successful builds - JSONObject message = MessageHandler.buildDeployableMappingMessage(envVars, printStream); - MessageHandler.postToWebhook(webhookUrl, true, message, printStream); - } - return null; - } -} diff --git a/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationStep.java b/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationStep.java deleted file mode 100644 index 2d479ab..0000000 --- a/src/main/java/com/ibm/devops/notification/steps/DeployableMappingNotificationStep.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification.steps; - -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import hudson.Extension; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class DeployableMappingNotificationStep extends AbstractStepImpl { - //required parameter to support pipeline script - private String status; - - //option parameters - private String webhookUrl; - - @DataBoundConstructor - public DeployableMappingNotificationStep(String status){ - this.status = status; - } - - @DataBoundSetter - public void setWebhookUrl(String webhookUrl){ - this.webhookUrl = webhookUrl; - } - - public String getWebhookUrl(){ - return this.webhookUrl; - } - - public String getStatus(){ - return this.status; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - public DescriptorImpl() { super(DeployableMappingNotificationExecution.class); } - - @Override - public String getFunctionName() { - return "sendDeployableMessage"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Send deployable mapping message to OTC"; - } - } -} diff --git a/src/main/java/com/ibm/devops/notification/steps/OTCNotificationExecution.java b/src/main/java/com/ibm/devops/notification/steps/OTCNotificationExecution.java deleted file mode 100644 index 185f627..0000000 --- a/src/main/java/com/ibm/devops/notification/steps/OTCNotificationExecution.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification.steps; - -import com.ibm.devops.dra.Util; -import com.ibm.devops.notification.MessageHandler; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; -import javax.inject.Inject; -import hudson.EnvVars; -import hudson.model.Run; -import hudson.model.TaskListener; -import net.sf.json.JSONObject; - - -import java.io.PrintStream; - -public class OTCNotificationExecution extends AbstractSynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; - @Inject - private transient OTCNotificationStep step; - @StepContextParameter - private transient TaskListener listener; - @StepContextParameter - private transient Run build; - @StepContextParameter - private transient EnvVars envVars; - - @Override - protected Void run() throws Exception { - String stageName = step.getStageName().trim(); - String status = step.getStatus().trim(); - PrintStream printStream = listener.getLogger(); - String webhookUrl; - - if(Util.isNullOrEmpty(step.getWebhookUrl())){ - webhookUrl = Util.getWebhookUrl(envVars); - } else { - webhookUrl = step.getWebhookUrl(); - } - - //check all the required env vars - if (!Util.allNotNullOrEmpty(stageName, status)) { - printStream.println("[IBM Cloud DevOps] Required parameter null or empty."); - printStream.println("[IBM Cloud DevOps] Error: Failed to notify OTC."); - return null; - } - - JSONObject message = MessageHandler.buildMessage(build, envVars, stageName, status); - MessageHandler.postToWebhook(webhookUrl, false, message, printStream); - - return null; - } -} diff --git a/src/main/java/com/ibm/devops/notification/steps/OTCNotificationStep.java b/src/main/java/com/ibm/devops/notification/steps/OTCNotificationStep.java deleted file mode 100644 index 34f9409..0000000 --- a/src/main/java/com/ibm/devops/notification/steps/OTCNotificationStep.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - - - Copyright 2017 IBM Corporation - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */ - -package com.ibm.devops.notification.steps; - -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import hudson.Extension; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; - -import javax.annotation.Nonnull; - -public class OTCNotificationStep extends AbstractStepImpl { - //required parameter to support pipeline script - private String status; - private String stageName; - - //option parameters - private String webhookUrl; - - @DataBoundConstructor - public OTCNotificationStep(String stageName, String status){ - this.stageName = stageName; - this.status = status; - } - - @DataBoundSetter - public void setWebhookUrl(String webhookUrl){ - this.webhookUrl = webhookUrl; - } - - public String getWebhookUrl(){ - return this.webhookUrl; - } - - public String getStatus(){ - return this.status; - } - - public String getStageName(){ - return this.stageName; - } - - @Extension - public static class DescriptorImpl extends AbstractStepDescriptorImpl { - public DescriptorImpl() { super(OTCNotificationExecution.class); } - - @Override - public String getFunctionName() { - return "notifyOTC"; - } - - @Nonnull - @Override - public String getDisplayName() { - return "Send notification to OTC"; - } - } -} diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/config.jelly similarity index 84% rename from src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly rename to src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/config.jelly index d19e4e6..205ebad 100644 --- a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/config.jelly +++ b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/config.jelly @@ -26,18 +26,7 @@ tags they use. Views are always organized according to its owner class, so it should be straightforward to find them. --> - - - - - - - - - + @@ -52,8 +41,6 @@

- - diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-consoleUrl.html b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-consoleUrl.html similarity index 100% rename from src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-consoleUrl.html rename to src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-consoleUrl.html diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-instanceName.html similarity index 100% rename from src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-instanceName.html rename to src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-instanceName.html diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-syncId.html similarity index 100% rename from src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncId.html rename to src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-syncId.html diff --git a/src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html b/src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-syncToken.html similarity index 100% rename from src/main/resources/com/ibm/devops/dra/DevOpsGlobalConfiguration/help-syncToken.html rename to src/main/resources/com/ibm/devops/connect/DevOpsGlobalConfiguration/help-syncToken.html diff --git a/src/main/resources/com/ibm/devops/dra/BuildPublisherAction/summary.jelly b/src/main/resources/com/ibm/devops/dra/BuildPublisherAction/summary.jelly deleted file mode 100755 index e1afa0a..0000000 --- a/src/main/resources/com/ibm/devops/dra/BuildPublisherAction/summary.jelly +++ /dev/null @@ -1,26 +0,0 @@ - - - - - -

IBM Cloud DevOps

- -

- Please click here to view the build status of all application - -

- - -
diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/config.jelly b/src/main/resources/com/ibm/devops/dra/EvaluateGate/config.jelly deleted file mode 100644 index 6b307a6..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/config.jelly +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - -
diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-additionalBuildInfo.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-additionalBuildInfo.html deleted file mode 100644 index 1b141d8..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-additionalBuildInfo.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box if the deployed app is not built in Jenkins. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-applicationName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-applicationName.html deleted file mode 100644 index 123f11f..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-applicationName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type a name for the application that information is being uploaded for. Use this application name when you configure DevOps Insights gates. You can use an environment variable, such as $APP_NAME. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildJobName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildJobName.html deleted file mode 100644 index 4a4049b..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildJobName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Enter the name of the build job that triggers this test job. You can use an environment variable, such as $BUILD_JOB. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildNumber.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildNumber.html deleted file mode 100644 index 1f71cd5..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-buildNumber.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Specify the number for external builds. You can define this number as an environment variable. . -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-credentialsId.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-credentialsId.html deleted file mode 100644 index 941d6ed..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-credentialsId.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a Bluemix ID from the menu. If no Bluemix IDs are in the menu, click Add to add one. Click Test Connection to verify the selected credentials. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-envName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-envName.html deleted file mode 100644 index ffc39b0..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-envName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the environment that this job deploys to. If this environment is your staging environment, type "STAGING." If this environment is your production requirement, type "PRODUCTION." If you do not specify staging and production environments, DevOps Insights cannot completely analyze your project. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-orgName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-orgName.html deleted file mode 100644 index f8feb78..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-orgName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the Bluemix organization that you want to use. You can type the name here or use an environment variable, such as $ORG_NAME, that is defined elsewhere. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-policyName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-policyName.html deleted file mode 100644 index cc57ab5..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-policyName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select the policy that you want this gate to enforce. You can create more policies in DevOps Insights. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-toolchainName.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-toolchainName.html deleted file mode 100644 index 42b2702..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-toolchainName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a toolchain. If you have not created a toolchain yet, create one here. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-willDisrupt.html b/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-willDisrupt.html deleted file mode 100644 index cff206c..0000000 --- a/src/main/resources/com/ibm/devops/dra/EvaluateGate/help-willDisrupt.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box to cancel associated Jenkins builds when this gate fails. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/GatePublisherAction/summary.jelly b/src/main/resources/com/ibm/devops/dra/GatePublisherAction/summary.jelly deleted file mode 100755 index 9e7861b..0000000 --- a/src/main/resources/com/ibm/devops/dra/GatePublisherAction/summary.jelly +++ /dev/null @@ -1,30 +0,0 @@ - - - - - -

IBM Cloud DevOps

-

Policy Name: ${it.policyName}

-

Decision: ${it.decision}

-

- Click here to view the Gate Evaluation Report. -

-

- Click here to view the Deployment Risk Dashboard. -

- - - -
diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/config.jelly b/src/main/resources/com/ibm/devops/dra/PublishBuild/config.jelly deleted file mode 100644 index 73f92de..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/config.jelly +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-additionalBuildInfo.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-additionalBuildInfo.html deleted file mode 100644 index 5662e10..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-additionalBuildInfo.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box if you want to set your own build number. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-applicationName.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-applicationName.html deleted file mode 100644 index 123f11f..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-applicationName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type a name for the application that information is being uploaded for. Use this application name when you configure DevOps Insights gates. You can use an environment variable, such as $APP_NAME. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-buildNumber.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-buildNumber.html deleted file mode 100644 index 6ca7fb8..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-buildNumber.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Specify the custom build number for this job. You can define this number as an environment variable. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-credentialsId.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-credentialsId.html deleted file mode 100644 index cd32d9a..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-credentialsId.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a Bluemix ID from the menu. If no Bluemix IDs are in the menu, click Add to add one. Click Test Connection to verify the selected credentials. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-orgName.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-orgName.html deleted file mode 100644 index f8feb78..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-orgName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the Bluemix organization that you want to use. You can type the name here or use an environment variable, such as $ORG_NAME, that is defined elsewhere. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-toolchainName.html b/src/main/resources/com/ibm/devops/dra/PublishBuild/help-toolchainName.html deleted file mode 100644 index 42b2702..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishBuild/help-toolchainName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a toolchain. If you have not created a toolchain yet, create one here. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/config.jelly b/src/main/resources/com/ibm/devops/dra/PublishDeploy/config.jelly deleted file mode 100644 index b2ebfc9..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/config.jelly +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-additionalBuildInfo.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-additionalBuildInfo.html deleted file mode 100644 index 999c790..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-additionalBuildInfo.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box if you want to set your own build number -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationName.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationName.html deleted file mode 100644 index 123f11f..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type a name for the application that information is being uploaded for. Use this application name when you configure DevOps Insights gates. You can use an environment variable, such as $APP_NAME. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationUrl.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationUrl.html deleted file mode 100644 index 3c86d17..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-applicationUrl.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Optional: If this is a web application, enter its URL. You can use an environment variable, such as $APP_URL. -
diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildJobName.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildJobName.html deleted file mode 100644 index 243dd3b..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildJobName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Enter the name of the build job that triggers this test job. You can use an environment variable, such as $BUILD_JOB. -
diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildNumber.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildNumber.html deleted file mode 100644 index 6ca7fb8..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-buildNumber.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Specify the custom build number for this job. You can define this number as an environment variable. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-credentialsId.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-credentialsId.html deleted file mode 100644 index cd32d9a..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-credentialsId.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a Bluemix ID from the menu. If no Bluemix IDs are in the menu, click Add to add one. Click Test Connection to verify the selected credentials. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-environmentName.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-environmentName.html deleted file mode 100644 index 731ad3a..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-environmentName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the environment that this job deploys to. If this environment is your staging environment, type "STAGING." If this environment is your production requirement, type "PRODUCTION." If you do not specify staging and production environments, DevOps Insights cannot completely analyze your project. -
diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-orgName.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-orgName.html deleted file mode 100644 index f8feb78..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-orgName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the Bluemix organization that you want to use. You can type the name here or use an environment variable, such as $ORG_NAME, that is defined elsewhere. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-toolchainName.html b/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-toolchainName.html deleted file mode 100644 index 42b2702..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishDeploy/help-toolchainName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a toolchain. If you have not created a toolchain yet, create one here. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/config.jelly b/src/main/resources/com/ibm/devops/dra/PublishSQ/config.jelly deleted file mode 100644 index 6f8d784..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/config.jelly +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQAuthToken.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQAuthToken.html deleted file mode 100644 index 2070328..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQAuthToken.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Enter your API token that SonarQube generated for you. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQHostName.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQHostName.html deleted file mode 100644 index f669942..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQHostName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the hostname of the server that your SonarQube instance runs on. Do not enter a trailing slash. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQProjectKey.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQProjectKey.html deleted file mode 100644 index cc3785b..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-SQProjectKey.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the key of the SonarQube project that you wish to scan. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-additionalBuildInfo.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-additionalBuildInfo.html deleted file mode 100644 index 999c790..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-additionalBuildInfo.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box if you want to set your own build number -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-applicationName.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-applicationName.html deleted file mode 100644 index 5faec8c..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-applicationName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type a name for the application that information is being uploaded for. Use this application name when you configure DevOps Insights gates. You can use an environment variable, such as $APP_NAME. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildJobName.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildJobName.html deleted file mode 100644 index 08847b7..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildJobName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Enter the name of the build job that triggers this test job. You can use an environment variable, such as $BUILD_JOB. -
diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildNumber.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildNumber.html deleted file mode 100644 index 6ca7fb8..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-buildNumber.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Specify the custom build number for this job. You can define this number as an environment variable. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-credentialsId.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-credentialsId.html deleted file mode 100644 index 8fef5fe..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-credentialsId.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a Bluemix ID from the menu. If no Bluemix IDs are in the menu, click Add to add one. Click Test Connection to verify the selected credentials. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-orgName.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-orgName.html deleted file mode 100644 index dfe4277..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-orgName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the Bluemix organization that you want to use. You can type the name here or use an environment variable, such as $ORG_NAME, that is defined elsewhere. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-toolchainName.html b/src/main/resources/com/ibm/devops/dra/PublishSQ/help-toolchainName.html deleted file mode 100644 index 0aec8dc..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishSQ/help-toolchainName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a toolchain. If you have not created a toolchain yet, create one here. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/config.jelly b/src/main/resources/com/ibm/devops/dra/PublishTest/config.jelly deleted file mode 100644 index 03a5d8a..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/config.jelly +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalBuildInfo.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalBuildInfo.html deleted file mode 100644 index 999c790..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalBuildInfo.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select this check box if you want to set your own build number -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalContents.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalContents.html deleted file mode 100644 index ea3336e..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalContents.html +++ /dev/null @@ -1,20 +0,0 @@ - - -
- Enter the test result file location relative to the root directory. The result file must contain results in the format that you selected for the metric type. This field supports wildcards and environment variables. - If you leave this field empty, DevOps Insights generates a simple test report that is based on job status. - Mocha, KarmaMocha, Istanbul, and BlanketJS test results must be in the JSON format. - xUnit test results must be in the XML format. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalLifecycleStage.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalLifecycleStage.html deleted file mode 100644 index bb6b5c3..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalLifecycleStage.html +++ /dev/null @@ -1,22 +0,0 @@ - - -
- Select the type of test. Your tests must correspond to rules in policies. -
    - Supported formats: -
  • Code coverage: Istanbul, BlanketJS
  • -
  • Unit and functional verification tests: Mocha, xUnit, and Karma Mocha
  • -
-
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalUpload.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalUpload.html deleted file mode 100644 index 5702e51..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-additionalUpload.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Optional: You can upload another test result file and select another metric type in this job. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-applicationName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-applicationName.html deleted file mode 100644 index 3f5adfd..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-applicationName.html +++ /dev/null @@ -1,18 +0,0 @@ - - -
- Type a name for the application that information is being uploaded for. Use this application name when you configure DevOps Insights gates. You can use an environment variable, such as $APP_NAME. -
- diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildJobName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildJobName.html deleted file mode 100644 index 4a4049b..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildJobName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Enter the name of the build job that triggers this test job. You can use an environment variable, such as $BUILD_JOB. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildNumber.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildNumber.html deleted file mode 100644 index 6ca7fb8..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-buildNumber.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Specify the custom build number for this job. You can define this number as an environment variable. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-contents.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-contents.html deleted file mode 100644 index ea3336e..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-contents.html +++ /dev/null @@ -1,20 +0,0 @@ - - -
- Enter the test result file location relative to the root directory. The result file must contain results in the format that you selected for the metric type. This field supports wildcards and environment variables. - If you leave this field empty, DevOps Insights generates a simple test report that is based on job status. - Mocha, KarmaMocha, Istanbul, and BlanketJS test results must be in the JSON format. - xUnit test results must be in the XML format. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-credentialsId.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-credentialsId.html deleted file mode 100644 index cd32d9a..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-credentialsId.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a Bluemix ID from the menu. If no Bluemix IDs are in the menu, click Add to add one. Click Test Connection to verify the selected credentials. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-envName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-envName.html deleted file mode 100644 index c89fb18..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-envName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the deployment environment where the tests are run. This name must match the environment name that is used in upstream test or deployment information upload jobs, such as "STAGING" or "PRODUCTION." -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-environment.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-environment.html deleted file mode 100644 index 4416bf6..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-environment.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Choose the environment that the test runs on. If the test runs during the build process, click Build environment.>. If the test runs on a deployed application, click Deploy environment and enter the environment name. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-lifecycleStage.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-lifecycleStage.html deleted file mode 100644 index bb6b5c3..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-lifecycleStage.html +++ /dev/null @@ -1,22 +0,0 @@ - - -
- Select the type of test. Your tests must correspond to rules in policies. -
    - Supported formats: -
  • Code coverage: Istanbul, BlanketJS
  • -
  • Unit and functional verification tests: Mocha, xUnit, and Karma Mocha
  • -
-
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-orgName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-orgName.html deleted file mode 100644 index f8feb78..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-orgName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Type the name of the Bluemix organization that you want to use. You can type the name here or use an environment variable, such as $ORG_NAME, that is defined elsewhere. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-policyName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-policyName.html deleted file mode 100644 index 5875757..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-policyName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- To evaluate test results in this job, select this check box and select a policy. The job uploads information and acts as a gate. You can create more policies in DevOps Insights. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/dra/PublishTest/help-toolchainName.html b/src/main/resources/com/ibm/devops/dra/PublishTest/help-toolchainName.html deleted file mode 100644 index 42b2702..0000000 --- a/src/main/resources/com/ibm/devops/dra/PublishTest/help-toolchainName.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- Select a toolchain. If you have not created a toolchain yet, create one here. -
\ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/notification/OTCNotifier/config.jelly b/src/main/resources/com/ibm/devops/notification/OTCNotifier/config.jelly deleted file mode 100644 index b587e69..0000000 --- a/src/main/resources/com/ibm/devops/notification/OTCNotifier/config.jelly +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/com/ibm/devops/notification/OTCNotifier/help.html b/src/main/resources/com/ibm/devops/notification/OTCNotifier/help.html deleted file mode 100644 index 4f7de10..0000000 --- a/src/main/resources/com/ibm/devops/notification/OTCNotifier/help.html +++ /dev/null @@ -1,21 +0,0 @@ - - -
- Please create a String Parameter named "IBM_CLOUD_DEVOPS_WEBHOOK_URL" and set the default value to the webhook that you'd like to send messages to. -
-
-
- Select Track deployment of code changes check box to track the deployment of code changes by creating tags, labels and comments on commits, pull requests and referenced issues. -
\ No newline at end of file From 91b0d852842a79f146bb13ccab1ea89429efad51 Mon Sep 17 00:00:00 2001 From: aberkeb1 Date: Tue, 27 Feb 2018 13:54:52 -0500 Subject: [PATCH 24/25] Forgot to commit the pom --- pom.xml | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index dfa189d..f403098 100644 --- a/pom.xml +++ b/pom.xml @@ -14,10 +14,10 @@ 4.0.0 com.ibm.devops - ibm-cloud-devops - 1.1.7-SNAPSHOT + ibm-continuous-release + 1.1.0 hpi - IBM Cloud DevOps + IBM Continuous Release This plugin can be used to integrate your Jenkins pipelines and jobs with IBM DevOps Insights offering. The IBM DevOps Insights offering tracks your deployment risk based on the data that you publish to it. It provides Gates so that you can stop builds from getting deployed if the Gate policy is not met. The offering also provides analysis of your Git repo. For example, it can provide trends for large commits, error prone files, and team dynamics. Very soon, the offering will provide more trends, e.g., Deploy Frequency, Build Frequency, Unit Tests and Code Coverage and Function Verification Tests for builds that have deployed to production or other environments. https://wiki.jenkins-ci.org/display/JENKINS/IBM+Cloud+DevOps+Plugin @@ -73,29 +73,9 @@ - xunrongl - Xunrong Li - xunrongli@us.ibm.com - - - aggarwav - Vijay Aggarwal - aggarwav@us.ibm.com - - - imvijay2007 - Vijay Jegaselvan - vjegase@us.ibm.com - - - ejodet - Eric Jodet - eric_jodet@fr.ibm.com - - - patjoy - Patrick Joy - patrick.joy1@ibm.com + aberk + Andy Berkebile + aberkeb1@us.ibm.com @@ -223,9 +203,9 @@ 2.11.2 - org.eclipse.hudson.plugins + org.jenkins-ci.plugins git - 3.0.1 + 3.6.0 org.jenkins-ci.plugins.workflow @@ -243,5 +223,11 @@ 1.0.7 provided + + com.ibm.devops + ibm-cloud-devops + 1.1.18 + true + \ No newline at end of file From 8a8270f88ad8d5bc420519892a9bcaac1c2d28dd Mon Sep 17 00:00:00 2001 From: Jerome Biotidara Date: Tue, 27 Feb 2018 15:37:54 -0500 Subject: [PATCH 25/25] Addressed issue where there are no build steps when job is triggered from CR --- .../ibm/devops/connect/Status/JenkinsPipelineStatus.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java b/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java index 3f4877c..5b8aac4 100644 --- a/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java +++ b/src/main/java/com/ibm/devops/connect/Status/JenkinsPipelineStatus.java @@ -91,7 +91,11 @@ protected void evaluatePipelineStep() { } else if(!newStep && node != null) { if(node.getError() == null) { - cloudCause.updateLastStep(null, JobStatus.success.toString(), "Stage is successful", false); + if(cloudCause.isCreatedByCR()) { + cloudCause.updateLastStep(null, JobStatus.success.toString(), "Stage is successful", false); + } else { + cloudCause.addStep(null, JobStatus.success.toString(), "Stage is successful", false); + } } else { cloudCause.updateLastStep(null, JobStatus.failure.toString(), node.getError().getDisplayName(), false); }