From 26dc5c5e70d0a45efc9373e9af35116bb1fb32a2 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:44:28 +0200 Subject: [PATCH 001/262] updated to Jenkins 1.580 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3b67fa1d..43714f71 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.jenkins-ci.plugins plugin - 1.509 + 1.580 Parameterized-Remote-Trigger From 80369af9bf262b91e84ddf8a8f82e20f9e0ce4c3 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:45:29 +0200 Subject: [PATCH 002/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-2.2.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 43714f71..0b9e2dd2 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ Parameterized-Remote-Trigger - 2.2.1-SNAPSHOT + 2.2.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -41,7 +41,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-2.2.1 From a1904cf302de460ab1e767170ba58123b835139a Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:45:32 +0200 Subject: [PATCH 003/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0b9e2dd2..99628455 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ Parameterized-Remote-Trigger - 2.2.1 + 2.2.2-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -41,7 +41,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-2.2.1 + HEAD From 6b75fa28c2c0af464b072fb261db0759485c3f85 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:52:05 +0200 Subject: [PATCH 004/262] ignore javadoc errors during release --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index 99628455..558afa71 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,13 @@ org.jenkins-ci.tools maven-hpi-plugin 1.95 + + + maven-javadoc-plugin + 2.10.3 + + false + From 1a3db32fa458f54785843473c4a7bdfeb6f9af17 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:56:01 +0200 Subject: [PATCH 005/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-2.2.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 558afa71..f521613d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ Parameterized-Remote-Trigger - 2.2.2-SNAPSHOT + 2.2.2 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -48,7 +48,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-2.2.2 From e281cd657ae9b31f6b2fff7b3339bbd91b7ab99e Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 12:56:05 +0200 Subject: [PATCH 006/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f521613d..edcbbe43 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ Parameterized-Remote-Trigger - 2.2.2 + 2.2.3-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -48,7 +48,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-2.2.2 + HEAD From 21b56c538ec9d76d3c8782b427a20e84b4e13c30 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 13:02:15 +0200 Subject: [PATCH 007/262] Fix links to changelog and screenshots --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 479ca10a..123aac61 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,20 @@ This plugin also has support for build authorization tokens (as defined [here](h - [Credentials Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Credentials+Plugin) - [Token Macro Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Token+Macro+Plugin) -Please take a look at the [change log](https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/blob/master/CHANGELOG.md) for a complete list of features and what not. +Please take a look at the [change log](CHANGELOG.md) for a complete list of features and what not. ###Screenshots System configuration option -![System onfiguration option](https://raw.github.com/morficus/Parameterized-Remote-Trigger-Plugin/master/screenshots/1-system-settings.png) +![System onfiguration option](screenshots/1-system-settings.png) Job setup options -![select from drop-down](https://raw.github.com/morficus/Parameterized-Remote-Trigger-Plugin/master/screenshots/2-build-configuration-1.png) +![select from drop-down](screenshots/2-build-configuration-1.png) -![Job setup options](https://raw.github.com/morficus/Parameterized-Remote-Trigger-Plugin/master/screenshots/3-build-configuration-2.png) +![Job setup options](screenshots/3-build-configuration-2.png) ####Current Limitations From 535bd4379af1c8b4ef6b4b0369dd7711d65d597f Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 13:03:26 +0200 Subject: [PATCH 008/262] replay changes from https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/commit/8845aaa489c1e260eded85105e517c925ed0e6cf --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82cd1b36..e3b3a58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -#2.1.4 (May 12th, 2015) +#2.2.0 (May 12th, 2015) ###New Feature/Enhancement: - Ability to debug connection errors with (optional) enhanced console output ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/9)) ###Bug fixes: -- fing [JENKINS-23748](https://issues.jenkins-ci.org/browse/JENKINS-23748) - Better error handleing for console output and logs to display info about the failure ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/10)) +- fixing [JENKINS-23748](https://issues.jenkins-ci.org/browse/JENKINS-23748) - Better error handleing for console output and logs to display info about the failure ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/10)) - Don't fail build on 404 ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/8)) - Fixed unhandled NPE ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/7)) - Hand-full of other bugs ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/5/commits)): From 6e15739634caac29e71bcf589efc2ce9b6cb83c8 Mon Sep 17 00:00:00 2001 From: Christoph Kutzinski Date: Sun, 16 Aug 2015 13:05:03 +0200 Subject: [PATCH 009/262] 2.2.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b3a58e..e7d3ee50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +#2.2.2 (Aug 16th, 2015) +### Misc: +- require Jenkins 1.580+ + +###Bug fixes: +- 2.2.0 didn't make it to the update center + + #2.2.0 (May 12th, 2015) ###New Feature/Enhancement: - Ability to debug connection errors with (optional) enhanced console output ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/9)) From 7e4100ff7b3dd71ad7479ee185e8c0cd102a4060 Mon Sep 17 00:00:00 2001 From: Peter Mihaly Avramucz Date: Wed, 2 Mar 2016 12:18:26 +0100 Subject: [PATCH 010/262] Set read timeout in sendHTTPCall --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index fa62789e..325988c0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -967,6 +967,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui connection.setRequestMethod(requestType); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); connection.connect(); InputStream is; From 0a34884904ef901df6c0b5ccbfc18eb41ac9d737 Mon Sep 17 00:00:00 2001 From: Peter Mihaly Avramucz Date: Wed, 2 Mar 2016 12:29:43 +0100 Subject: [PATCH 011/262] Fixed whitespace --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 325988c0..d664c912 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -967,7 +967,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui connection.setRequestMethod(requestType); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); + connection.setReadTimeout(5000); connection.connect(); InputStream is; From ef8aa49e53fc2d48b626dc9b43560309b78a6712 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 18 Oct 2017 13:06:20 +0200 Subject: [PATCH 012/262] Enables Jenkins 2.x Pipelines Enables Jenkins 2.x Pipelines via 'triggerRemoteJob' build step (see Pipeline Syntax Generator). Bug fixes: - Issues solved with identifying correct build (queueId,...). - Issues solved with triggering non-parameterized jobs. --- README.md | 22 +- README_JobConfiguration.md | 19 + README_PipelineConfiguration.md | 188 ++ README_SystemConfiguration.md | 8 + pom.xml | 79 +- screenshots/1-system-settings.png | Bin 83454 -> 54438 bytes screenshots/2-build-configuration-1.png | Bin 13908 -> 7056 bytes screenshots/3-build-configuration-2.png | Bin 71209 -> 60583 bytes screenshots/3-build-configuration-2b.png | Bin 0 -> 48862 bytes screenshots/pipelineSyntaxGenerator.png | Bin 0 -> 62840 bytes screenshots/pipelineSyntaxGenerator2.png | Bin 0 -> 54500 bytes .../ParameterizedRemoteTrigger/Auth.java | 185 +- .../BuildContext.java | 92 + .../ConnectionResponse.java | 54 + .../JenkinsCrumb.java | 29 + .../RemoteBuildConfiguration.java | 1820 ++++++++++------- .../RemoteJenkinsServer.java | 130 +- .../SearchPattern.java | 42 - .../auth2/Auth2.java | 85 + .../auth2/CredentialsAuth.java | 164 ++ .../auth2/NoneAuth.java | 52 + .../auth2/NullAuth.java | 52 + .../auth2/TokenAuth.java | 80 + .../CredentialsNotFoundException.java | 22 + .../exceptions/ForbiddenException.java | 23 + .../exceptions/UnauthorizedException.java | 23 + .../pipeline/Handle.java | 415 ++++ .../pipeline/PrintStreamWrapper.java | 57 + .../pipeline/RemoteBuildPipelineStep.java | 270 +++ .../remoteJob/BuildData.java | 44 + .../BuildInfoExporterAction.java | 95 +- .../remoteJob/BuildStatus.java | 92 + .../remoteJob/QueueItem.java | 47 + .../remoteJob/QueueItemData.java | 105 + .../utils/Base64Utils.java | 62 + .../utils/FormValidationUtils.java | 117 ++ .../utils/TokenMacroUtils.java | 44 + .../Auth/config.jelly | 18 +- .../RemoteBuildConfiguration/config.jelly | 102 +- .../RemoteBuildConfiguration/global.jelly | 10 +- .../RemoteBuildConfiguration/help-auth2.html | 21 + .../RemoteBuildConfiguration/help-job.html | 10 +- .../help-remoteJenkinsUrl.html | 3 + .../RemoteJenkinsServer/config.jelly | 8 +- .../RemoteJenkinsServer/help-auth2.html | 18 + .../RemoteJenkinsServer/help.html | 3 + .../auth2/CredentialsAuth/config.jelly | 7 + .../auth2/TokenAuth/config.jelly | 11 + .../RemoteBuildPipelineStep/config.jelly | 53 + .../RemoteBuildPipelineStep/help-auth.html | 21 + .../help-blockBuildUntilComplete.html | 13 + .../help-enhancedLogging.html | 10 + .../RemoteBuildPipelineStep/help-job.html | 9 + .../help-parameters.html | 10 + .../help-pollInterval.html | 11 + .../help-preventRemoteBuildQueue.html | 10 + .../help-remoteJenkinsName.html | 9 + .../help-remoteJenkinsUrl.html | 6 + .../help-shouldNotFailBuild.html | 10 + .../RemoteBuildPipelineStep/help-token.html | 12 + .../RemoteBuildPipelineStep/help.html | 24 + .../RemoteBuildConfigurationTest.java | 342 +++- .../SearchPatternTest.java | 23 - .../pipeline/HandleTest.java | 32 + .../utils/FormValidationUtilsTest.java | 26 + 65 files changed, 4247 insertions(+), 1102 deletions(-) create mode 100644 README_JobConfiguration.md create mode 100644 README_PipelineConfiguration.md create mode 100644 README_SystemConfiguration.md create mode 100644 screenshots/3-build-configuration-2b.png create mode 100644 screenshots/pipelineSyntaxGenerator.png create mode 100644 screenshots/pipelineSyntaxGenerator2.png create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java delete mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPattern.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UnauthorizedException.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java rename src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/{ => remoteJob}/BuildInfoExporterAction.java (58%) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-remoteJenkinsUrl.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-auth.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-blockBuildUntilComplete.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-enhancedLogging.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-job.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-pollInterval.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-preventRemoteBuildQueue.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsName.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsUrl.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-shouldNotFailBuild.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-token.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help.html delete mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPatternTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtilsTest.java diff --git a/README.md b/README.md index 123aac61..0e1a1a0e 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,7 @@ This plugin also has support for build authorization tokens (as defined [here](h Please take a look at the [change log](CHANGELOG.md) for a complete list of features and what not. - -###Screenshots -System configuration option - -![System onfiguration option](screenshots/1-system-settings.png) - - -Job setup options - -![select from drop-down](screenshots/2-build-configuration-1.png) - -![Job setup options](screenshots/3-build-configuration-2.png) - - -####Current Limitations -1. ~~Does not play well with [Build Token Root Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Build+Token+Root+Plugin) URL formats.~~ (added with [this commit](https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/commit/f687dbe75d1c4f39f7e14b68220890384d7c5674) ) -2. ~~No username/password authentication, must use a 'build authorization token'.~~ (added with [this commit](https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/commit/a23ade0add621830e85eb228990a95658e239b80) ) -3. ~~Follows a "fire & forget" model when triggering the remote build, which means that we don't know the status of the remote build, only if the request was successful or not.~~ (added with [this commit](https://github.com/morficus/Parameterized-Remote-Trigger-Plugin/commit/d32c69d0033aefda382c55e9394ebab8d1da10ae) thanks to [@timbrown5](https://github.com/timbrown5)) +## Usage +1. [System configuration options](README_SystemConfiguration.md)
+2. [Job setup options](README_JobConfiguration.md)
+3. [Pipeline setup options](README_PipelineConfiguration.md) diff --git a/README_JobConfiguration.md b/README_JobConfiguration.md new file mode 100644 index 00000000..625abb21 --- /dev/null +++ b/README_JobConfiguration.md @@ -0,0 +1,19 @@ +# Job setup options + +Select `Build` > `Add build step` > `Trigger a remote parameterized job` + +![select from drop-down](screenshots/2-build-configuration-1.png) + +You can select a globally configured remote server and only specify a job name here. +The full URL is calculated based on the remote server, the authentication is taken from the global configuration. +However it is possible to override the Jenkins base URL (or set the full Job URL) and override credentials used for authentication. + +![Job setup options](screenshots/3-build-configuration-2.png) + +You can also specify the full job URL and use only the authentication from the global configuration or specify the authentication per job. + +![Job setup options](screenshots/3-build-configuration-2b.png) + + +# Support of Folders on Remote Jenkins +[See here for more information](README_PipelineConfiguration.md#user-content-folders) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md new file mode 100644 index 00000000..f7284d0f --- /dev/null +++ b/README_PipelineConfiguration.md @@ -0,0 +1,188 @@ +# Pipeline setup options + +- [Defaults](#user-content-defaults) +- [Remote Server Configuration](#user-content-server) +- [Authentication](#user-content-authentication) +- [The Handle Object](#user-content-handle) +- [Blocking vs. Non-Blocking](#user-content-blockingnonblocking) + - [Blocking usage (recommended)](#user-content-blocking) + - [Non-blocking usage](#user-content-nonblocking) +- [Support of Folders on Remote Jenkins](#user-content-folders) + +The `triggerRemoteJob` pipeline step triggers a job on a remote Jenkins. This command is also available in the Jenkins Pipeline Syntax Generator: + +You can select a globally configured remote server and only specify a job name here. +The full URL is calculated based on the remote server, the authentication is taken from the global configuration. +However it is possible to override the Jenkins base URL (or set the full Job URL) and override credentials used for authentication. + +![Pipeline Syntax Generator](screenshots/pipelineSyntaxGenerator.png) + +You can also specify the full job URL and use only the authentication from the global configuration or specify the authentication per job. + +![Pipeline Syntax Generator](screenshots/pipelineSyntaxGenerator2.png) + + +
+ +## Defaults +The simplest way to trigger a job is: +``` +def handle = triggerRemoteJob job: 'https://myjenkins:8080/job/JobWithoutParams' +echo 'Remote Status: ' + handle.getBuildStatus().toString() +``` + +If the job has parameters: +``` +def handle = triggerRemoteJob job: 'https://myjenkins:8080/job/JobWithParams', parameters: 'param1=abc\nparam2=xyz' +``` + +If authentication is required: +``` +def handle = triggerRemoteJob job: 'https://myjenkins:8080/job/JobWithoutParams', auth: TokenAuth(apiToken: '', userName: '') +``` + + +The pipeline will wait/block until the remote build finished. + + +
+ +## Remote Server Configuration + +:information_source: You can configure jobs/pipelines also without any global configuration. + +The remote Jenkins server containing the target job(s) can be configured in different ways. +- **Jenkins System Configuration**
+ Remote servers can be configured in the [Jenkins System Configuration](README_SystemConfiguration.md) and referenced in Pipelines by their name. The server configuration can also include authentication settings.
+ `triggerRemoteJob remoteJenkinsName: 'remoteJenkins' ...` +- **Override Server URL**
+ On Pipeline level the URL can be set/overridden with parameter `remoteJenkinsUrl`.
+ `triggerRemoteJob remoteJenkinsUrl: 'https://myjenkins:8080' ...`
+ If combined with `remoteJenkinsName` only the URL of the globally configured server will be overridden, the other settings like authentication will be used from the global configuration.
+ `triggerRemoteJob remoteJenkinsName: 'remoteJenkins', remoteJenkinsUrl: 'https://myjenkins:8080' ...`
+- **Full Job URL**
+ It is also possible to configure the full job URL instead of the job name only and the remote Jenkins server root URL.
+ `triggerRemoteJob job: 'https://myjenkins:8080/job/MyJob' ...`
+ +:information_source: If the remote Jenkins uses folders please [read this](#user-content-folders). + +
+ +## Authentication +Authentication can be configured globally in the system configuration or set/overridden for each pipeline via the `auth` parameter. + +The following authentication types are available: +- **Token Authentication** The specified user id and Jenkins API token is used.
+ ```auth: TokenAuth(apiToken: '', userName: '')``` +- **Credentials Authentication** The specified Jenkins Credentials are used. This can be either user/password or user/API Token.
+ ```auth: CredentialsAuth(credentials: '')``` +- **No Authentication** No Authorization header will be sent, independent of the global 'remote host' settings.
+ ```auth: NoneAuth()``` + +**Note:** *Jenkins API Tokens* are recommended since, if stolen, they allow access only to a specific Jenkins +while user and password typically provide access to many systems. + + + +
+ +## The Handle Object +The `Handle` object provides the following methods: + +- `String getJobName()` returns the remote job name +- `URL getBuildUrl()` returns the remote build URL including the build number +- `int getBuildNumber()` returns the remote build number +- `BuildStatus getBuildStatus()` returns the current remote build status +- `BuildStatus getBuildStatusBlocking()` waits for completion and returns the build result +- `boolean isFinished()` true if the remote build finished +- `boolean isQueued()` true if the job is queued but not yet running +- `String toString()` +- `Object readJsonFileFromBuildArchive(String filename)`
+ This is a convenience method to download and parse the specified JSON file (filename or relative path) from the build archive. + This mechanism might be used by remote builds to provide return parameters. + +``` +def handle = triggerRemoteJob blockBuildUntilComplete: true, ... +def results = handle.readJsonFileFromBuildArchive('build-results.json') +echo results.urlToTestResults //just an example +``` + +The `BuildStatus` enum provides the following types and methods: + +- Custom statuses: `UNKNOWN`, `NOT_STARTED`, `QUEUED`, `RUNNING`, if the remote job did not finish yet. +- Jenkins Result statuses: `ABORTED`, `FAILURE`, `NOT_BUILT`, `SUCCESS`, `UNSTABLE`, if the remote job finished the status reflects the Jenkins build `Result`. +- `boolean isJenkinsResult()`, true if the `BuildStatus` reflects a Jenkins `Result`. +- `Result getJenkinsResult()`, the Jenkins `Result` if the status reflects one, null otherwise. +- `String toString()` + + +
+ +## Blocking vs. Non-Blocking +The `triggerRemoteJob` command always returns a [`Handle`](#user-content-the-handle-object) object. This object can be used to track the status of the remote build (instead of using the environment variables like in the Job case). + +There are two ways to use the command, in a blocking way (it will wait/block until the remote job finished) and in a non-blocking way (the handle is returned immediately and the remote status can be checked asynchronously). + +
+ +### Blocking usage (recommended) +The recommended way to trigger jobs is in a blocking way. Set `blockBuildUntilComplete: true` to let the plugin wait +until the remote build finished: +``` +def handle = triggerRemoteJob( + remoteJenkinsName: 'remoteJenkins', + job: 'TheJob', + parameters: 'a=b', + blockBuildUntilComplete: true, + ...) +echo 'Remote Status: ' + handle.getBuildStatus().toString() +``` + +
+ +### Non-blocking usage +It is also possible to use it in a non-blocking way. Set `blockBuildUntilComplete: false` and the plugin directly +returns the `handle` for further tracking the status: +``` +def handle = triggerRemoteJob( + remoteJenkinsName: 'remoteJenkins', + job: 'TheJob', + parameters: 'a=b', + blockBuildUntilComplete: false, + ...) +while( !handle.isFinished() ) { + echo 'Current Status: ' + handle.getBuildStatus().toString(); + sleep 5 +} +echo handle.getBuildStatus().toString(); +``` + +Even with `blockBuildUntilComplete: false` it is possible to wait synchronously until the remote job finished: +``` +def handle = triggerRemoteJob blockBuildUntilComplete: false, ... +def status = handle.getBuildStatusBlocking() +``` + +:warning: Currently the plugin cannot log to the pipeline log directly if used in non-blocking mode. As workaround you can use `handle.lastLog()` after each command to get the log entries. + + +
+ +# Support of Folders on Remote Jenkins + +The Parameterized Remote Trigger plugin also supports the use of folders on the remote Jenkins server, for example if it uses the [`CloudBees Folders Plugin`](https://wiki.jenkins.io/display/JENKINS/CloudBees+Folders+Plugin) or the [`GitHub Branch Source Plugin`](https://plugins.jenkins.io/github-branch-source) (formerly [`GitHub Organization Folder Plugin`](https://wiki.jenkins.io/display/JENKINS/GitHub+Organization+Folder+Plugin)) + +Remote URLs with folders look like this +``` +https://server:8080/job/Folder1/job/Folder2/job/TheJob +``` + +Without folders it would only be `https://server:8080/job/TheJob` + +To be able to trigger such jobs you have to either +1. Specify the full Job URL as `Remote Job Name or URL` +2. Specify the job fullname as `Remote Job Name or URL` + a globally configured [`Remote Host`](#user-content-server).
+ The jobs fullname in the example above would be 'Folder1/Folder2/TheJob'. + + +






























diff --git a/README_SystemConfiguration.md b/README_SystemConfiguration.md new file mode 100644 index 00000000..3791f3dc --- /dev/null +++ b/README_SystemConfiguration.md @@ -0,0 +1,8 @@ +# System configuration options + +The Parameterized Remote Trigger plugin can used without any global Jenkins system configuration. + +However it might be useful to configure one or more remote server & credentials globally. +From a job/pipeline this server can be referenced together with the job name, without the need to specify the full URL and authentication credentials. Still it is possible to specify or override each part on job/pipeline level. + +![System onfiguration option](screenshots/1-system-settings.png) diff --git a/pom.xml b/pom.xml index edcbbe43..57f9b874 100644 --- a/pom.xml +++ b/pom.xml @@ -3,16 +3,23 @@ org.jenkins-ci.plugins plugin - 1.580 + 2.2 + + 1.577 + 7 + 2.1 + false + + Parameterized-Remote-Trigger - 2.2.3-SNAPSHOT + 2.3.0-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. http://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Remote+Trigger+Plugin - + MIT license @@ -27,12 +34,16 @@ - - - - org.jenkins-ci.tools - maven-hpi-plugin - 1.95 + + + + org.jenkins-ci.tools + maven-hpi-plugin + 1.95 + + + 2.3.0-SNAPSHOT + maven-javadoc-plugin @@ -40,9 +51,9 @@ false - - - + + + scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git @@ -74,7 +85,49 @@ org.jenkins-ci.plugins token-macro - 1.9 + 2.0 + + + org.jenkins-ci.plugins.workflow + workflow-job + 2.7 + true + + + org.jenkins-ci.plugins.workflow + workflow-cps + 2.2 + true + + + org.jenkins-ci.plugins.workflow + workflow-step-api + 2.9 + true + + + org.jenkins-ci.plugins + structs + 1.2 + true + + + org.mockito + mockito-core + 1.10.19 + test + + + hamcrest-core + org.hamcrest + + + + + org.mortbay.jetty + jetty-util + 6.1.26 + test diff --git a/screenshots/1-system-settings.png b/screenshots/1-system-settings.png index 015e2b885717bd0fa16db9806b05653d7ec45173..b03fbe1ceea14f9a7e6573b4f320b45855b604c9 100644 GIT binary patch literal 54438 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 z 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

%RhXu;?30_XR=1`Xqqf0W5Ml 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 uae`pV89x2?Ph literal 83454 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%0DuEq6#(+oI%*8ChNa&#cO0r#Hs3mvJiyDxs!e@OE z0RPVs-}gf8+t|GVWv)_l`b?RIcI^8`gc;)`+Fm9h9Hb)X$wi-jHZ7VO+Bd~xMi>Li z&}NgNxt5^~q>$oZA~u=nM+R{2dp${h<6?H$yhWpsJUgu6X~LJ;@c z6V1i6$bgvr1Pagi4<{%Wg21SesJEpF+&7{BGI>!0?%(pK6Syh9i_7Vl8Id?K9gl&6 z{c9p9FMkMuBLM^{8fe0H!Zw;p)RBG8wG;af<>(;QWl4EjdbJpL<8*R5iJ%|^os;}V zXjmbx*OgipZw0!`o(Iss@a1j=%|mPcU&4b0-ukuDES=!*yzM$@{qp|(M)ohGzhtKH z1Bmc2Nk{)_64dHBXk&-tlkLPh!(UP~4Pq5$SY)E0qn-b#>pf`cGQ0OTQ(Mgcm`Ao5 zsAZ-IwERC#2lV|E4YnG79OMs^#J8^gWkOGV`zy8+84#O#i~{HUj|8BYl7M=t4o^Mf_g|lC zdBBe+&0gt!VF@P8;8n}!q_H_5-jX@kZ?q44tP;dmCd{EmW`S9zr5aLPZ#3+l`RzHl zJZR&g36~BeY|vb*@&@Wh%g>O^!ZcGpE(1Lvy;>IX4T_{IQOzE)USqeE?M> z{QM0Hl#JAc8dBT#8K3*?!>)5b$v8MJJihw#W;0t6V>H)D8!P#?Z1Rf_-T9m!J1Flz z#t(W96HK!ncnI8Ru#?FPwSk?lLbAN`r}HZ;FHuWKj0~CH9V6zBxgZw4<6XgO$jN@$wVB)89IKJ^O#A$UW_CkAa5Fq6 zYTn%9Nc?X3;W4N_)tlLBO(NzPxU!=pVRNL;X|in~gK=~?^ip^IYY_+&G)3jvAnPd$ z2^!TGsPh@6f5h`$TB1`QaEIP9AceqP5)QTTWU+Qhi3^uTJfPME!21qTxMtU^i z)XMUe5CJ4*{5NCq^(O@hZjMB^YIIg}FgRe>wj`YA)l;pn{Z;=l`XFt$-G@@~!^^&E z{f*@FRa_9GF$)$+{twhI4>&kiMS)g9^Wt;R#9 zIT0?+u}U)_P1IC?subBv#gc}Cgm@9kjtOc#Dr2qc5_|3v5aRfX_#9MHYD=#aNzbHZay3S1Fu*^8l+@7?WWwwZaKCs^8Ux`~ zM?~4mH#US!2`0Y0C6`dT!N_>C9ZUq1<)I7Wwe|&e~Ii9Osqof|0=z8{?+2e zX}^8MZ3>B9LhpF-1-{cn)sNbo5@bZ6ad5eOD zMB=~4`XA6s%r3PBMe!fbg7Xmj3iczzBGH0`{Rc#uBEYmiFMyyI^@C}JOV;`UfLHQLE zyFN-Z<-h)mifiFOjnVj3IZyy>!~tAvEfPt+9ui!JbXIZ^tGBZxcF!tj!YdcvRgHZ| zc6+(urhj~(x*sSgPRxeG#e*8#Gq%#DW;3ej7mT3x%qdmrR03}L zP;$boTA9&s5Esjqtyo$4Hoih}p!^sip}!h(3_zhw>kJ97~eggFP`0Qhf z7gY@$OHu2Zaoa(h?e?Mjz&dmN<3DQKsRO4g+^-{C0tqy$S5MuGsBQ66qoJh}dM`VW z(i?8r;>J4(+JF9*qNWGgBo@&RXXH2DRlYeH`JaOClY?x-F+S#;h-YNicx?Y(E*xYo zZ*@Qp1cW9_pZi%Nx(vI*rPCmS=>HMYcjLE7AWO%LhP*|?!`5F!_J3s8LFijl{$$?B z9$53nyT18}lrKSZgufY`uMVin)_gbY-rqSP&kvnxFaPew h9{US6_k`!K-~{P~vXFS0VPK$-n2@w!Ilr#o{{e>mL|6a- diff --git a/screenshots/2-build-configuration-1.png b/screenshots/2-build-configuration-1.png index 1b0484f629e8ba555a934a91792ff15d129993a1..072dc54dab1ec7a9acd1fc22344540519a3689b2 100644 GIT binary patch literal 7056 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¥z8k1v1uGcYhn!auQ( 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 MK|{X$wOR0g09ifH$^ZZW literal 13908 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�oHnfON4cfZcCsmDNkdZbc4Syd;b*lrZR3S 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

TN+fQ5BIZv=jT|z3- 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�~W$^ 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

    * If you don't want fields to be persisted, use transient. -v */ + */ private CopyOnWriteList remoteSites = new CopyOnWriteList(); /** * In order to load the persisted global configuration, you have to call load() in the constructor. */ public DescriptorImpl() { - load(); + this(true); + } + + private DescriptorImpl(boolean load) { + if(load) load(); + } + + public static DescriptorImpl newInstanceForTests() + { + return new DescriptorImpl(false); } + /* /** * 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. @@ -1270,9 +1534,37 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc return super.configure(req, formData); } + public FormValidation doCheckJob( + @QueryParameter("job") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); + if(result.isAffected(AffectedField.jobNameOrUrl)) return result.formValidation; + return FormValidation.ok(); + } + + public FormValidation doCheckRemoteJenkinsUrl( + @QueryParameter("remoteJenkinsUrl") final String value, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); + if(result.isAffected(AffectedField.remoteJenkinsUrl)) return result.formValidation; + return FormValidation.ok(); + } + + public FormValidation doCheckRemoteJenkinsName( + @QueryParameter("remoteJenkinsName") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); + if(result.isAffected(AffectedField.remoteJenkinsName)) return result.formValidation; + return FormValidation.ok(); + } + public ListBoxModel doFillRemoteJenkinsNameItems() { ListBoxModel model = new ListBoxModel(); + model.add(""); for (RemoteJenkinsServer site : getRemoteSites()) { model.add(site.getDisplayName()); } @@ -1288,5 +1580,15 @@ public RemoteJenkinsServer[] getRemoteSites() { public void setRemoteSites(RemoteJenkinsServer... remoteSites) { this.remoteSites.replaceBy(remoteSites); } + + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NullAuth.DESCRIPTOR; + } + } + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 22475360..ba9a36e7 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -1,23 +1,23 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import static org.apache.commons.lang.StringUtils.trimToEmpty; + import java.net.HttpURLConnection; import java.net.MalformedURLException; -import java.net.URI; import java.net.URL; -import java.util.ArrayList; import java.util.List; -import net.sf.json.JSONObject; - +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; -import hudson.util.CopyOnWriteList; import hudson.util.FormValidation; -import hudson.util.ListBoxModel; /** * Holds everything regarding the remote server we wish to connect to, including validations and what not. @@ -27,37 +27,47 @@ */ public class RemoteJenkinsServer extends AbstractDescribableImpl { - private final URL address; - private final String displayName; - private final boolean hasBuildTokenRootSupport; - private final String username; - private final String apiToken; + /** + * We need to keep this for compatibility - old config deserialization! + * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. + */ + private List auth; - private CopyOnWriteList auth = new CopyOnWriteList(); + private String displayName; + private boolean hasBuildTokenRootSupport; + private Auth2 auth2; + private URL address; @DataBoundConstructor - public RemoteJenkinsServer(String address, String displayName, boolean hasBuildTokenRootSupport, JSONObject auth) - throws MalformedURLException { - - this.address = new URL(address); - this.displayName = displayName.trim(); - this.hasBuildTokenRootSupport = hasBuildTokenRootSupport; - - // Holding on to both of these variables for legacy purposes. The seemingly 'dirty' getters for these properties - // are for the same reason. - this.username = ""; - this.apiToken = ""; + public RemoteJenkinsServer() { + this.auth2 = new NoneAuth(); + } - // this.auth = new Auth(auth); - this.auth.replaceBy(new Auth(auth)); + @DataBoundSetter + public void setDisplayName(String displayName) + { + this.displayName = trimToEmpty(displayName); + } + @DataBoundSetter + public void setHasBuildTokenRootSupport(boolean hasBuildTokenRootSupport) + { + this.hasBuildTokenRootSupport = hasBuildTokenRootSupport; } - // Getters + @DataBoundSetter + public void setAuth2(Auth2 auth2) + { + this.auth2 = (auth2 != null) ? auth2 : new NoneAuth(); + } - public Auth[] getAuth() { - return auth.toArray(new Auth[this.auth.size()]); + @DataBoundSetter + public void setAddress(String address) throws MalformedURLException + { + this.address = new URL(address); } + + // Getters public String getDisplayName() { String displayName = null; @@ -70,12 +80,32 @@ public String getDisplayName() { return displayName; } - public URL getAddress() { - return address; + public boolean getHasBuildTokenRootSupport() { + return hasBuildTokenRootSupport; + } + + public Auth2 getAuth2() { + migrateAuthToAuth2(); + return auth2; } - public boolean getHasBuildTokenRootSupport() { - return this.hasBuildTokenRootSupport; + /** + * Migrates old Auth to Auth2 if necessary. + * @deprecated since 2.3.0-SNAPSHOT - get rid once all users migrated + */ + private void migrateAuthToAuth2() { + if(auth2 == null) { + if(auth == null || auth.size() <= 0) { + auth2 = new NoneAuth(); + } else { + auth2 = Auth.authToAuth2(auth); + } + } + auth = null; + } + + public URL getAddress() { + return address; } @Override @@ -86,35 +116,10 @@ public DescriptorImpl getDescriptor() { @Extension public static class DescriptorImpl extends Descriptor { - private JSONObject authenticationMode; - - /** - * In order to load the persisted global configuration, you have to call load() in the constructor. - */ - /* - * public DescriptorImpl() { load(); } - */ - public String getDisplayName() { return ""; } - public ListBoxModel doFillCredsItems() { - // StandardUsernameListBoxModel model = new StandardUsernameListBoxModel(); - - // Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); - // model.withAll(CredentialsProvider.lookupCredentials(StandardUsernameCredentials.class, item, ACL.SYSTEM, - // Collections.emptyList())); - - return Auth.DescriptorImpl.doFillCredsItems(); - - // return model; - } - - public JSONObject doFillAuthenticationMode() { - return this.authenticationMode.getJSONObject("authenticationType"); - } - /** * Validates the given address to see that it's well-formed, and is reachable. * @@ -134,7 +139,7 @@ public FormValidation doValidateAddress(@QueryParameter String address) { // check if we have a valid, well-formed URL try { host = new URL(address); - URI uri = host.toURI(); + host.toURI(); } catch (Exception e) { return FormValidation.error("Malformed address (" + address + "), please double-check it."); } @@ -150,6 +155,13 @@ public FormValidation doValidateAddress(@QueryParameter String address) { return FormValidation.okWithMarkup("Address looks good"); } - } + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NoneAuth.DESCRIPTOR; + } + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPattern.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPattern.java deleted file mode 100644 index 5d29acd1..00000000 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPattern.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Search around a specified {@link startingValue} by a magnitude of {@link maxDrift}. - */ -public class SearchPattern implements Iterable { - private final int startingValue; - private final int maxDrift; - - public SearchPattern(int startingValue, int maxDrift) { - this.startingValue = startingValue; - this.maxDrift = maxDrift; - } - - public Iterator iterator() { - return new Iterator() { - private int drift = 0; - - public boolean hasNext() { - return (drift != -maxDrift - 1); - } - - public Integer next() { - if (! hasNext()) throw new NoSuchElementException(); - int ret = startingValue + drift; - if (drift < 0) { - drift = -drift; - } else { - drift = -drift - 1; - } - return ret; - } - - public void remove() { - next(); - } - }; - } -} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java new file mode 100644 index 00000000..d28c97a5 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -0,0 +1,85 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URLConnection; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; + +import hudson.DescriptorExtensionList; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import hudson.model.Item; +import jenkins.model.Jenkins; + +public abstract class Auth2 extends AbstractDescribableImpl implements Serializable { + + private static final long serialVersionUID = -3217381962636283564L; + + private static final DescriptorExtensionList ALL = DescriptorExtensionList + .createDescriptorList(Jenkins.getInstance(), Auth2.class); + + public static DescriptorExtensionList all() + { + return ALL; + } + + public static abstract class Auth2Descriptor extends Descriptor + { + } + + /** + * Tries to identify a user. Depending on the Auth2 type it might be null. + * + * @param auth + * authorization to trigger jobs in the remote server + * @return the user name + * @throws CredentialsNotFoundException + * if the credentials are not found + */ + public static String identifyUser(Auth2 auth) throws CredentialsNotFoundException + { + if (auth == null) { + return null; + } + else if (auth instanceof TokenAuth) { + return ((TokenAuth) auth).getUserName(); + } + else if (auth instanceof CredentialsAuth) { + return ((CredentialsAuth) auth).getUserName(null); + } + else { + return null; + } + } + + /** + * Depending on the purpose the Auth2 implementation has to override the + * Authorization header of the connection appropriately. It might also ignore this + * step or remove an existing Authorization header. + * + * @param connection + * connection between the application and the remote server. + * @param context + * the context of this Builder/BuildStep. + * @throws IOException + * if there is an error generating the authorization header. + */ + public abstract void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException; + + public abstract String toString(); + + /** + * Returns a string representing the authorization. + * + * @param item + * the Item (Job, Pipeline,...) we are currently running in. + * The item is required to also get Credentials which are defined in the items scope and not Jenkins globally. + * Value can be null, but Credentials e.g. configured on a Folder will not be found in this case, + * only globally configured Credentials. + * @return a string representing the authorization. + */ + public abstract String toString(Item item); + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java new file mode 100644 index 00000000..4277d7d8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -0,0 +1,164 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils.AUTHTYPE_BASIC; + +import java.io.IOException; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.Stapler; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernameListBoxModel; +import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; +import com.cloudbees.plugins.credentials.domains.DomainRequirement; + +import hudson.Extension; +import hudson.model.Item; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; + +public class CredentialsAuth extends Auth2 { + + private static final long serialVersionUID = -2650007108928532552L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new CredentialsAuthDescriptor(); + + private String credentials; + + @DataBoundConstructor + public CredentialsAuth() { + this.credentials = null; + } + + @DataBoundSetter + public void setCredentials(String credentials) { + this.credentials = credentials; + } + + public String getCredentials() { + return credentials; + } + + /** + * Tries to find the Jenkins Credential and returns the user name. + * @param item the Item (Job, Pipeline,...) we are currently running in. + * The item is required to also get Credentials which are defined in the items scope and not Jenkins globally. + * Value can be null, but Credentials e.g. configured on a Folder will not be found in this case, only globally configured Credentials. + * @return The user name configured in this Credential + * @throws CredentialsNotFoundException if credential could not be found. + */ + public String getUserName(Item item) throws CredentialsNotFoundException { + UsernamePasswordCredentials creds = _getCredentials(item); + return creds.getUsername(); + } + + /** + * Tries to find the Jenkins Credential and returns the password. + * @param item the Item (Job, Pipeline,...) we are currently running in. + * The item is required to also get Credentials which are defined in the items scope and not Jenkins globally. + * Value can be null, but Credentials e.g. configured on a Folder will not be found in this case, only globally configured Credentials. + * @return The password configured in this Credential + * @throws CredentialsNotFoundException if credential could not be found. + */ + public String getPassword(Item item) throws CredentialsNotFoundException { + UsernamePasswordCredentials creds = _getCredentials(item); + return creds.getPassword().getPlainText(); + } + + @Override + public String toString() { + return toString(null); + } + + @Override + public String toString(Item item) { + try { + String userName = getUserName(item); + return String.format("'%s' as user '%s' (Credentials ID '%s')", getDescriptor().getDisplayName(), userName, credentials); + } + catch (CredentialsNotFoundException e) { + return String.format("'%s'. WARNING! No credentials found with ID '%s'!", getDescriptor().getDisplayName(), credentials); + } + } + + /** + * Looks up the credentialsID attached to this object in the Global Credentials plugin datastore + * @param item the Item (Job, Pipeline,...) we are currently running in. + * The item is required to also get Credentials which are defined in the items scope and not Jenkins globally. + * Value can be null, but Credentials e.g. configured on a Folder will not be found in this case, only globally configured Credentials. + * @return the matched credentials + * @throws CredentialsNotFoundException if not found + */ + private UsernamePasswordCredentials _getCredentials(Item item) throws CredentialsNotFoundException { + List listOfCredentials = CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, item, ACL.SYSTEM, Collections. emptyList()); + + return (UsernamePasswordCredentials) _findCredential(credentials, listOfCredentials); + } + + private StandardUsernameCredentials _findCredential(String credentialId, List listOfCredentials) throws CredentialsNotFoundException{ + for (StandardUsernameCredentials cred : listOfCredentials) { + if (credentialId.equals(cred.getId())) { + return cred; + } + } + throw new CredentialsNotFoundException(credentialId); + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins != null) { + Item item = jenkins.getItem(context.currentItem, jenkins.getItem("/")); + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(item), getPassword(item), context); + connection.setRequestProperty("Authorization", authHeaderValue); + } + } + + @Override + public Auth2Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("CredentialsAuth") + public static class CredentialsAuthDescriptor extends Auth2Descriptor { + @Override + public String getDisplayName() { + return "Credentials Authentication"; + } + + public static ListBoxModel doFillCredentialsItems() { + StandardUsernameListBoxModel model = new StandardUsernameListBoxModel(); + + Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + + List listOfAllCredentails = CredentialsProvider.lookupCredentials( + StandardUsernameCredentials.class, item, ACL.SYSTEM, Collections. emptyList()); + + List listOfSandardUsernameCredentials = new ArrayList(); + + // since we only care about 'UsernamePasswordCredentials' objects, lets seek those out and ignore the rest. + for (StandardUsernameCredentials c : listOfAllCredentails) { + if (c instanceof UsernamePasswordCredentials) { + listOfSandardUsernameCredentials.add(c); + } + } + model.withAll(listOfSandardUsernameCredentials); + + return model; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java new file mode 100644 index 00000000..546b7baa --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -0,0 +1,52 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import java.io.IOException; +import java.net.URLConnection; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.kohsuke.stapler.DataBoundConstructor; + +import hudson.Extension; +import hudson.model.Item; + +public class NoneAuth extends Auth2 { + + private static final long serialVersionUID = -3128995428538415113L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new NoneAuthDescriptor(); + + @DataBoundConstructor + public NoneAuth() { + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + connection.setRequestProperty("Authorization", null); + } + + @Override + public String toString() { + return "'" + getDescriptor().getDisplayName() + "'"; + } + + @Override + public String toString(Item item) { + return toString(); + } + + @Override + public Auth2Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("NoneAuth") + public static class NoneAuthDescriptor extends Auth2Descriptor { + @Override + public String getDisplayName() { + return "No Authentication"; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java new file mode 100644 index 00000000..2573053d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -0,0 +1,52 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import java.io.IOException; +import java.net.URLConnection; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.kohsuke.stapler.DataBoundConstructor; + +import hudson.Extension; +import hudson.model.Item; + +public class NullAuth extends Auth2 { + + private static final long serialVersionUID = -1209658644855942360L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new NullAuthDescriptor(); + + @DataBoundConstructor + public NullAuth() { + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + //Ignore + } + + @Override + public String toString() { + return "'" + getDescriptor().getDisplayName() + "'"; + } + + @Override + public String toString(Item item) { + return toString(); + } + + @Override + public Auth2Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("NullAuth") + public static class NullAuthDescriptor extends Auth2Descriptor { + @Override + public String getDisplayName() { + return "Don't Set/Override"; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java new file mode 100644 index 00000000..ac346e5f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -0,0 +1,80 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils.AUTHTYPE_BASIC; + +import java.io.IOException; +import java.net.URLConnection; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; +import hudson.model.Item; + +public class TokenAuth extends Auth2 { + + private static final long serialVersionUID = 7912089565969112023L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new TokenAuthDescriptor(); + + private String userName; + private String apiToken; + + @DataBoundConstructor + public TokenAuth() { + this.userName = null; + this.apiToken = null; + } + + @DataBoundSetter + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUserName() { + return this.userName; + } + + @DataBoundSetter + public void setApiToken(String apiToken) { + this.apiToken = apiToken; + } + + public String getApiToken() { + return this.apiToken; + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken(), context); + connection.setRequestProperty("Authorization", authHeaderValue); + } + + @Override + public String toString() { + return "'" + getDescriptor().getDisplayName() + "' as user '" + getUserName() + "'"; + } + + @Override + public String toString(Item item) { + return toString(); + } + + @Override + public Auth2Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("TokenAuth") + public static class TokenAuthDescriptor extends Auth2Descriptor { + @Override + public String getDisplayName() { + return "Token Authentication"; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java new file mode 100644 index 00000000..bb2ceba5 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java @@ -0,0 +1,22 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions; + +import java.io.IOException; + +public class CredentialsNotFoundException extends IOException +{ + + private static final long serialVersionUID = -2489306184948013529L; + private String credentialsId; + + public CredentialsNotFoundException(String credentialsId) + { + this.credentialsId = credentialsId; + } + + @Override + public String getMessage() + { + return "No Jenkins Credentials found with ID '" + credentialsId + "'"; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java new file mode 100644 index 00000000..19b0a382 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java @@ -0,0 +1,23 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions; + +import java.io.IOException; +import java.net.URL; + +public class ForbiddenException extends IOException +{ + + private static final long serialVersionUID = -4049611671761455585L; + private URL url; + + public ForbiddenException(URL url) + { + this.url = url; + } + + @Override + public String getMessage() + { + return "Server returned 403 - Forbidden. User does not have enough permissions for this request: " + url; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UnauthorizedException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UnauthorizedException.java new file mode 100644 index 00000000..9157990c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UnauthorizedException.java @@ -0,0 +1,23 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions; + +import java.io.IOException; +import java.net.URL; + +public class UnauthorizedException extends IOException +{ + private static final long serialVersionUID = -7505703592596401545L; + + private URL url; + + public UnauthorizedException(URL url) + { + this.url = url; + } + + @Override + public String getMessage() + { + return "Server returned 401 - Unauthorized. Most likely there is something wrong with the provided credentials for this request: " + url; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java new file mode 100644 index 00000000..b888b2c3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -0,0 +1,415 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; + +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNullableByDefault; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; + +/** + * A handle to the triggered remote build. This handle is used in Pipelines + * to have direct access to the (correct) remote build instead of relying on + * environment variables (like in a Job). This prevents issues e.g. when triggering + * remote jobs in a parallel pipeline step. + */ +@ParametersAreNullableByDefault +public class Handle implements Serializable { + + private static final long serialVersionUID = 4418782245518194292L; + + private final RemoteBuildConfiguration remoteBuildConfiguration; + private final String queueId; + + //Available once moved from queue to an executor + private BuildData buildData; + private BuildStatus buildStatus; + + private String jobName; + private String jobFullName; + private String jobDisplayName; + private String jobFullDisplayName; + private String jobUrl; + + /** + * The current local Item (Job, Pipeline,...) where this plugin is currently used. + */ + private final String currentItem; + + /* + * The latest log entries from the last called method. + * Unfortunately the TaskListener.getLogger() from the StepContext does + * not write to the pipeline log anymore since the RemoteBuildPipelineStep + * already finished. + * TODO: Once we found a way to log to the pipeline log directly we can switch + */ + private String lastLog; + + public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, @Nonnull String currentItem) + { + this.remoteBuildConfiguration = remoteBuildConfiguration; + this.queueId = queueId; + this.buildData = null; + this.buildStatus = null; + this.lastLog = ""; + this.currentItem = currentItem; + if(trimToNull(currentItem) == null) throw new IllegalArgumentException("currentItem null"); + } + + /** + * Check if the remote build is still queued (not building yet). + * + * @return true if still queued, false if already running. + * @throws IOException + * if there is an error retrieving the remote build number. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @Whitelisted + public boolean isQueued() throws IOException, InterruptedException { + //Return if we already have the buildData + if(buildData != null) return false; + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + //TODO: This currently blocks + getBuildData(queueId, log.getPrintStream()); + return false; + } finally { + lastLog = log.getContent(); + } + } + + /** + * Check if the remote job build is finished. + * + * @return true if the remote job build ran and finished successfully, otherwise false. + * @throws IOException + * if there is an error retrieving the remote build number, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @Whitelisted + public boolean isFinished() throws IOException, InterruptedException { + BuildStatus buildStatus = getBuildStatus(); + return isFinishedBuildStatus(buildStatus); + } + + /** + * @return the name or URL of the remote job as configured in the job/pipeline. + */ + public String getConfiguredJobNameOrUrl() { + return remoteBuildConfiguration.getJob(); + } + + public String getJobName() + { + return jobName; + } + + public String getJobFullName() + { + return jobFullName; + } + + public String getJobDisplayName() + { + return jobDisplayName; + } + + public String getJobFullDisplayName() + { + return jobFullDisplayName; + } + + public String getJobUrl() + { + return jobUrl; + } + + /** + * @return the name of the remote job. + */ + public String getQueueId() { + return queueId; + } + +// /** +// * @return The full name of the item (Job, Pipeline,...) where we are currently running in locally. +// */ +// public String getCurrentItem() { +// return currentItem; +// } + + /** + * Get the build URL of the remote build. + * + * @return the URL, or null if it could not be identified (yet). + * @throws IOException + * if there is an error retrieving the remote build number, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @CheckForNull + @Whitelisted + public URL getBuildUrl() throws IOException, InterruptedException { + //Return if we already have the buildData + if(buildData != null) return buildData.getURL(); + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + //TODO: This currently blocks + BuildData buildData = getBuildData(queueId, log.getPrintStream()); + return buildData.getURL(); + } finally { + lastLog = log.getContent(); + } + } + + /** + * Get the build number of the remote build. + * + * @return the number, or -1 if it could not be identified (yet). + * @throws IOException + * if there is an error retrieving the remote build number, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @Whitelisted + public int getBuildNumber() throws IOException, InterruptedException { + //Return if we already have the buildData + if(buildData != null) return buildData.getBuildNumber(); + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + //TODO: This currently blocks + BuildData buildData = getBuildData(queueId, log.getPrintStream()); + return buildData.getBuildNumber(); + } finally { + lastLog = log.getContent(); + } + } + + /** + * Gets the current build status of the remote job. + * + * @return the {@link BuildStatus} - either reflecting a {@link hudson.model.Result} if finished, + * or if not finished yet a custom status like QUEUED, RUNNING,... + * @throws IOException + * if there is an error retrieving the remote build number, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @CheckForNull + @Whitelisted + public BuildStatus getBuildStatus() throws IOException, InterruptedException { + return getBuildStatus(false); + } + + /** + * Gets the build status of the remote build and blocks until it finished. + * + * @return the {@link BuildStatus} reflecting a {@link hudson.model.Result}. + * @throws IOException + * if there is an error retrieving the remote build number, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @Whitelisted + public BuildStatus getBuildStatusBlocking() throws IOException, InterruptedException { + return getBuildStatus(true); + } + + private BuildStatus getBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { + //Return if buildStatus exists and is final (does not change anymore) + if(buildStatus != null && isFinishedBuildStatus(buildStatus)) return buildStatus; + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + buildStatus = null; + boolean finished = false; + while(!finished) { + //TODO: This currently blocks + BuildData buildData = getBuildData(queueId, log.getPrintStream()); + String jobLocation = buildData.getURL() + "api/json/"; + BuildContext context = new BuildContext(log.getPrintStream(), this.currentItem); + buildStatus = remoteBuildConfiguration.getBuildStatus(jobLocation, context); + finished = isFinishedBuildStatus(buildStatus); + if(!blockUntilFinished) break; + } + return buildStatus; + } finally { + lastLog = log.getContent(); + } + } + + public void setBuildStatus(BuildStatus buildStatus) + { + this.buildStatus = buildStatus; + } + + private boolean isFinishedBuildStatus(BuildStatus buildStatus) + { + if(buildStatus == null) return false; + return buildStatus.isJenkinsResult(); + } + + /** + * This method returns the log entries which resulted from the last method call + * to the Handle. This is a workaround since logging to the pipeline log directly does + * not work yet if used asynchronously. + * + * @return The latest log entries from the last called method. + */ + @Whitelisted + public String lastLog() { + String log = lastLog.trim(); + lastLog = ""; + return log; + } + + public void setBuildData(BuildData buildData) + { + this.buildData = buildData; + } + + @Whitelisted + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + String remoteServerURL; + try { + remoteServerURL = remoteBuildConfiguration.findEffectiveRemoteHost(null).getAddress().toString(); + } + catch (IOException e) { + remoteServerURL = e.getMessage(); + } + + sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); + if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); + if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); + sb.append("]"); + return sb.toString(); + } + + /** + * This method returns a all available methods. This might be helpful to get available methods + * while developing and testing a pipeline script. + * + * @return a string representing all the available methods. + */ + @Whitelisted + public static String help() { + StringBuilder sb = new StringBuilder(); + sb.append("This object provides the following methods:\n"); + for (Method method : Handle.class.getDeclaredMethods()) { + if (method.getAnnotation(Whitelisted.class) != null && Modifier.isPublic(method.getModifiers())) { + sb.append("- ").append(method.getReturnType().getSimpleName()).append(" "); + sb.append(method.getName()).append("("); + Class[] params = method.getParameterTypes(); + for(Class param : params) { + if(params.length > 1 && !param.equals(params[0])) sb.append( ", "); + sb.append(param.getSimpleName()); + } + sb.append(")\n"); + } + } + return sb.toString(); + } + + private BuildData getBuildData(String queueId, PrintStream logger) throws IOException, InterruptedException + { + //Return if we already have the buildData + if(buildData != null) return buildData; + + BuildContext context = new BuildContext(logger, this.currentItem); + BuildData build = remoteBuildConfiguration.getBuildData(queueId, context); + this.buildData = build; + return build; + } + + /** + * This method reads and parses a JSON file which has been archived by the last remote build. + * From Groovy/Pipeline code elements can be accessed directly via object.nodeC.nodeB.leafC. + * + * @param filename + * the filename or path to the remote JSON file relative to the last builds archive folder + * @return JSON structure as Object (consisting of Map, List, and primitive types), or null if not available (yet) + * @throws IOException + * if there is an error identifying the remote host, or + * if there is an error setting the authorization header, or + * if the request fails due to an unknown host, unauthorized credentials, or another reason. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * + */ + @Whitelisted + public Object readJsonFileFromBuildArchive(String filename) throws IOException, InterruptedException { + if(isEmpty(filename)) return null; + + URL remoteBuildUrl = getBuildUrl(); + if(remoteBuildUrl == null) return null; + URL fileUrl = new URL(remoteBuildUrl, "artifact/" + filename); + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + BuildContext context = new BuildContext(log.getPrintStream(), this.currentItem); + return remoteBuildConfiguration.sendHTTPCall(fileUrl.toString(), "GET", context); + } finally { + lastLog = log.getContent(); + } + } + + public void setJobMetadata(JSONObject remoteJobMetadata) + { + this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); + this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); + this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); + this.jobFullDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "fullDisplayName"); + this.jobUrl = getParameterFromJobMetadata(remoteJobMetadata, "url"); + } + + private String getParameterFromJobMetadata(JSONObject remoteJobMetadata, String string) + { + try { + return trimToNull(remoteJobMetadata.getString("name")); + } + catch (JSONException e) { + return null; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java new file mode 100644 index 00000000..4e19081a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java @@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.io.IOUtils; + +/** + * Wrapper to provide a PrintStream for writing content to + * and a corresponding getContent() method to get the content + * which has been written to the PrintStream. + * + * The reason is from the async Pipeline Handle we don't have + * an active TaskListener.getLogger() anymore this means everything + * written to the PrintStream (logger) will not be printed to the Pipeline log. + * Therefore we provide this PrintStream for logging and the content can be + * obtained later via getContent(). + */ +public class PrintStreamWrapper +{ + + private final ByteArrayOutputStream byteStream; + private final PrintStream printStream; + + public PrintStreamWrapper() throws UnsupportedEncodingException { + byteStream = new ByteArrayOutputStream(); + printStream = new PrintStream(byteStream, false, "UTF-8"); + } + + public PrintStream getPrintStream() { + return printStream; + } + + /** + * Returns all logs since creation and closes the streams. + * + * @return all logs. + * @throws IOException + * if UTF-8 charset is not supported. + */ + public String getContent() throws IOException { + String string = byteStream.toString("UTF-8"); + close(); + return string; + } + + /** + * Closes the streams. + */ + public void close() { + IOUtils.closeQuietly(printStream); + IOUtils.closeQuietly(byteStream); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java new file mode 100644 index 00000000..fd440b89 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -0,0 +1,270 @@ +/* + * The MIT License + * + * 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 org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; + +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +public class RemoteBuildPipelineStep extends Step { + + private RemoteBuildConfiguration remoteBuildConfig; + + + @DataBoundConstructor + public RemoteBuildPipelineStep(String job) { + remoteBuildConfig = new RemoteBuildConfiguration(); + remoteBuildConfig.setJob(job); + remoteBuildConfig.setShouldNotFailBuild(false); //We need to get notified. Failure feedback is collected async then. + remoteBuildConfig.setBlockBuildUntilComplete(true); //default for Pipeline Step + } + + @DataBoundSetter + public void setAuth(Auth2 auth) { + remoteBuildConfig.setAuth2(auth); + } + + public Auth2 getAuth() { + return remoteBuildConfig.getAuth2(); + } + + @DataBoundSetter + public void setRemoteJenkinsName(String remoteJenkinsName) { + remoteBuildConfig.setRemoteJenkinsName(remoteJenkinsName); + } + + @DataBoundSetter + public void setRemoteJenkinsUrl(String remoteJenkinsUrl) { + remoteBuildConfig.setRemoteJenkinsUrl(remoteJenkinsUrl); + } + + @DataBoundSetter + public void setShouldNotFailBuild(boolean shouldNotFailBuild) { + remoteBuildConfig.setShouldNotFailBuild(shouldNotFailBuild); + } + + @DataBoundSetter + public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { + remoteBuildConfig.setPreventRemoteBuildQueue(preventRemoteBuildQueue); + } + + @DataBoundSetter + public void setPollInterval(int pollInterval) { + remoteBuildConfig.setPollInterval(pollInterval); + } + + @DataBoundSetter + public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { + remoteBuildConfig.setBlockBuildUntilComplete(blockBuildUntilComplete); + } + + @DataBoundSetter + public void setToken(String token) { + remoteBuildConfig.setToken(token); + } + + @DataBoundSetter + public void setParameters(String parameters) { + remoteBuildConfig.setParameters(parameters); + } + + @DataBoundSetter + public void setEnhancedLogging(boolean enhancedLogging) { + remoteBuildConfig.setEnhancedLogging(enhancedLogging); + } + + @DataBoundSetter + public void setLoadParamsFromFile(boolean loadParamsFromFile) { + remoteBuildConfig.setLoadParamsFromFile(loadParamsFromFile); + } + + @DataBoundSetter + public void setParameterFile(String parameterFile) { + remoteBuildConfig.setParameterFile(parameterFile); + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new Execution(context, remoteBuildConfig); + } + + @Extension(optional = true) + public static final class DescriptorImpl extends StepDescriptor { + + @Override public String getFunctionName() { + return "triggerRemoteJob"; + } + + @Override public String getDisplayName() { + return "Trigger Remote Job"; + } + + @Override public Set> getRequiredContext() { + Set> set = new HashSet>(); + Collections.addAll(set, Run.class, FilePath.class, Launcher.class, TaskListener.class); + return set; + } + + public ListBoxModel doFillRemoteJenkinsNameItems() { + return RemoteBuildConfiguration.getDescriptorStatic().doFillRemoteJenkinsNameItems(); + } + + public FormValidation doCheckJob( + @QueryParameter("job") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); + if(result.isAffected(AffectedField.jobNameOrUrl)) return result.formValidation; + return FormValidation.ok(); + } + + public FormValidation doCheckRemoteJenkinsUrl( + @QueryParameter("remoteJenkinsUrl") final String value, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); + if(result.isAffected(AffectedField.remoteJenkinsUrl)) return result.formValidation; + return FormValidation.ok(); + } + + public FormValidation doCheckRemoteJenkinsName( + @QueryParameter("remoteJenkinsName") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); + if(result.isAffected(AffectedField.remoteJenkinsName)) return result.formValidation; + return FormValidation.ok(); + } + + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NullAuth.DESCRIPTOR; + } + } + + public static class Execution extends SynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 5339071667093320735L; + + private final RemoteBuildConfiguration remoteBuildConfig; + + Execution(StepContext context, RemoteBuildConfiguration remoteBuildConfig) { + super(context); + this.remoteBuildConfig = remoteBuildConfig; + } + + @Override protected Handle run() throws Exception { + StepContext stepContext = getContext(); + Run build = stepContext.get(Run.class); + FilePath workspace = stepContext.get(FilePath.class); + TaskListener listener = stepContext.get(TaskListener.class); + BuildContext context = new BuildContext(build, workspace, listener); + Handle handle = remoteBuildConfig.performTriggerAndGetQueueId(context); + if(remoteBuildConfig.getBlockBuildUntilComplete()) { + remoteBuildConfig.performWaitForBuild(context, handle); + } + return handle; + } + } + + public String getRemoteJenkinsName() { + return remoteBuildConfig.getRemoteJenkinsName(); + } + + public String getRemoteJenkinsUrl() { + return remoteBuildConfig.getRemoteJenkinsUrl(); + } + + public String getJob() { + return remoteBuildConfig.getJob(); + } + + public boolean getShouldNotFailBuild() { + return remoteBuildConfig.getShouldNotFailBuild(); + } + + public boolean getPreventRemoteBuildQueue() { + return remoteBuildConfig.getPreventRemoteBuildQueue(); + } + + public int getPollInterval() { + return remoteBuildConfig.getPollInterval(); + } + + public boolean getBlockBuildUntilComplete() { + return remoteBuildConfig.getBlockBuildUntilComplete(); + } + + public String getToken() { + return remoteBuildConfig.getToken(); + } + + public String getParameters() { + return remoteBuildConfig.getParameters(); + } + + public boolean getEnhancedLogging() { + return remoteBuildConfig.getEnhancedLogging(); + } + + public boolean getLoadParamsFromFile() { + return remoteBuildConfig.getLoadParamsFromFile(); + } + + public String getParameterFile() { + return remoteBuildConfig.getParameterFile(); + } + + public int getConnectionRetryLimit() { + return remoteBuildConfig.getConnectionRetryLimit(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java new file mode 100644 index 00000000..339da8ed --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java @@ -0,0 +1,44 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.io.Serializable; +import java.net.URL; + +import javax.annotation.Nonnull; + +/** + * Contains information about the location of the job while is being built. + * + */ +public class BuildData implements Serializable +{ + private static final long serialVersionUID = 3553303097206059203L; + + @Nonnull + private final int buildNumber; + + @Nonnull + private final URL buildURL; + + public BuildData(@Nonnull int buildNumber, @Nonnull URL buildURL) + { + this.buildNumber = buildNumber; + this.buildURL = buildURL; + } + + public int getBuildNumber() + { + return buildNumber; + } + + public URL getURL() + { + return buildURL; + } + + @Override + public String toString() + { + return "RemoteBuild [buildNumber=" + buildNumber + ", buildURL=" + buildURL + "]"; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java similarity index 58% rename from src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildInfoExporterAction.java rename to src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index b000c271..931e6e4e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -1,19 +1,23 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger; - -import hudson.EnvVars; -import hudson.model.EnvironmentContributingAction; -import hudson.model.Result; -import hudson.model.AbstractBuild; +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; +import java.net.URL; import java.util.ArrayList; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -class BuildInfoExporterAction implements EnvironmentContributingAction { +import javax.annotation.Nonnull; + +import hudson.EnvVars; +import hudson.model.AbstractBuild; +import hudson.model.EnvironmentContributingAction; +import hudson.model.Run; + +public class BuildInfoExporterAction implements EnvironmentContributingAction { public static final String JOB_NAME_VARIABLE = "LAST_TRIGGERED_JOB_NAME"; public static final String ALL_JOBS_NAME_VARIABLE = "TRIGGERED_JOB_NAMES"; + public static final String BUILD_URL_VARIABLE_PREFIX = "TRIGGERED_BUILD_URL_"; public static final String BUILD_NUMBER_VARIABLE_PREFIX = "TRIGGERED_BUILD_NUMBER_"; public static final String ALL_BUILD_NUMBER_VARIABLE_PREFIX = "TRIGGERED_BUILD_NUMBERS_"; public static final String BUILD_RESULT_VARIABLE_PREFIX = "TRIGGERED_BUILD_RESULT_"; @@ -22,39 +26,69 @@ class BuildInfoExporterAction implements EnvironmentContributingAction { private List builds; - public BuildInfoExporterAction(AbstractBuild parentBuild, BuildReference buildRef) { + public BuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { super(); this.builds = new ArrayList(); - this.builds.add(buildRef); + addBuildReferenceSafe(buildRef); } - static BuildInfoExporterAction addBuildInfoExporterAction(AbstractBuild parentBuild, String triggeredProject, int buildNumber, Result buildResult) { - BuildReference reference = new BuildReference(triggeredProject, buildNumber, buildResult); + public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, BuildStatus buildResult) { + BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildResult); BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); if (action == null) { action = new BuildInfoExporterAction(parentBuild, reference); - parentBuild.getActions().add(action); + parentBuild.addAction(action); } else { action.addBuildReference(reference); } return action; } + /** + * Prevents duplicate build refs. The latest BuildReference wins (to reflect the latest Result). + */ + private void addBuildReferenceSafe(BuildReference buildRef) + { + removeDuplicates(builds, buildRef); + builds.add(buildRef); + } + + /** + * Finds and removes duplicates of buildRef in the buildRefList based on the projectName and buildNumber (only). + * @return true if duplicates found and removed, false if nothing found + */ + private boolean removeDuplicates(List buildRefList, BuildReference buildRef) { + List duplicates = new ArrayList(); + for(BuildReference build : buildRefList) { + if(build.projectName.equals(buildRef.projectName) && build.buildNumber == buildRef.buildNumber) { + duplicates.add(build); + } + } + if(duplicates.size() > 0) { + buildRefList.removeAll(duplicates); + return true; + } else { + return false; + } + } + public void addBuildReference(BuildReference buildRef) { - this.builds.add(buildRef); + addBuildReferenceSafe(buildRef); } public static class BuildReference { public final String projectName; public final int buildNumber; - public final Result buildResult; + public final BuildStatus buildResult; + public final URL jobURL; - public BuildReference(String projectName, int buildNumber, Result buildResult) { + public BuildReference(String projectName, int buildNumber, URL jobURL, BuildStatus buildResult) { this.projectName = projectName; this.buildNumber = buildNumber; this.buildResult = buildResult; + this.jobURL = jobURL; } } @@ -75,14 +109,14 @@ public String getUrlName() { public void buildEnvVars(AbstractBuild build, EnvVars env) { for (String project : getProjectsWithBuilds()) { - String sanatizedBuildName = project.replaceAll("[^a-zA-Z0-9]+", "_"); + String sanatizedProjectName = sanitizeProjectName(project); List refs = getBuildRefs(project); - env.put(ALL_BUILD_NUMBER_VARIABLE_PREFIX + sanatizedBuildName, getBuildNumbersString(refs, ",")); - env.put(BUILD_RUN_COUNT_PREFIX + sanatizedBuildName, Integer.toString(refs.size())); + env.put(ALL_BUILD_NUMBER_VARIABLE_PREFIX + sanatizedProjectName, getBuildNumbersString(refs, ",")); + env.put(BUILD_RUN_COUNT_PREFIX + sanatizedProjectName, Integer.toString(refs.size())); for (BuildReference br : refs) { if (br.buildNumber != 0) { - String tiggeredBuildRunResultKey = BUILD_RESULT_VARIABLE_PREFIX + sanatizedBuildName + RUN + Integer.toString(br.buildNumber); + String tiggeredBuildRunResultKey = BUILD_RESULT_VARIABLE_PREFIX + sanatizedProjectName + RUN + Integer.toString(br.buildNumber); env.put(tiggeredBuildRunResultKey, br.buildResult.toString()); } } @@ -94,12 +128,20 @@ public void buildEnvVars(AbstractBuild build, EnvVars env) { } } if (lastBuild != null) { - env.put(BUILD_NUMBER_VARIABLE_PREFIX + sanatizedBuildName, Integer.toString(lastBuild.buildNumber)); - env.put(BUILD_RESULT_VARIABLE_PREFIX + sanatizedBuildName, lastBuild.buildResult.toString()); + env.put(JOB_NAME_VARIABLE, lastBuild.projectName); + env.put(BUILD_NUMBER_VARIABLE_PREFIX + sanatizedProjectName, Integer.toString(lastBuild.buildNumber)); + env.put(BUILD_URL_VARIABLE_PREFIX + sanatizedProjectName, lastBuild.jobURL.toString()); + env.put(BUILD_RESULT_VARIABLE_PREFIX + sanatizedProjectName, lastBuild.buildResult.toString()); } } } + public static String sanitizeProjectName(String project) + { + if(project == null) return null; + return project.replaceAll("[^a-zA-Z0-9]+", "_"); + } + private List getBuildRefs(String project) { List refs = new ArrayList(); for (BuildReference br : builds) { @@ -134,15 +176,18 @@ private String getBuildNumbersString(List refs, String separator } /** - * Gets the unique set of project names that have a linked build. + * Gets the unique set of project names that have a linked build.
    + * The later triggered jobs are later in the list. E.g.
    + * C, A, B -> C, A, B
    + * C, A, B, A, C -> B, A, C
    * * @return Set of project names that have at least one build linked. */ private Set getProjectsWithBuilds() { - Set projects = new HashSet(); - + Set projects = new LinkedHashSet(); for (BuildReference br : this.builds) { if (br.buildNumber != 0) { + if(projects.contains(br.projectName)) projects.remove(br.projectName); //Move to the end projects.add(br.projectName); } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java new file mode 100644 index 00000000..ab39a4c2 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java @@ -0,0 +1,92 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import hudson.model.Result; + +/** + * The build status of a remote build either reflecting a {@link hudson.model.Result} if finished, + * or if not finished yet a custom status like QUEUED, RUNNING,...
    + * Using {@link #isJenkinsResult()} it can be checked if the status if reflecting a {@link Result} or a custom status.
    + * The Jenkins {@link Result} can be obtained using {@link BuildStatus#getJenkinsResult()}. + */ +public enum BuildStatus +{ + + /** + * custom status indicating an UNKNOWN state + */ + UNKNOWN("UNKNOWN"), + + /** + * custom status indicating nothing started yet, neither QUEUED nor RUNNING + */ + NOT_STARTED("NOT_STARTED"), + + /** + * custom status indicating the remote build is in the QUEUE but not running yet + */ + QUEUED("QUEUED"), + + /** + * custom status indicating the build is RUNNING currently. + */ + RUNNING("RUNNING"), + + /** + * Status corresponding to the Jenkins Result.ABORTED + */ + ABORTED(Result.ABORTED), + + /** + * Status corresponding to the Jenkins Result.FAILURE + */ + FAILURE(Result.FAILURE), + + /** + * Status corresponding to the Jenkins Result.NOT_BUILT + */ + NOT_BUILT(Result.NOT_BUILT), + + /** + * Status corresponding to the Jenkins Result.SUCCESS + */ + SUCCESS(Result.SUCCESS), + + /** + * Status corresponding to the Jenkins Result.UNSTABLE + */ + UNSTABLE(Result.UNSTABLE); + + + private final String id; + private final Result jenkinsResult; + + private BuildStatus(String id) { + this.id = id; + this.jenkinsResult = null; + } + + private BuildStatus(Result jenkinsResult) { + this.id = jenkinsResult.toString(); + this.jenkinsResult = jenkinsResult; + } + + /** + * @return The corresponding Jenkins {@link Result} or null if it is a custom status + */ + public Result getJenkinsResult() { + return jenkinsResult; + } + + /** + * @return true if it reflects a Jenkins {@link Result} or false if it is a custom status + */ + public boolean isJenkinsResult() { + return jenkinsResult != null; + } + + @Override + public String toString() { + return id; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java new file mode 100644 index 00000000..522e69e7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java @@ -0,0 +1,47 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; + +import hudson.AbortException; + +/** + * Represents an item on the queue. Contains information about the location + * of the job in the queue. + * + */ +public class QueueItem +{ + final static private String key = "Location"; + + @Nonnull + private final String location; + + @Nonnull + private final String id; + + + public QueueItem(@Nonnull Map> header) throws AbortException + { + if (!header.containsKey(key)) + throw new AbortException(String.format("Error triggering the remote job. The header of the response has an unexpected format: %n%s", header)); + location = header.get(key).get(0); + try { + String loc = location.substring(0, location.lastIndexOf('/')); + id = loc.substring(loc.lastIndexOf('/')+1); + } catch (Exception ex) { + throw new AbortException(String.format("Error triggering the remote job. The header of the response contains an unexpected location: %s", location)); + } + } + + public String getLocation() { + return location; + } + + public String getId() { + return id; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java new file mode 100644 index 00000000..d8eddb83 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -0,0 +1,105 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.net.MalformedURLException; +import java.net.URL; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; + +import net.sf.json.JSONException; +import net.sf.json.JSONObject; + +/** + * Contains information about the job while is waiting on the queue. + * + */ +public class QueueItemData +{ + @Nonnull + private final JSONObject queueResponse; + + + public QueueItemData(@Nonnull JSONObject queueResponse) + { + this.queueResponse = queueResponse; + } + + public boolean isBlocked() + { + return queueResponse.getBoolean("blocked"); + } + + public boolean isBuildable() + { + return queueResponse.getBoolean("buildable"); + } + + public boolean isPending() + { + return getOptionalBoolean("pending"); + } + + public boolean isCancelled() + { + return getOptionalBoolean("cancelled"); + } + + public String getWhy() + { + return queueResponse.getString("why"); + } + + public boolean isExecutable() + { + return (!isBlocked() && !isBuildable() && !isPending() && !isCancelled()); + } + + /** + * When a queue item is executable, the build number and the build URL + * of the remote job are available in the queue item data. + * + * @param context + * the context of this Builder/BuildStep. + * @return {@link BuildData} + * the remote build or null if the queue item is not executable. + * @throws MalformedURLException + * if there is an error creating the build URL. + */ + @CheckForNull + public BuildData getBuildData(@Nonnull BuildContext context) throws MalformedURLException + { + if (!isExecutable()) return null; + + JSONObject remoteJobInfo; + try { + remoteJobInfo = queueResponse.getJSONObject("executable"); + } catch (JSONException e) { + context.logger.println("The attribute \"executable\" was not found. Unexpected response: " + queueResponse.toString()); + return null; + } + int buildNumber; + try { + buildNumber = remoteJobInfo.getInt("number"); + } catch (JSONException e) { + context.logger.println("The attribute \"number\" was not found. Unexpected response: " + queueResponse.toString()); + return null; + } + String buildUrl; + try { + buildUrl = remoteJobInfo.getString("url"); + } catch (JSONException e) { + context.logger.println("The attribute \"url\" was not found. Unexpected response: " + queueResponse.toString()); + return null; + } + return new BuildData(buildNumber, new URL(buildUrl)); + } + + private boolean getOptionalBoolean(String attribute) + { + if (queueResponse.containsKey(attribute)) + return queueResponse.getBoolean(attribute); + else return false; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java new file mode 100644 index 00000000..e6abf2e5 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java @@ -0,0 +1,62 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.apache.commons.lang.StringUtils.isEmpty; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import javax.annotation.Nonnull; + +import org.apache.commons.codec.binary.Base64; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; + +public class Base64Utils +{ + + public static final String AUTHTYPE_BASIC = "Basic"; + + public static String encode(String input) throws UnsupportedEncodingException + { + byte[] encoded = Base64.encodeBase64(input.getBytes("UTF-8")); + return new String(encoded, "UTF-8"); + } + + /** + * Creates the value for an Authorization header consisting of:
    + * "authType base64Encoded(user:password)"
    + * e.g. "Basic zhwef9tz33ergwerg4394zh370345zh==" + * + * @param authType + * the authorization type. + * @param user + * the user name. + * @param password + * the user password. + * @param context + * the context of this Builder/BuildStep. + * @return the base64 encoded authorization. + * @throws IOException + * if there is a failure while replacing token macros, or + * if there is a failure while encoding user:password. + */ + @Nonnull + public static String generateAuthorizationHeaderValue(String authType, String user, String password, + BuildContext context) throws IOException + { + if (isEmpty(user)) throw new IllegalArgumentException("user null or empty"); + if (password == null) throw new IllegalArgumentException("password null"); // is empty password allowed for Basic Auth? + String authTypeKey = getAuthType(authType); + String tuple = user + ":" + password; + tuple = TokenMacroUtils.applyTokenMacroReplacements(tuple, context); + String encodedTuple = Base64Utils.encode(tuple); + return authTypeKey + " " + encodedTuple; + } + + @Nonnull + private static String getAuthType(String authType) + { + if ("Basic".equalsIgnoreCase(authType)) return "Basic"; + throw new IllegalArgumentException("AuthType wrong or not supported yet: " + authType); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java new file mode 100644 index 00000000..d4acdea3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -0,0 +1,117 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import hudson.util.FormValidation; + +public class FormValidationUtils +{ + + public static enum AffectedField { + jobNameOrUrl, remoteJenkinsUrl, remoteJenkinsName + } + + public static class RemoteURLCombinationsResult { + + public final FormValidation formValidation; + public final AffectedField[] affectedFields; + + public RemoteURLCombinationsResult(FormValidation formValidation, AffectedField...affectedFields) { + this.formValidation = formValidation; + this.affectedFields = affectedFields; + } + + public boolean isAffected(AffectedField field) { + return Arrays.asList(affectedFields).contains(field); + } + + public static RemoteURLCombinationsResult OK() + { + return new RemoteURLCombinationsResult(FormValidation.ok(), AffectedField.values()); + } + + } + + public static RemoteURLCombinationsResult checkRemoteURLCombinations(String remoteJenkinsUrl, String remoteJenkinsName, String jobNameOrUrl) { + remoteJenkinsUrl = trimToNull(remoteJenkinsUrl); + remoteJenkinsName = trimToNull(remoteJenkinsName); + jobNameOrUrl = trimToNull(jobNameOrUrl); + boolean remoteUrl_setAndValidUrl = isEmpty(remoteJenkinsUrl) ? false : isURL(remoteJenkinsUrl); + boolean remoteName_setAndValid = !isEmpty(remoteJenkinsName); + boolean job_setAndValidUrl = isEmpty(jobNameOrUrl) ? false : isURL(jobNameOrUrl); + boolean job_setAndNoUrl = isEmpty(jobNameOrUrl) ? false : !isURL(jobNameOrUrl); + boolean job_containsVariable = isEmpty(jobNameOrUrl) ? false : jobNameOrUrl.indexOf("$") >= 0; + final String TEXT_WARNING_JOB_VARIABLE = "You are using a variable in the 'Remote Job Name or URL' ('job') field. You have to make sure the value at runtime results in the full job URL"; + final String TEXT_ERROR_NO_URL_AT_ALL = "You have to configure either 'Select a remote host' ('remoteJenkinsName'), 'Override remote host URL' ('remoteJenkinsUrl') or specify a full job URL 'Remote Job Name or URL' ('job')"; + + if(isEmpty(jobNameOrUrl)) { + return new RemoteURLCombinationsResult( + FormValidation.error("'Remote Job Name or URL' ('job') not specified"), + AffectedField.jobNameOrUrl); + } else if(!isEmpty(remoteJenkinsUrl) && !isURL(remoteJenkinsUrl)) { + return new RemoteURLCombinationsResult( + FormValidation.error("Invalid URL in 'Override remote host URL' ('remoteJenkinsUrl')"), + AffectedField.remoteJenkinsUrl); + } else if(!remoteUrl_setAndValidUrl && !remoteName_setAndValid && !job_setAndValidUrl) { + //Root URL or full job URL not specified at all + if(job_containsVariable) { + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + } else { + return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), + AffectedField.jobNameOrUrl, AffectedField.remoteJenkinsName, AffectedField.remoteJenkinsUrl); + } + } else if(job_setAndValidUrl) { + return RemoteURLCombinationsResult.OK(); + } else if(remoteUrl_setAndValidUrl && job_setAndNoUrl) { + if(job_containsVariable) { + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + } else { + return RemoteURLCombinationsResult.OK(); + } + } else if(remoteName_setAndValid && job_setAndNoUrl) { + if(job_containsVariable) { + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + } else { + return RemoteURLCombinationsResult.OK(); + } + } else { + return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), + AffectedField.jobNameOrUrl, AffectedField.remoteJenkinsName, AffectedField.remoteJenkinsUrl); + } + } + + /** + * Checks if a string is a valid http/https URL. + * + * @param string + * the url to check. + * @return true if parameter is a valid http/https URL. + */ + public static boolean isURL(String string) { + if(isEmpty(trimToNull(string))) return false; + String stringLower = string.toLowerCase(); + if(stringLower.startsWith("http://") || stringLower.startsWith("https://")) { + if(stringLower.indexOf("://") >= stringLower.length()-3) { + return false; //URL ends after protocol + } + if(stringLower.indexOf("$") >= 0) { + return false; //We interpret $ in URLs as variables which need to be replaced. TODO: What about URI standard which allows $? + } + try { + new URL(string); + return true; + } + catch (MalformedURLException e) { + return false; + } + } else { + return false; + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java new file mode 100644 index 00000000..14f3906b --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java @@ -0,0 +1,44 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; +import org.jenkinsci.plugins.tokenmacro.TokenMacro; + +public class TokenMacroUtils +{ + + public static String applyTokenMacroReplacements(String input, BuildContext context) throws IOException + { + try { + if (isUseTokenMacro(context)) { + return TokenMacro.expandAll(context.run, context.workspace, context.listener, input); + } + } + catch (MacroEvaluationException e) { + throw new IOException(e); + } + catch (InterruptedException e) { + throw new IOException(e); + } + return input; + } + + public static List applyTokenMacroReplacements(List inputs, BuildContext context) throws IOException + { + List outputs = new ArrayList(); + for (String input : inputs) { + outputs.add(applyTokenMacroReplacements(input, context)); + } + return outputs; + } + + public static boolean isUseTokenMacro(BuildContext context) + { + return context != null && context.run != null && context.workspace != null && context.listener != null; + } + +} diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly index d65fac76..de8e2c30 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly @@ -1,27 +1,27 @@ - - - - + +
    + + - + - + - - + +
    -
    +
    diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index bd792469..3b56ac58 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -1,55 +1,55 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly index eff95cf3..d2274898 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html new file mode 100644 index 00000000..4f66f4aa --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html @@ -0,0 +1,21 @@ +

    diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-job.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-job.html index a166efd7..5cf797c5 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-job.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-job.html @@ -1,6 +1,6 @@
    -
    - Remote Job Name -
    - The job on the remote Jenkins host which you would like to trigger -
    \ No newline at end of file +
    + Remote Job Name or full URL. +
    + The name or URL of the job on the remote Jenkins host which you would like to trigger. If the full job URL is specified the URL of the remote Jenkins host configured above will be ignored. +

    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 literal 0 HcmV?d00001 diff --git a/screenshots/pipelineSyntaxGenerator2.png b/screenshots/pipelineSyntaxGenerator2.png new file mode 100644 index 0000000000000000000000000000000000000000..e301b4b496534c24d59f9c08e1697b5b4378068c GIT binary patch literal 54500 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 implements Serializable { + + private static final long serialVersionUID = 5110932168554914718L; -public class Auth extends AbstractDescribableImpl { + public static final String NONE = "none"; + public static final String API_TOKEN = "apiToken"; + public static final String CREDENTIALS_PLUGIN = "credentialsPlugin"; private final String authType; private final String username; private final String apiToken; private final String creds; - public final String NONE = "none"; - public final String API_TOKEN = "apiToken"; - public final String CREDENTIALS_PLUGIN = "credentialsPlugin"; - - // @DataBoundConstructor - /* - * public Auth(String value, String username, String apiToken, String creds) { this.authType = value; this.username - * = username; this.apiToken = apiToken; this.creds = creds; } - */ - @DataBoundConstructor - public Auth(JSONObject formData) { - - JSONObject authMode = new JSONObject(); - // because I don't know how to automatically bind a JSON object to properties in a constructor, we are manually - // pulling out each item and assigning it - if (formData.has("authenticationMode")) { - authMode = (JSONObject) formData.get("authenticationMode"); - } - - this.authType = (String) authMode.get("value"); - this.username = (String) authMode.get("username"); - this.apiToken = (String) authMode.get("apiToken"); - this.creds = (String) authMode.get("creds"); + public Auth(String authType, String username, String apiToken, String creds) { + this.authType = authType; + this.username = username; + this.apiToken = apiToken; + this.creds = creds; } public String getAuthType() { - return this.authType; - } - - public Boolean isMatch(String value) { - return this.getAuthType().equals(value); + return authType; } public String getUsername() { - String authType = this.getAuthType(); - String username = null; - - if(authType == null) { - username = ""; - }else if (authType.equals(NONE)) { - username = ""; - } else if (authType.equals(API_TOKEN)) { - username = this.username; - } else if (authType.equals(CREDENTIALS_PLUGIN)) { - username = this.getCredentials().getUsername(); - } - return username; } - public String getPassword() { - String authType = this.getAuthType(); - String password = null; + public String getApiToken() { + return apiToken; + } - if (authType.equals(NONE)) { - password = ""; - } else if (authType.equals(API_TOKEN)) { - password = this.getApiToken(); - } else if (authType.equals(CREDENTIALS_PLUGIN)) { - password = Secret.toString(this.getCredentials().getPassword()); - } + public String getCreds() { + return creds; + } - return password; + public Boolean isMatch(String value) { + return authType.equals(value); } - public String getApiToken() { - return this.apiToken; + public String getUser(){ + if (authType.equals(API_TOKEN)){ + return username; + } else if (authType.equals(CREDENTIALS_PLUGIN)){ + UsernamePasswordCredentials creds = getCredentials(); + return creds != null ? creds.getUsername() : ""; + } else { + return ""; + } } - public String getCreds() { - return this.creds; + public String getPassword(){ + if (authType.equals(API_TOKEN)){ + return apiToken; + } else if (authType.equals(CREDENTIALS_PLUGIN)){ + UsernamePasswordCredentials creds = getCredentials(); + return creds != null ? creds.getPassword().getPlainText() : ""; + } else { + return ""; + } } /** @@ -111,21 +99,21 @@ public String getCreds() { * @return the matched credentials */ private UsernamePasswordCredentials getCredentials() { - String credetialId = this.getCreds(); - StandardUsernameCredentials matchedCredentials = null; Item item = null; List listOfCredentials = CredentialsProvider.lookupCredentials( StandardUsernameCredentials.class, item, ACL.SYSTEM, Collections. emptyList()); + + return (UsernamePasswordCredentials) findCredential(creds, listOfCredentials); + } + + private StandardUsernameCredentials findCredential(String credetialId, List listOfCredentials){ for (StandardUsernameCredentials cred : listOfCredentials) { if (credetialId.equals(cred.getId())) { - matchedCredentials = cred; - break; + return cred; } } - - // now we have matchedCredentials.getPassword() and matchedCredentials.getUsername(); - return (UsernamePasswordCredentials) matchedCredentials; + return null; } @Override @@ -133,6 +121,10 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } + /** + * We need to keep this for compatibility - old config deserialization! + * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. + */ @Extension public static class DescriptorImpl extends Descriptor { @Override @@ -160,6 +152,51 @@ public static ListBoxModel doFillCredsItems() { return model; } + } + + public static Auth auth2ToAuth(Auth2 auth) { + if (auth == null) + return null; + if (auth instanceof NoneAuth) { + return new Auth(Auth.NONE, null, null, null); + } else if (auth instanceof TokenAuth) { + TokenAuth tokenAuth = (TokenAuth) auth; + return new Auth(Auth.API_TOKEN, tokenAuth.getUserName(), tokenAuth.getApiToken(), null); + } else if (auth instanceof CredentialsAuth) { + CredentialsAuth credAuth = (CredentialsAuth) auth; + try { + String credUser = credAuth.getUserName(null); + String credPass = credAuth.getPassword(null); + return new Auth(Auth.CREDENTIALS_PLUGIN, credUser, credPass, credAuth.getCredentials()); + } + catch (CredentialsNotFoundException e) { + return new Auth(Auth.CREDENTIALS_PLUGIN, "", "", credAuth.getCredentials()); + } + } else { + return null; + } + } + public static Auth2 authToAuth2(List oldAuth) { + if(oldAuth == null || oldAuth.size() <= 0) return new NullAuth(); + return authToAuth2(oldAuth.get(0)); + } + + public static Auth2 authToAuth2(Auth oldAuth) { + String authType = oldAuth.getAuthType(); + if (Auth.NONE.equals(authType)) { + return new NoneAuth(); + } else if (Auth.API_TOKEN.equals(authType)) { + TokenAuth newAuth = new TokenAuth(); + newAuth.setUserName(oldAuth.getUsername()); + newAuth.setApiToken(oldAuth.getApiToken()); + return newAuth; + } else if (Auth.CREDENTIALS_PLUGIN.equals(authType)) { + CredentialsAuth newAuth = new CredentialsAuth(); + newAuth.setCredentials(oldAuth.getCreds()); + return newAuth; + } else { + return new NullAuth(); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java new file mode 100644 index 00000000..dcd39e74 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -0,0 +1,92 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import static org.apache.commons.lang.StringUtils.trimToNull; + +import java.io.PrintStream; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNullableByDefault; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.model.TaskListener; + +/** + * This object wraps a {@link Run}, {@link FilePath}, {@link TaskListener} and {@link PrintStream} - + * the typical objects passed from one method to the other in a Jenkins Builder/BuildStep implementation.
    + *
    + * The reason for wrapping is simplicity on the one hand. On the other in an asynchronous pipeline usage + * via the {@link Handle} we might not have a {@link Run}, {@link FilePath}, {@link TaskListener}, but we still + * want to provide a {@link PrintStream} for logging. Therefore the first three objects can be null, the {@link PrintStream} + * must not be null. + */ +@ParametersAreNullableByDefault +public class BuildContext +{ + @Nullable + public final Run run; + + @Nullable + public final FilePath workspace; + + @Nullable + public final TaskListener listener; + + @Nonnull + public final PrintStream logger; + + /** + * The current Item (job, pipeline,...) where the plugin is used from. + * + */ + @Nonnull + public final String currentItem; + + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem) { + this.run = run; + this.workspace = workspace; + this.listener = listener; + this.logger = logger; + this.currentItem = getCurrentItem(run, currentItem); + } + + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger) { + this(run, workspace, listener, logger, null); + } + + public BuildContext(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnull TaskListener listener) + { + this(run, workspace, listener, listener.getLogger()); + } + + public BuildContext(@Nonnull PrintStream logger, String currentItem) + { + this(null, null, null, logger, currentItem); + } + + private String getCurrentItem(Run run, String currentItem) + { + String runItem = null; + String curItem = trimToNull(currentItem); + if(run != null && run.getParent() != null) { + runItem = trimToNull(run.getParent().getFullName()); + } + if(runItem != null && curItem != null) { + if(runItem.equals(curItem)) { + return runItem; + } else { + throw new IllegalArgumentException(String.format("Current Item ('%s') and Parent Item from Run ('%s') differ!", curItem, runItem)); + } + } else if(runItem != null) { + return runItem; + } else if(curItem != null) { + return curItem; + } else { + throw new IllegalArgumentException("Both null, Run and Current Item!"); + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java new file mode 100644 index 00000000..dfb8f81f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -0,0 +1,54 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import net.sf.json.JSONObject; + +/** + * Http response containing header, body (JSON format) and response code. + * + */ +public class ConnectionResponse +{ + @Nonnull + private final Map> header; + + @Nullable + private final JSONObject body; + + @Nonnull + private final int responseCode; + + + public ConnectionResponse(@Nonnull Map> header, @Nullable JSONObject body, @Nonnull int responseCode) + { + this.header = header; + this.body = body; + this.responseCode = responseCode; + } + + public ConnectionResponse(@Nonnull Map> header, @Nonnull int responseCode) + { + this.header = header; + this.body = null; + this.responseCode = responseCode; + } + + public Map> getHeader() + { + return header; + } + + public JSONObject getBody() { + return body; + } + + public int getResponseCode() { + return responseCode; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java new file mode 100644 index 00000000..dc724fe4 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java @@ -0,0 +1,29 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +/** + * If the remote Jenkins server uses the "Prevent Cross Site Request Forgery exploits" security option, + * a CSRF protection token must be sent in the header of the request to trigger the remote job. + * This token is called crumb. + * + */ +public class JenkinsCrumb +{ + String headerId; + String crumbValue; + + public JenkinsCrumb(String headerId, String crumbValue) + { + this.headerId = headerId; + this.crumbValue = crumbValue; + } + + public String getHeaderId() + { + return headerId; + } + + public String getCrumbValue() + { + return crumbValue; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index fa62789e..79e603ae 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,187 +1,242 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import hudson.AbortException; -import hudson.FilePath; -import hudson.EnvVars; -import hudson.Launcher; -import hudson.Extension; -import hudson.util.CopyOnWriteList; -import hudson.util.ListBoxModel; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; -import hudson.model.Result; -import hudson.model.AbstractProject; -import hudson.tasks.Builder; -import hudson.tasks.BuildStepDescriptor; -import net.sf.json.JSONObject; -import net.sf.json.JSONArray; -import net.sf.json.JSONSerializer; -//import net.sf.json. -//import net.sf.json. - -import net.sf.json.util.JSONUtils; - -import org.jenkinsci.plugins.tokenmacro.TokenMacro; -import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; +import static org.apache.commons.io.IOUtils.closeQuietly; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; import java.io.BufferedReader; +import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; import java.net.URLEncoder; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNullableByDefault; -import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildInfoExporterAction; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; + +import hudson.AbortException; +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.model.Item; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import hudson.util.CopyOnWriteList; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import jenkins.tasks.SimpleBuildStep; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; +import net.sf.json.util.JSONUtils; /** - * + * * @author Maurice W. - * + * */ -public class RemoteBuildConfiguration extends Builder { +@ParametersAreNullableByDefault +public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep, Serializable { - private final String token; - private final String remoteJenkinsName; - private final String job; + private static final long serialVersionUID = -4059001060991775146L; - private final boolean shouldNotFailBuild; - private final int pollInterval; - private final int connectionRetryLimit = 5; - private final boolean preventRemoteBuildQueue; - private final boolean blockBuildUntilComplete; - private final boolean enhancedLogging; + private static final int DEFAULT_POLLINTERVALL = 10; + private static final String paramerizedBuildUrl = "/buildWithParameters"; + private static final String normalBuildUrl = "/build"; + private static final String buildTokenRootUrl = "/buildByToken"; + private static final int connectionRetryLimit = 5; - // "parameters" is the raw string entered by the user - private final String parameters; - // "parameterList" is the cleaned-up version of "parameters" (stripped out comments, character encoding, etc) - - private final List parameterList; - - private static String paramerizedBuildUrl = "/buildWithParameters"; - private static String normalBuildUrl = "/build"; - //private static String normalBuildUrl = "/buildWithParameters"; - private static String buildTokenRootUrl = "/buildByToken"; + /** + * We need to keep this for compatibility - old config deserialization! + * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. + */ + private transient List auth; + + private String remoteJenkinsName; + private String remoteJenkinsUrl; + private Auth2 auth2; + private boolean shouldNotFailBuild; + private boolean preventRemoteBuildQueue; + private int pollInterval; + private boolean blockBuildUntilComplete; + private String job; + private String token; + private String parameters; + private boolean enhancedLogging; + private boolean loadParamsFromFile; + private String parameterFile; - private final boolean overrideAuth; - private CopyOnWriteList auth = new CopyOnWriteList(); + @DataBoundConstructor + public RemoteBuildConfiguration() { + remoteJenkinsName = null; + remoteJenkinsUrl = null; + auth = null; + auth2 = new NullAuth(); + shouldNotFailBuild = false; + preventRemoteBuildQueue = false; + pollInterval = DEFAULT_POLLINTERVALL; + blockBuildUntilComplete = false; + job = null; + token = ""; + parameters = ""; + enhancedLogging = false; + loadParamsFromFile = false; + parameterFile = ""; + } - private final boolean loadParamsFromFile; - private String parameterFile = ""; + @DataBoundSetter + public void setRemoteJenkinsName(String remoteJenkinsName) + { + this.remoteJenkinsName = trimToNull(remoteJenkinsName); + } - private String queryString = ""; + @DataBoundSetter + public void setRemoteJenkinsUrl(String remoteJenkinsUrl) + { + this.remoteJenkinsUrl = trimToNull(remoteJenkinsUrl); + } - @DataBoundConstructor - public RemoteBuildConfiguration(String remoteJenkinsName, boolean shouldNotFailBuild, String job, String token, - String parameters, boolean enhancedLogging, JSONObject overrideAuth, JSONObject loadParamsFromFile, boolean preventRemoteBuildQueue, - boolean blockBuildUntilComplete, int pollInterval) throws MalformedURLException { + @DataBoundSetter + public void setAuth2(Auth2 auth) { + this.auth2 = auth; + // disable old auth + this.auth = null; + } - this.token = token.trim(); - this.remoteJenkinsName = remoteJenkinsName; - this.job = job.trim(); + @DataBoundSetter + public void setShouldNotFailBuild(boolean shouldNotFailBuild) { this.shouldNotFailBuild = shouldNotFailBuild; - this.preventRemoteBuildQueue = preventRemoteBuildQueue; - this.blockBuildUntilComplete = blockBuildUntilComplete; - this.pollInterval = pollInterval; - this.enhancedLogging = enhancedLogging; + } - if (overrideAuth != null && overrideAuth.has("auth")) { - this.overrideAuth = true; - this.auth.replaceBy(new Auth(overrideAuth.getJSONObject("auth"))); - } else { - this.overrideAuth = false; - this.auth.replaceBy(new Auth(new JSONObject())); - } + @DataBoundSetter + public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { + this.preventRemoteBuildQueue = preventRemoteBuildQueue; + } - if (loadParamsFromFile != null && loadParamsFromFile.has("parameterFile")) { - this.loadParamsFromFile = true; - this.parameterFile = loadParamsFromFile.getString("parameterFile"); - this.parameters = ""; - //manually add a leading-slash if we don't have one - if( this.parameterFile.charAt(0) != '/' ){ - this.parameterFile = "/" + this.parameterFile; - } - } else { - this.loadParamsFromFile = false; - this.parameters = parameters; - } + @DataBoundSetter + public void setPollInterval(int pollInterval) { + if(pollInterval <= 0) this.pollInterval = DEFAULT_POLLINTERVALL; + else this.pollInterval = pollInterval; + } - // TODO: clean this up a bit - // split the parameter-string into an array based on the new-line character - String[] params = parameters.split("\n"); + @DataBoundSetter + public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { + this.blockBuildUntilComplete = blockBuildUntilComplete; + } - // convert the String array into a List of Strings, and remove any empty entries - this.parameterList = new ArrayList(Arrays.asList(params)); + @DataBoundSetter + public void setJob(String job) { + this.job = trimToNull(job); + } + @DataBoundSetter + public void setToken(String token) { + if (token == null) this.token = ""; + else this.token = token.trim(); } - public RemoteBuildConfiguration(String remoteJenkinsName, boolean shouldNotFailBuild, - boolean preventRemoteBuildQueue, boolean blockBuildUntilComplete, int pollInterval, String job, - String token, String parameters, boolean enhancedLogging) throws MalformedURLException { + @DataBoundSetter + public void setParameters(String parameters) { + if (parameters == null) this.parameters = ""; + else this.parameters = parameters; + } - this.token = token.trim(); - this.remoteJenkinsName = remoteJenkinsName; - this.parameters = parameters; + @DataBoundSetter + public void setEnhancedLogging(boolean enhancedLogging) { this.enhancedLogging = enhancedLogging; - this.job = job.trim(); - this.shouldNotFailBuild = shouldNotFailBuild; - this.preventRemoteBuildQueue = preventRemoteBuildQueue; - this.blockBuildUntilComplete = blockBuildUntilComplete; - this.pollInterval = pollInterval; - this.overrideAuth = false; - this.auth.replaceBy(new Auth(null)); + } - this.loadParamsFromFile = false; + @DataBoundSetter + public void setLoadParamsFromFile(boolean loadParamsFromFile) { + this.loadParamsFromFile = loadParamsFromFile; + } - // split the parameter-string into an array based on the new-line character - String[] params = parameters.split("\n"); + @DataBoundSetter + public void setParameterFile(String parameterFile) { + if (loadParamsFromFile && (parameterFile == null || parameterFile.isEmpty())) + throw new IllegalArgumentException("Parameter file path is empty"); - // convert the String array into a List of Strings, and remove any empty entries - this.parameterList = new ArrayList(Arrays.asList(params)); + if (parameterFile == null) this.parameterFile = ""; + else this.parameterFile = parameterFile; + } + public List getParameterList(BuildContext context) { + if (parameters != null && !parameters.isEmpty()){ + String[] params = parameters.split("\n"); + return new ArrayList(Arrays.asList(params)); + } else if (loadParamsFromFile){ + return loadExternalParameterFile(context); + } else { + return new ArrayList(); + } } /** * Reads a file from the jobs workspace, and loads the list of parameters from with in it. It will also call * ```getCleanedParameters``` before returning. - * + * * @param build * @return List of build parameters */ - private List loadExternalParameterFile(AbstractBuild build) { + private List loadExternalParameterFile(BuildContext context) { - FilePath workspace = build.getWorkspace(); BufferedReader br = null; - List ParameterList = new ArrayList(); + List parameterList = new ArrayList(); try { - String filePath = workspace + this.getParameterFile(); + String filePath = String.format("%s/%s", context.workspace, parameterFile); String sCurrentLine; - String fileContent = ""; - br = new BufferedReader(new FileReader(filePath)); + context.logger.println(String.format("Loading parameters from file %s", filePath)); + + br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8")); while ((sCurrentLine = br.readLine()) != null) { - // fileContent += sCurrentLine; - ParameterList.add(sCurrentLine); + parameterList.add(sCurrentLine); } - - // ParameterList = new ArrayList(Arrays.asList(fileContent)); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); } finally { try { if (br != null) { @@ -191,8 +246,7 @@ private List loadExternalParameterFile(AbstractBuild build) { ex.printStackTrace(); } } - // FilePath. - return getCleanedParameters(ParameterList); + return getCleanedParameters(parameterList); } /** @@ -203,20 +257,10 @@ private void removeEmptyElements(Collection collection) { collection.removeAll(Arrays.asList(null, " ")); } - /** - * Convenience method - * - * @return List of build parameters - */ - private List getCleanedParameters() { - - return getCleanedParameters(this.getParameterList()); - } - /** * Same as "getParameterList", but removes comments and empty strings Notice that no type of character encoding is * happening at this step. All encoding happens in the "buildUrlQueryString" method. - * + * * @param List * parameters * @return List of build parameters @@ -228,46 +272,6 @@ private List getCleanedParameters(List parameters) { return params; } - /** - * Similar to "replaceToken", but acts on a list in place of just a single string - * - * @param build - * @param listener - * @param params - * List of params to be tokenized/replaced - * @return List of resolved variables/tokens - */ - private List replaceTokens(AbstractBuild build, BuildListener listener, List params) { - List tokenizedParams = new ArrayList(); - - for (int i = 0; i < params.size(); i++) { - tokenizedParams.add(replaceToken(build, listener, params.get(i))); - // params.set(i, replaceToken(build, listener, params.get(i))); - } - - return tokenizedParams; - } - - /** - * Resolves any environment variables in the string - * - * @param build - * @param listener - * @param input - * String to be tokenized/replaced - * @return String with resolved Environment variables - */ - private String replaceToken(AbstractBuild build, BuildListener listener, String input) { - try { - return TokenMacro.expandAll(build, listener, input); - } catch (Exception e) { - listener.getLogger().println( - String.format("Failed to resolve parameters in string %s due to following error:\n%s", input, - e.getMessage())); - } - return input; - } - /** * Strip out any comments (lines that start with a #) from the collection that is passed in. */ @@ -283,12 +287,11 @@ private void removeCommentsFromParameters(Collection collection) { } /** - * Return the Collection in an encoded query-string - * - * @return query-parameter-formated URL-encoded string - * @throws InterruptedException - * @throws IOException - * @throws MacroEvaluationException + * Return the Collection in an encoded query-string. + * + * @param parameters + * the parameters needed to trigger the remote job. + * @return query-parameter-formated URL-encoded string. */ private String buildUrlQueryString(Collection parameters) { @@ -322,590 +325,662 @@ private String buildUrlQueryString(Collection parameters) { return StringUtils.join(encodedParameters, "&"); } + /** + * Tries to identify the effective Remote Host configuration based on the different parameters + * like remoteJenkinsName and the globally configured remote host, remoteJenkinsURL which overrides + * the address locally or job which can be a full job URL. + * + * @param context + * the context of this Builder/BuildStep. + * @return {@link RemoteJenkinsServer} + * a RemoteJenkinsServer object, never null. + * @throws AbortException + * if no server found and remoteJenkinsUrl empty. + * @throws MalformedURLException + * if remoteJenkinsName no valid URL or job an URL but nor valid. + */ + public @Nonnull RemoteJenkinsServer findEffectiveRemoteHost(BuildContext context) throws IOException { + RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); + RemoteJenkinsServer server = globallyConfiguredServer; + String expandedJob = getJobExpanded(context); + boolean isJobEmpty = isEmpty(trimToNull(expandedJob)); + boolean isJobUrl = FormValidationUtils.isURL(expandedJob); + boolean isRemoteUrlEmpty = isEmpty(trimToNull(this.remoteJenkinsUrl)); + boolean isRemoteUrlSet = FormValidationUtils.isURL(this.remoteJenkinsUrl); + boolean isRemoteNameEmpty = isEmpty(trimToNull(this.remoteJenkinsName)); + + if(isJobEmpty) throw new AbortException("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified."); + if(!isRemoteUrlEmpty && !isRemoteUrlSet) throw new AbortException(String.format( + "The 'Override remote host URL' parameter value (remoteJenkinsUrl: '%s') is no valid URL", this.remoteJenkinsUrl)); + + if(isJobUrl) { + // Full job URL configured - get remote Jenkins root URL from there + if(server == null) server = new RemoteJenkinsServer(); + server.setAddress(getRootUrlFromJobUrl(expandedJob)); + + } else if(isRemoteUrlSet) { + // Remote Jenkins root URL overridden locally in Job/Pipeline + if(server == null) server = new RemoteJenkinsServer(); + server.setAddress(this.remoteJenkinsUrl); + + } + + if (server == null) { + if(!isJobUrl) { + if(!isRemoteUrlSet && isRemoteNameEmpty) + throw new AbortException("Configuration of the remote Jenkins host is missing."); + if(!isRemoteUrlSet && !isRemoteNameEmpty && globallyConfiguredServer == null) + throw new AbortException(String.format( + "Could get remote host with ID '%s' configured in Jenkins global configuration. Please check your global configuration.", + this.remoteJenkinsName)); + } + //Generic error message + throw new AbortException(String.format( + "Could not identify remote host - neither via 'Remote Job Name or URL' (job:'%s'), globally configured" + + " remote host (remoteJenkinsName:'%s') nor 'Override remote host URL' (remoteJenkinsUrl:'%s').", + expandedJob, this.remoteJenkinsName, this.remoteJenkinsUrl)); + } + return server; + } + /** * Lookup up a Remote Jenkins Server based on display name - * + * * @param displayName * Name of the configuration you are looking for - * @return A RemoteSitez object + * @return A RemoteJenkinsServer object */ - public RemoteJenkinsServer findRemoteHost(String displayName) { - RemoteJenkinsServer match = null; - + private RemoteJenkinsServer findRemoteHost(String displayName) { + if(isEmpty(displayName)) return null; + RemoteJenkinsServer server = null; for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { // if we find a match, then stop looping if (displayName.equals(host.getDisplayName())) { - match = host; + server = host; break; } } + return server; + } + + protected static String removeTrailingSlashes(String string) + { + if(isEmpty(string)) return string; + string = string.trim(); + while(string.endsWith("/")) string = string.substring(0, string.length() - 1); + return string; + } + + protected static String removeQueryParameters(String string) + { + if(isEmpty(string)) return string; + string = string.trim(); + int idx = string.indexOf("?"); + if(idx > 0) string = string.substring(0, idx); + return string; + } - return match; + protected static String removeHashParameters(String string) + { + if(isEmpty(string)) return string; + string = string.trim(); + int idx = string.indexOf("#"); + if(idx > 0) string = string.substring(0, idx); + return string; + } + + private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException + { + if(isEmpty(jobUrl)) return null; + if(FormValidationUtils.isURL(jobUrl)) { + int index = jobUrl.indexOf("/job/"); + if(index < 0) throw new MalformedURLException("Expected '/job/' element in job URL but was: " + jobUrl); + return jobUrl.substring(0, index); + } else { + return null; + } } /** * Helper function to allow values to be added to the query string from any method. - * + * * @param item */ - private void addToQueryString(String item) { - String currentQueryString = this.getQueryString(); - String newQueryString = ""; - - if (currentQueryString == null || currentQueryString.equals("")) { - newQueryString = item; + private String addToQueryString(String queryString, String item) { + if (queryString == null || queryString.equals("")) { + return item; } else { - newQueryString = currentQueryString + "&" + item; + return queryString + "&" + item; } - this.setQueryString(newQueryString); } /** * Build the proper URL to trigger the remote build - * + * * All passed in string have already had their tokens replaced with real values. All 'params' also have the proper * character encoding - * - * @param job + * + * @param jobNameOrUrl * Name of the remote job * @param securityToken * Security token used to trigger remote job * @param params * Parameters for the remote job * @return fully formed, fully qualified remote trigger URL + * @throws MalformedURLException */ - private String buildTriggerUrl(String job, String securityToken, Collection params, boolean isRemoteJobParameterized) { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - String triggerUrlString = remoteServer.getAddress().toString(); + private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, boolean isRemoteJobParameterized, + BuildContext context) throws IOException { + String triggerUrlString; + String query = ""; + RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); - // start building the proper URL based on known capabiltiies of the remote server if (remoteServer.getHasBuildTokenRootSupport()) { + // start building the proper URL based on known capabiltiies of the remote server + triggerUrlString = remoteServer.getAddress().toString(); triggerUrlString += buildTokenRootUrl; triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); - - this.addToQueryString("job=" + this.encodeValue(job)); + query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? } else { - triggerUrlString += "/job/"; - triggerUrlString += this.encodeValue(job); + triggerUrlString = generateJobUrl(remoteServer, jobNameOrUrl); triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); } // don't try to include a security token in the URL if none is provided if (!securityToken.equals("")) { - this.addToQueryString("token=" + encodeValue(securityToken)); + query = addToQueryString(query, "token=" + encodeValue(securityToken)); } // turn our Collection into a query string String buildParams = buildUrlQueryString(params); if (!buildParams.isEmpty()) { - this.addToQueryString(buildParams); + query = addToQueryString(query, buildParams); } // by adding "delay=0", this will (theoretically) force this job to the top of the remote queue - this.addToQueryString("delay=0"); + query = addToQueryString(query, "delay=0"); - triggerUrlString += "?" + this.getQueryString(); + triggerUrlString += "?" + query; return triggerUrlString; } /** - * Build the proper URL for GET calls - * + * Build the proper URL for GET calls. + * * All passed in string have already had their tokens replaced with real values. - * - * @param job - * Name of the remote job + * + * @param jobNameOrUrl + * name or URL of the remote job. * @param securityToken - * Security token used to trigger remote job - * @return fully formed, fully qualified remote trigger URL + * security token used to trigger remote job. + * @param context + * the context of this Builder/BuildStep. + * @return String + * fully formed, fully qualified remote trigger URL. + * @throws IOException + * if there is an error identifying the remote host. */ - private String buildGetUrl(String job, String securityToken) { - - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - String urlString = remoteServer.getAddress().toString(); - - urlString += "/job/"; - urlString += this.encodeValue(job); - + private String buildGetUrl(String jobNameOrUrl, String securityToken, BuildContext context) throws IOException { + RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + String urlString = generateJobUrl(remoteServer, jobNameOrUrl); // don't try to include a security token in the URL if none is provided - if (!securityToken.equals("")) { - this.addToQueryString("token=" + encodeValue(securityToken)); + if (!isEmpty(securityToken)) { + urlString += "?token=" + encodeValue(securityToken); } return urlString; } /** - * Convenience function to mark the build as failed. It's intended to only be called from this.perform(); - * + * Convenience function to mark the build as failed. It's intended to only be called from this.perform(). + * * @param e - * Exception that caused the build to fail - * @param listener - * Build Listener + * exception that caused the build to fail. + * @param logger + * build listener. * @throws IOException + * if the build fails and shouldNotFailBuild is not set. */ - private void failBuild(Exception e, BuildListener listener) throws IOException { - System.out.print(e.getStackTrace()); - if (this.getShouldNotFailBuild()) { - listener.error("Remote build failed for the following reason, but the build will continue:"); - listener.error(e.getMessage()); - } else { - listener.error("Remote build failed for the following reason:"); - throw new AbortException(e.getMessage()); + protected void failBuild(Exception e, PrintStream logger) throws IOException { + StringBuilder msg = new StringBuilder(); + if(e instanceof InterruptedException) { + Thread current = Thread.currentThread(); + msg.append(String.format("[Thread: %s/%s]: ", current.getId(), current.getName())); + } + msg.append(String.format("Remote build failed with '%s' for the following reason: '%s'.%s", + e.getClass().getSimpleName(), + e.getMessage(), + this.getShouldNotFailBuild() ? " But the build will continue." : "")); + if(enhancedLogging) { + msg.append("\n").append(ExceptionUtils.getFullStackTrace(e)); + } + if(logger != null) logger.println("ERROR: " + msg.toString()); + if (!this.getShouldNotFailBuild()) { + throw new AbortException(e.getClass().getSimpleName() + ": " + e.getMessage()); } } @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, - IOException, IllegalArgumentException { - - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - - // Stores the status of the remote build - String buildStatusStr = "UNKNOWN"; + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, + IOException, IllegalArgumentException + { + FilePath workspace = build.getWorkspace(); + if (workspace == null) throw new IllegalArgumentException("The workspace can not be null"); + perform(build, workspace, launcher, listener); + return true; + } - if (remoteServer == null) { - this.failBuild(new Exception("No remote host is defined for this job."), listener); - return true; - } - String remoteServerURL = remoteServer.getAddress().toString(); - List cleanedParams = null; + /** + * Triggers the remote job and, waits until completion if blockBuildUntilComplete is set. + * + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if there is an error retrieving the remote build data, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + */ + @Override + public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) + throws InterruptedException, IOException + { + BuildContext context = new BuildContext(build, workspace, listener); + Handle handle = performTriggerAndGetQueueId(context); + performWaitForBuild(context, handle); + } - if (this.getLoadParamsFromFile()) { - cleanedParams = loadExternalParameterFile(build); - } else { - // tokenize all variables and encode all variables, then build the fully-qualified trigger URL - cleanedParams = getCleanedParameters(); - cleanedParams = replaceTokens(build, listener, cleanedParams); + /** + * Triggers the remote job, identifies the queue ID and, returns a Handle to this remote execution. + * + * @param context + * the context of this Builder/BuildStep. + * @return Handle + * to further tracking of the remote build status. + * @throws IOException + * if there is an error triggering the remote job. + */ + public Handle performTriggerAndGetQueueId(BuildContext context) + throws IOException + { + List cleanedParams = getCleanedParameters(getParameterList(context)); + String jobNameOrUrl = this.getJob(); + String securityToken = this.getToken(); + try { + cleanedParams = TokenMacroUtils.applyTokenMacroReplacements(cleanedParams, context); + jobNameOrUrl = TokenMacroUtils.applyTokenMacroReplacements(jobNameOrUrl, context); + securityToken = TokenMacroUtils.applyTokenMacroReplacements(securityToken, context); + } catch(IOException e) { + this.failBuild(e, context.logger); } - String jobName = replaceToken(build, listener, this.getJob()); + logConfiguration(context, cleanedParams); - String securityToken = replaceToken(build, listener, this.getToken()); + final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); + boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); - boolean isRemoteParameterized = isRemoteJobParameterized(jobName, build, listener); - String triggerUrlString = this.buildTriggerUrl(jobName, securityToken, cleanedParams, isRemoteParameterized); + final String triggerUrlString = this.buildTriggerUrl(jobNameOrUrl, securityToken, cleanedParams, isRemoteParameterized, context); + final String jobUrlString = this.buildGetUrl(jobNameOrUrl, securityToken, context); // Trigger remote job - // print out some debugging information to the console + context.logger.println(String.format("Triggering %s remote job '%s'", + (isRemoteParameterized ? "parameterized" : "non-parameterized"), jobUrlString )); - //listener.getLogger().println("URL: " + triggerUrlString); - listener.getLogger().println("Triggering this remote job: " + jobName); + logAuthInformation(context); // get the ID of the Next Job to run. if (this.getPreventRemoteBuildQueue()) { - listener.getLogger().println("Checking that the remote job " + jobName + " is not currently building."); - String preCheckUrlString = this.buildGetUrl(jobName, securityToken); + context.logger.println(" Checking if the remote job " + jobNameOrUrl + " is currently running."); + String preCheckUrlString = jobUrlString; preCheckUrlString += "/lastBuild"; preCheckUrlString += "/api/json/"; - JSONObject preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", build, listener); - + JSONObject preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", context); + if ( preCheckResponse != null ) { // check the latest build on the remote server to see if it's running - if so wait until it has stopped. // if building is true then the build is running // if result is null the build hasn't finished - but might not have started running. while (preCheckResponse.getBoolean("building") == true || preCheckResponse.getString("result") == null) { - listener.getLogger().println("Remote build is currently running - waiting for it to finish."); - preCheckResponse = sendHTTPCall(preCheckUrlString, "POST", build, listener); - listener.getLogger().println("Waiting for " + this.pollInterval + " seconds until next retry."); - - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) + context.logger.println(String.format( + " Remote build is currently running - waiting for it to finish. Next try in %s seconds.", + this.pollInterval)); try { Thread.sleep(this.pollInterval * 1000); } catch (InterruptedException e) { - this.failBuild(e, listener); + this.failBuild(e, context.logger); } + preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", context); } - listener.getLogger().println("Remote job remote job " + jobName + " is not currenlty building."); + context.logger.println(" Remote job " + jobNameOrUrl + " is currently not building."); } else { - this.failBuild(new Exception("Got a blank response from Remote Jenkins Server, cannot continue."), listener); + this.failBuild(new Exception("Got a blank response from Remote Jenkins Server, cannot continue."), context.logger); } - } else { - listener.getLogger().println("Not checking if the remote job " + jobName + " is building."); - } - - String queryUrlString = this.buildGetUrl(jobName, securityToken); - queryUrlString += "/api/json/"; - - //listener.getLogger().println("Getting ID of next job to build. URL: " + queryUrlString); - JSONObject queryResponseObject = sendHTTPCall(queryUrlString, "GET", build, listener); - if (queryResponseObject == null ) { - //This should not happen as this page should return a JSON object - this.failBuild(new Exception("Got a blank response from Remote Jenkins Server [" + remoteServerURL + "], cannot continue."), listener); } - - int nextBuildNumber = queryResponseObject.getInt("nextBuildNumber"); - if (this.getOverrideAuth()) { - listener.getLogger().println( - "Using job-level defined credentails in place of those from remote Jenkins config [" - + this.getRemoteJenkinsName() + "]"); - } + context.logger.println("Triggering remote job now."); - listener.getLogger().println("Triggering remote job now."); - sendHTTPCall(triggerUrlString, "POST", build, listener); - // Validate the build number via parameters - foundIt: for (int tries = 3; tries > 0; tries--) { - for (int buildNumber : new SearchPattern(nextBuildNumber, 2)) { - listener.getLogger().println("Checking parameters of #" + buildNumber); - String validateUrlString = this.buildGetUrl(jobName, securityToken) + "/" + buildNumber + "/api/json/"; - JSONObject validateResponse = sendHTTPCall(validateUrlString, "GET", build, listener); - if (validateResponse == null) { - listener.getLogger().println("Query failed."); - continue; - } - JSONArray actions = validateResponse.getJSONArray("actions"); - for (int i = 0; i < actions.size(); i++) { - JSONObject action = actions.getJSONObject(i); - if (!action.has("parameters")) continue; - JSONArray parameters = action.getJSONArray("parameters"); - // Check if the parameters match - if (compareParameters(listener, parameters, cleanedParams)) { - // We now have a very high degree of confidence that this is the correct build. - // It is still possible that this is a false positive if there are no parameters, - // or multiple jobs use the same parameters. - nextBuildNumber = buildNumber; - break foundIt; - } - // This is the wrong build - break; - } + ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); + QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); + Handle handle = new Handle(this, queueItem.getId(), context.currentItem); + handle.setJobMetadata(remoteJobMetadata); + return handle; + } - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } - } - } - listener.getLogger().println("This job is build #[" + Integer.toString(nextBuildNumber) + "] on the remote server."); - BuildInfoExporterAction.addBuildInfoExporterAction(build, jobName, nextBuildNumber, Result.NOT_BUILT); - - //Have to form the string ourselves, as we might not get a response from non-parameterized builds - String jobURL = remoteServerURL + "/job/" + this.encodeValue(jobName) + "/"; - - // This is only for Debug - // This output whether there is another job running on the remote host that this job had conflicted with. - // The first condition is what is expected, The second is what would happen if two jobs launched jobs at the - // same time (and two remote builds were triggered). - // The third is what would happen if this job was triggers and the remote queue was already full (as the 'next - // build bumber' would still be the same after this job has triggered the remote job) - // int newNextBuildNumber = responseObject.getInt( "nextBuildNumber" ); // This should be nextBuildNumber + 1 OR - // there has been another job scheduled. - // if (newNextBuildNumber == (nextBuildNumber + 1)) { - // listener.getLogger().println("DEBUG: No other jobs triggered" ); - // } else if( newNextBuildNumber > (nextBuildNumber + 1) ) { - // listener.getLogger().println("DEBUG: WARNING Other jobs triggered," + newNextBuildNumber + ", " + - // nextBuildNumber ); - // } else { - // listener.getLogger().println("DEBUG: WARNING Did not get the correct build number for the triggered job, previous nextBuildNumber:" - // + newNextBuildNumber + ", newNextBuildNumber" + nextBuildNumber ); - // } + /** + * Checks the remote build status and, waits for completion if blockBuildUntilComplete is set. + * + * @param context + * the context of this Builder/BuildStep. + * @param handle + * the handle to the remote execution. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if there is an error retrieving the remote build data, or, + * if there is an error retrieving the remote build status, or, + * if there is an error retrieving the console output of the remote build, or, + * if the remote build does not succeed. + */ + public void performWaitForBuild(BuildContext context, Handle handle) + throws InterruptedException, IOException + { + String jobName = handle.getJobName(); + BuildData buildData = getBuildData(handle.getQueueId(), context); + handle.setBuildData(buildData); - // If we are told to block until remoteBuildComplete: - if (this.getBlockBuildUntilComplete()) { - listener.getLogger().println("Blocking local job until remote job completes"); - // Form the URL for the triggered job - String jobLocation = jobURL + nextBuildNumber + "/api/json"; + context.logger.println(" Remote build URL: " + buildData.getURL()); + context.logger.println(" Remote build number: " + buildData.getBuildNumber()); - buildStatusStr = getBuildStatus(jobLocation, build, listener); + int jobNumber = buildData.getBuildNumber(); + URL jobURL = buildData.getURL(); - while (buildStatusStr.equals("not started")) { - listener.getLogger().println("Waiting for remote build to start."); - listener.getLogger().println("Waiting for " + this.pollInterval + " seconds until next poll."); - buildStatusStr = getBuildStatus(jobLocation, build, listener); - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } - } + if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, BuildStatus.NOT_BUILT); - listener.getLogger().println("Remote build started!"); - while (buildStatusStr.equals("running")) { - listener.getLogger().println("Waiting for remote build to finish."); - listener.getLogger().println("Waiting for " + this.pollInterval + " seconds until next poll."); - buildStatusStr = getBuildStatus(jobLocation, build, listener); - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } - } - listener.getLogger().println("Remote build finished with status " + buildStatusStr + "."); - BuildInfoExporterAction.addBuildInfoExporterAction(build, jobName, nextBuildNumber, Result.fromString(buildStatusStr)); - - if (this.getEnhancedLogging()) { - String buildUrl = getBuildUrl(jobLocation, build, listener); - String consoleOutput = getConsoleOutput(buildUrl, "GET", build, listener); - - listener.getLogger().println(); - listener.getLogger().println("Console output of remote job:"); - listener.getLogger().println("--------------------------------------------------------------------------------"); - listener.getLogger().println(consoleOutput); - listener.getLogger().println("--------------------------------------------------------------------------------"); - } + // Stores the status of the remote build + BuildStatus buildStatus = BuildStatus.UNKNOWN; + handle.setBuildStatus(buildStatus); - // If build did not finish with 'success' then fail build step. - if (!buildStatusStr.equals("SUCCESS")) { - // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so will decide how to - // handle the failure. - this.failBuild(new Exception("The remote job did not succeed."), listener); - } + // If we are told to block until remoteBuildComplete: + if (this.getBlockBuildUntilComplete()) { + context.logger.println("Blocking local job until remote job completes."); + // Form the URL for the triggered job + String jobLocation = jobURL + "api/json/"; + + buildStatus = getBuildStatus(jobLocation, context); + handle.setBuildStatus(buildStatus); + + if (buildStatus.equals(BuildStatus.NOT_STARTED)) + context.logger.println("Waiting for remote build to start ..."); + + while (buildStatus.equals(BuildStatus.NOT_STARTED)) { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + // Sleep for 'pollInterval' seconds. + // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) + try { + // Could do with a better way of sleeping... + Thread.sleep(this.pollInterval * 1000); + } catch (InterruptedException e) { + this.failBuild(e, context.logger); + } + buildStatus = getBuildStatus(jobLocation, context); + handle.setBuildStatus(buildStatus); + } + + context.logger.println("Remote build started!"); + + if (buildStatus.equals(BuildStatus.RUNNING)) + context.logger.println("Waiting for remote build to finish ..."); + + while (buildStatus.equals(BuildStatus.RUNNING)) { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + // Sleep for 'pollInterval' seconds. + // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) + try { + // Could do with a better way of sleeping... + Thread.sleep(this.pollInterval * 1000); + } catch (InterruptedException e) { + this.failBuild(e, context.logger); + } + buildStatus = getBuildStatus(jobLocation, context); + handle.setBuildStatus(buildStatus); + } + context.logger.println("Remote build finished with status " + buildStatus + "."); + if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildStatus); + + if (this.getEnhancedLogging()) { + String consoleOutput = getConsoleOutput(jobURL, "GET", context); + + context.logger.println(); + context.logger.println("Console output of remote job:"); + context.logger.println("--------------------------------------------------------------------------------"); + context.logger.println(consoleOutput); + context.logger.println("--------------------------------------------------------------------------------"); + } + + // If build did not finish with 'success' then fail build step. + if (!buildStatus.equals(BuildStatus.SUCCESS)) { + // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so will decide how to + // handle the failure. + this.failBuild(new Exception("The remote job did not succeed."), context.logger); + } } else { - listener.getLogger().println("Not blocking local job until remote job completes - fire and forget."); + context.logger.println("Not blocking local job until remote job completes - fire and forget."); } - - return true; } - private String findParameter(String parameter, List parameters) { - for (String search : parameters) { - if (search.startsWith(parameter + "=")) { - return search.substring(parameter.length() + 1); - } - } - return null; - } + /** + * Sends a HTTP request to the API of the remote server requesting a queue item. + * + * @param queueId + * the id of the remote job on the queue. + * @param context + * the context of this Builder/BuildStep. + * @return {@link QueueItemData} + * the queue item data. + * @throws IOException + * if there is an error identifying the remote host, or + * if there is an error setting the authorization header, or + * if the request fails due to an unknown host, unauthorized credentials, or another reason, or + * if there is an invalid queue response. + */ + @Nonnull + private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) + throws IOException + { + URL remoteServerURL = findEffectiveRemoteHost(context).getAddress(); + String queueQuery = String.format("%s/queue/item/%s/api/json/", remoteServerURL, queueId); + JSONObject queueResponse = sendHTTPCall(queueQuery, "GET", context); - private boolean compareParameters(BuildListener listener, JSONArray parameters, List expectedParams) { - for (int j = 0; j < parameters.size(); j++) { - JSONObject parameter = parameters.getJSONObject(j); - String name = parameter.getString("name"); - String expected = findParameter(name, expectedParams); + if (queueResponse.isNullObject()) + throw new AbortException("Unexpected queue item response."); - if (expected == null) { - // If we didn't specify all of the parameters, this will happen, so we can not infer that this it he wrong build - listener.getLogger().println("Unable to find expected value for " + name); - continue; - } + QueueItemData queueItem = new QueueItemData(queueResponse); - String value = parameter.getString("value"); - // If we got the expected value, skip to the next parameter - if (expected.equals(value)) continue; + if (queueItem.isBlocked()) + context.logger.println("The remote job is blocked. Reason: " + queueItem.getWhy() + "."); - // We didn't get the expected value - listener.getLogger().println("Param " + name + " doesn't match!"); - return false; - } - // All found parameters matched. This if there are no uniquely identifying parameters, this could still be a false positive. - return true; - } + if (queueItem.isPending()) + context.logger.println("The remote job is pending. Reason: " + queueItem.getWhy() + "."); - public String getBuildStatus(String buildUrlString, AbstractBuild build, BuildListener listener) throws IOException { - String buildStatus = "UNKNOWN"; + if (queueItem.isBuildable()) + context.logger.println("The remote job is buildable. Reason: " + queueItem.getWhy() + "."); - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); + if (queueItem.isCancelled()) + throw new AbortException("The remote job was canceled"); - if (remoteServer == null) { - this.failBuild(new Exception("No remote host is defined for this job."), listener); - return null; - } + return queueItem; + } - // print out some debugging information to the console - //listener.getLogger().println("Checking Status of this job: " + buildUrlString); - if (this.getOverrideAuth()) { - listener.getLogger().println( - "Using job-level defined credentails in place of those from remote Jenkins config [" - + this.getRemoteJenkinsName() + "]"); + /** + * Requests the queue item data till the job is executable and the build data can be retrieved. + * + * @param queueId + * the id of the remote job on the queue. + * @param context + * the context of this Builder/BuildStep. + * @return {@link BuildData} + * the build data containing the build number and build URL. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if there is an error identifying the remote host, or + * if there is an error setting the authorization header, or + * if the request fails due to an unknown host, unauthorized credentials, or another reason. + * @throws AbortException + * if the queue item response is unexpected. + * @throws MalformedURLException + * if there is an error creating the build URL. + */ + @Nonnull + public BuildData getBuildData(@Nonnull String queueId, @Nonnull BuildContext context) + throws IOException, InterruptedException, AbortException, MalformedURLException + { + context.logger.println(" Remote job queue number: " + queueId); + + QueueItemData queueItem = getQueueItemData(queueId, context); + BuildData buildData = queueItem.getBuildData(context); + + context.logger.println("Waiting for remote build to be executed..."); + + while (buildData == null) + { + context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); + Thread.sleep(this.pollInterval * 1000); + queueItem = getQueueItemData(queueId, context); + buildData = queueItem.getBuildData(context); } + return buildData; + } + + public BuildStatus getBuildStatus(String buildUrlString, BuildContext context) throws IOException { + BuildStatus buildStatus = BuildStatus.UNKNOWN; - JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", build, listener); + //logAuthInformation(context); + JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); // get the next build from the location - if (responseObject == null || responseObject.getString("result") == null && responseObject.getBoolean("building") == false) { + try { + if (responseObject == null || responseObject.getString("result") == null && responseObject.getBoolean("building") == false) { // build not started - buildStatus = "not started"; - } else if (responseObject.getBoolean("building")) { + buildStatus = BuildStatus.NOT_STARTED; + } else if (responseObject.getBoolean("building")) { // build running - buildStatus = "running"; - } else if (responseObject.getString("result") != null) { + buildStatus = BuildStatus.RUNNING; + } else if (responseObject.getString("result") != null) { // build finished - buildStatus = responseObject.getString("result"); - } else { + buildStatus = BuildStatus.valueOf(responseObject.getString("result")); + } else { // Add additional else to check for unhandled conditions - listener.getLogger().println("WARNING: Unhandled condition!"); + context.logger.println("WARNING: Unhandled condition!"); + } + } catch (Exception ex) { + return buildStatus; } return buildStatus; } - public String getBuildUrl(String buildUrlString, AbstractBuild build, BuildListener listener) throws IOException { - String buildUrl = ""; - - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - - if (remoteServer == null) { - this.failBuild(new Exception("No remote host is defined for this job."), listener); - return null; - } - - // print out some debugging information to the console - //listener.getLogger().println("Checking Status of this job: " + buildUrlString); - if (this.getOverrideAuth()) { - listener.getLogger().println( - "Using job-level defined credentails in place of those from remote Jenkins config [" - + this.getRemoteJenkinsName() + "]"); - } - - JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", build, listener); - - // get the next build from the location - - if (responseObject != null && responseObject.getString("url") != null) { - buildUrl = responseObject.getString("url"); - } else { - // Add additional else to check for unhandled conditions - listener.getLogger().println("WARNING: URL not found in JSON Response!"); - return null; - } - - return buildUrl; - } - - public String getConsoleOutput(String urlString, String requestType, AbstractBuild build, BuildListener listener) + private String getConsoleOutput(URL url, String requestType, BuildContext context) throws IOException { - - return getConsoleOutput( urlString, requestType, build, listener, 1 ); + + return getConsoleOutput( url, requestType, context, 1 ); } /** * Orchestrates all calls to the remote server. * Also takes care of any credentials or failed-connection retries. - * - * @param urlString the URL that needs to be called - * @param requestType the type of request (GET, POST, etc) - * @param build the build that is being triggered - * @param listener build listener - * @return a valid JSON object, or null + * + * @param urlString + * the URL that needs to be called. + * @param requestType + * the type of request (GET, POST, etc). + * @param context + * the context of this Builder/BuildStep. + * @return JSONObject + * a valid JSON object, or null. * @throws IOException + * if there is an error identifying the remote host, or + * if there is an error setting the authorization header, or + * if the request fails due to an unknown host, unauthorized credentials, or another reason. */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener) + public JSONObject sendHTTPCall(String urlString, String requestType, BuildContext context) throws IOException { - - return sendHTTPCall( urlString, requestType, build, listener, 1 ); + + return sendHTTPCall( urlString, requestType, context, 1 ).getBody(); } - public String getConsoleOutput(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) + private String getConsoleOutput(URL url, String requestType, BuildContext context, int numberOfAttempts) throws IOException { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); + RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); int retryLimit = this.getConnectionRetryLimit(); - - if (remoteServer == null) { - this.failBuild(new Exception("No remote host is defined for this job."), listener); - return null; - } - - HttpURLConnection connection = null; String consoleOutput = null; - URL buildUrl = new URL(urlString+"consoleText"); - connection = (HttpURLConnection) buildUrl.openConnection(); - - // if there is a username + apiToken defined for this remote host, then use it - String usernameTokenConcat; - - if (this.getOverrideAuth()) { - usernameTokenConcat = this.getAuth()[0].getUsername() + ":" + this.getAuth()[0].getPassword(); - } else { - usernameTokenConcat = remoteServer.getAuth()[0].getUsername() + ":" - + remoteServer.getAuth()[0].getPassword(); - } - - if (!usernameTokenConcat.equals(":")) { - // token-macro replacment - try { - usernameTokenConcat = TokenMacro.expandAll(build, listener, usernameTokenConcat); - } catch (MacroEvaluationException e) { - this.failBuild(e, listener); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } + URL buildUrl = new URL(url, "consoleText"); - byte[] encodedAuthKey = Base64.encodeBase64(usernameTokenConcat.getBytes()); - connection.setRequestProperty("Authorization", "Basic " + new String(encodedAuthKey)); - } + HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, buildUrl); + int responseCode = 0; try { connection.setDoInput(true); connection.setRequestProperty("Accept", "application/json"); + addCrumbToConnection(getCrumb(remoteServer, context), connection); connection.setRequestMethod(requestType); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); connection.connect(); - - InputStream is; - try { - is = connection.getInputStream(); - } catch (FileNotFoundException e) { - // In case of a e.g. 404 status - is = connection.getErrorStream(); - } - - BufferedReader rd = new BufferedReader(new InputStreamReader(is)); - String line; - // String response = ""; - StringBuilder response = new StringBuilder(); - - while ((line = rd.readLine()) != null) { - response.append(line+"\n"); + responseCode = connection.getResponseCode(); + if(responseCode == 401) { + throw new UnauthorizedException(buildUrl); + } else if(responseCode == 403) { + throw new ForbiddenException(buildUrl); + } else { + consoleOutput = readInputStream(connection); } - rd.close(); - - - consoleOutput = response.toString(); + } catch (UnknownHostException e) { + this.failBuild(e, context.logger); + } catch (UnauthorizedException e) { + this.failBuild(e, context.logger); + } catch (ForbiddenException e) { + this.failBuild(e, context.logger); } catch (IOException e) { - + //If we have connectionRetryLimit set to > 0 then retry that many times. if( numberOfAttempts <= retryLimit) { - listener.getLogger().println("Connection to remote server failed, waiting for to retry - " + this.pollInterval + " seconds until next attempt."); + context.logger.println(String.format( + "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s", + (responseCode == 0 ? "" : "[" + responseCode + "]"), this.pollInterval, url)); e.printStackTrace(); - + // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { // Could do with a better way of sleeping... Thread.sleep(this.pollInterval * 1000); } catch (InterruptedException ex) { - this.failBuild(ex, listener); + this.failBuild(ex, context.logger); } - - listener.getLogger().println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); + context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); numberOfAttempts++; - consoleOutput = getConsoleOutput(urlString, requestType, build, listener, numberOfAttempts); + consoleOutput = getConsoleOutput(url, requestType, context, numberOfAttempts); } else if(numberOfAttempts > retryLimit){ //reached the maximum number of retries, time to fail - this.failBuild(new Exception("Max number of connection retries have been exeeded."), listener); + this.failBuild(new Exception("Max number of connection retries have been exeeded."), context.logger); } else{ //something failed with the connection and we retried the max amount of times... so throw an exception to mark the build as failed. - this.failBuild(e, listener); + this.failBuild(e, context.logger); } - + } finally { // always make sure we close the connection if (connection != null) { connection.disconnect(); } - // and always clear the query string and remove some "global" values - this.clearQueryString(); - // this.build = null; - // this.listener = null; - } return consoleOutput; } @@ -913,144 +988,266 @@ public String getConsoleOutput(String urlString, String requestType, AbstractBui /** * Same as sendHTTPCall, but keeps track of the number of failed connection attempts (aka: the number of times this * method has been called). - * In the case of a failed connection, the method calls it self recursively and increments numberOfAttempts - * + * In the case of a failed connection, the method calls it self recursively and increments the number of attempts. + * * @see sendHTTPCall - * @param numberOfAttempts number of time that the connection has been attempted - * @return + * @param urlString + * the URL that needs to be called. + * @param requestType + * the type of request (GET, POST, etc). + * @param context + * the context of this Builder/BuildStep. + * @param numberOfAttempts + * number of time that the connection has been attempted. + * @return {@link ConnectionResponse} + * the response to the HTTP request. * @throws IOException + * if there is an error identifying the remote host, or + * if there is an error setting the authorization header, or + * if the request fails due to an unknown host or unauthorized credentials, or + * if the request fails due to another reason and the number of attempts is exceeded. */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) + private ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, int numberOfAttempts) throws IOException { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); + RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); int retryLimit = this.getConnectionRetryLimit(); - - if (remoteServer == null) { - this.failBuild(new Exception("No remote host is defined for this job."), listener); - return null; - } - - HttpURLConnection connection = null; JSONObject responseObject = null; + Map> responseHeader = null; + int responseCode = 0; - URL buildUrl = new URL(urlString); - connection = (HttpURLConnection) buildUrl.openConnection(); - - // if there is a username + apiToken defined for this remote host, then use it - String usernameTokenConcat; - - if (this.getOverrideAuth()) { - usernameTokenConcat = this.getAuth()[0].getUsername() + ":" + this.getAuth()[0].getPassword(); - } else { - usernameTokenConcat = remoteServer.getAuth()[0].getUsername() + ":" - + remoteServer.getAuth()[0].getPassword(); - } - - if (!usernameTokenConcat.equals(":")) { - // token-macro replacment - try { - usernameTokenConcat = TokenMacro.expandAll(build, listener, usernameTokenConcat); - } catch (MacroEvaluationException e) { - this.failBuild(e, listener); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } - - byte[] encodedAuthKey = Base64.encodeBase64(usernameTokenConcat.getBytes()); - connection.setRequestProperty("Authorization", "Basic " + new String(encodedAuthKey)); - } + URL url = new URL(urlString); + HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, url); try { connection.setDoInput(true); connection.setRequestProperty("Accept", "application/json"); + addCrumbToConnection(getCrumb(remoteServer, context), connection); connection.setRequestMethod(requestType); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); connection.connect(); - - InputStream is; - try { - is = connection.getInputStream(); - } catch (FileNotFoundException e) { - // In case of a e.g. 404 status - is = connection.getErrorStream(); - } - - BufferedReader rd = new BufferedReader(new InputStreamReader(is)); - String line; - // String response = ""; - StringBuilder response = new StringBuilder(); - - while ((line = rd.readLine()) != null) { - response.append(line); - } - rd.close(); - - // JSONSerializer serializer = new JSONSerializer(); - // need to parse the data we get back into struct - //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); - - //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 - //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. - //But in newer versions of Jenkins, it just returns an empty response. - //So we need to compensate and check for both. - if ( JSONUtils.mayBeJSON(response.toString()) == false) { - listener.getLogger().println("Remote Jenkins server returned empty response or invalid JSON - but we can still proceed with the remote build."); - return null; + responseHeader = connection.getHeaderFields(); + responseCode = connection.getResponseCode(); + if(responseCode == 401) { + throw new UnauthorizedException(url); + } else if(responseCode == 403) { + throw new ForbiddenException(url); } else { - responseObject = (JSONObject) JSONSerializer.toJSON(response.toString()); + String response = trimToNull(readInputStream(connection)); + + // JSONSerializer serializer = new JSONSerializer(); + // need to parse the data we get back into struct + //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); + + //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 + //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. + //But in newer versions of Jenkins, it just returns an empty response. + //So we need to compensate and check for both. + if ( responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { + return new ConnectionResponse(responseHeader, responseCode); + } else { + responseObject = (JSONObject) JSONSerializer.toJSON(response); + } } + } catch (UnknownHostException e) { + this.failBuild(e, context.logger); + } catch (UnauthorizedException e) { + this.failBuild(e, context.logger); + } catch (ForbiddenException e) { + this.failBuild(e, context.logger); } catch (IOException e) { - listener.getLogger().println(e.getMessage()); + + //E.g. "HTTP/1.1 403 No valid crumb was included in the request" + List hints = responseHeader != null ? responseHeader.get(null) : null; + String hintsString = (hints != null && hints.size() > 0) ? " - " + hints.toString() : ""; + + context.logger.println(e.getMessage() + hintsString); //If we have connectionRetryLimit set to > 0 then retry that many times. if( numberOfAttempts <= retryLimit) { - listener.getLogger().println("Connection to remote server failed, waiting for to retry - " + this.pollInterval + " seconds until next attempt."); + context.logger.println(String.format( + "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s", + (responseCode == 0 ? "" : "[" + responseCode + "]"), this.pollInterval, urlString)); e.printStackTrace(); - + // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { // Could do with a better way of sleeping... Thread.sleep(this.pollInterval * 1000); } catch (InterruptedException ex) { - this.failBuild(ex, listener); + this.failBuild(ex, context.logger); } - - listener.getLogger().println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); + context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); numberOfAttempts++; - responseObject = sendHTTPCall(urlString, requestType, build, listener, numberOfAttempts); + responseObject = sendHTTPCall(urlString, requestType, context, numberOfAttempts).getBody(); }else if(numberOfAttempts > retryLimit){ //reached the maximum number of retries, time to fail - this.failBuild(new Exception("Max number of connection retries have been exeeded."), listener); + this.failBuild(new Exception("Max number of connection retries have been exeeded."), context.logger); }else{ //something failed with the connection and we retried the max amount of times... so throw an exception to mark the build as failed. - this.failBuild(e, listener); + this.failBuild(e, context.logger); } - + } finally { // always make sure we close the connection if (connection != null) { connection.disconnect(); } - // and always clear the query string and remove some "global" values - this.clearQueryString(); - // this.build = null; - // this.listener = null; + } + return new ConnectionResponse(responseHeader, responseObject, responseCode); + } + private void addCrumbToConnection(JenkinsCrumb crumb, HttpURLConnection connection) + { + if (crumb != null) { + connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); + } + } + + private String readInputStream(HttpURLConnection connection) throws IOException + { + BufferedReader rd = null; + try { + + InputStream is; + try { + is = connection.getInputStream(); + } catch (FileNotFoundException e) { + // In case of a e.g. 404 status + is = connection.getErrorStream(); + } + + rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String line; + StringBuilder response = new StringBuilder(); + while ((line = rd.readLine()) != null) { + response.append(line); + } + return response.toString(); + + } finally { + closeQuietly(rd); } - return responseObject; + } + + /** + * Tries to obtain a Jenkins Crumb from the remote Jenkins server. + * + * @param remoteServer + * the remote Jenkins server. + * @param context + * the context of this Builder/BuildStep. + * @return {@link JenkinsCrumb} + * a JenkinsCrumb or null, if a Jenkins Crumb is not activated on the remote Jenkins server. + * @throws IOException + * if the request failed. + */ + private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext context) throws IOException + { + URL address = remoteServer.getAddress(); + URL crumbProviderUrl; + try { + String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); + crumbProviderUrl = new URL(address.toString().concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); + HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, crumbProviderUrl); + int responseCode = connection.getResponseCode(); + if(responseCode == 401) { + throw new UnauthorizedException(crumbProviderUrl); + } else if(responseCode == 403) { + throw new ForbiddenException(crumbProviderUrl); + } else { + String response = readInputStream(connection); + String[] split = response.split(":"); + return new JenkinsCrumb(split[0], split[1]); + } + } catch (FileNotFoundException e) { + //Crumb not activated in Jenkins + return null; + } + } + + private HttpURLConnection getAuthorizedConnection(RemoteJenkinsServer remoteServer, BuildContext context, URL url) throws IOException + { + URLConnection connection = url.openConnection(); + + //Set Authorization Header configured globally for remoteServer + Auth2 serverAuth = remoteServer.getAuth2(); + if (serverAuth != null) serverAuth.setAuthorizationHeader(connection, context); + + //Override Authorization Header if configured locally + Auth2 auth = this.getAuth2(); + if(auth != null && !(auth instanceof NullAuth)) { + auth.setAuthorizationHeader(connection, context); + } + + return (HttpURLConnection)connection; + } + + private void logAuthInformation(BuildContext context) throws IOException { + Auth2 serverAuth = this.findEffectiveRemoteHost(context).getAuth2(); + Auth2 localAuth = this.getAuth2(); + if(localAuth != null && !(localAuth instanceof NullAuth)) { + context.logger.println(String.format(" Using job-level defined " + localAuth.toString((Item)context.run.getParent()) )); + } else if(serverAuth != null && !(serverAuth instanceof NullAuth)) { + context.logger.println(String.format(" Using globally defined " + serverAuth.toString((Item)context.run.getParent()) )); + } else { + context.logger.println(" No credentials configured"); + } + } + + private void logConfiguration(BuildContext context, List effectiveParams) throws IOException { + String _job = getJob(); + String _jobExpanded = getJobExpanded(context); + String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; + String _remoteJenkinsName = getRemoteJenkinsName(); + String _remoteJenkinsUrl = getRemoteJenkinsUrl(); + Auth2 _auth = getAuth2(); + int _connectionRetryLimit = getConnectionRetryLimit(); + boolean _blockBuildUntilComplete = getBlockBuildUntilComplete(); + String _parameterFile = getParameterFile(); + String _parameters = (effectiveParams == null || effectiveParams.size() <= 0) ? "" : effectiveParams.toString(); + boolean _loadParamsFromFile = getLoadParamsFromFile(); + context.logger.println("################################################################################################################"); + context.logger.println(" Parameterized Remote Trigger Configuration:"); + context.logger.println( + String.format(" - job: %s %s", _job, _jobExpandedLogEntry)); + if(!isEmpty(_remoteJenkinsName)) { + context.logger.println( + String.format(" - remoteJenkinsName: %s", _remoteJenkinsName)); + } + if(!isEmpty(_remoteJenkinsUrl)) { + context.logger.println( + String.format(" - remoteJenkinsUrl: %s", _remoteJenkinsUrl)); + } + if(_auth != null && !(_auth instanceof NullAuth)) { + context.logger.println( + String.format(" - auth: %s", _auth.toString((Item)context.run.getParent()))); + } + context.logger.println( + String.format(" - parameters: %s", _parameters)); + if(_loadParamsFromFile) { + context.logger.println( + String.format(" - loadParamsFromFile: %s", _loadParamsFromFile)); + context.logger.println( + String.format(" - parameterFile: %s", _parameterFile)); + } + context.logger.println( + String.format(" - blockBuildUntilComplete: %s", _blockBuildUntilComplete)); + context.logger.println( + String.format(" - connectionRetryLimit: %s", _connectionRetryLimit)); + context.logger.println("################################################################################################################"); } /** * Helper function for character encoding - * + * * @param dirtyValue * @return encoded value */ - private String encodeValue(String dirtyValue) { + private static String encodeValue(String dirtyValue) { String cleanValue = ""; try { @@ -1063,68 +1260,118 @@ private String encodeValue(String dirtyValue) { return cleanValue; } - // Getters + /** + * @return the configured remote Jenkins name. That's the ID of a globally configured remote host. + */ public String getRemoteJenkinsName() { - return this.remoteJenkinsName; + return remoteJenkinsName; } - public String getJob() { - return this.job; + /** + * @return the configured remote Jenkins URL. This is not necessarily the effective Jenkins URL, e.g. if a full URL is specified for job! + */ + public String getRemoteJenkinsUrl() + { + return trimToNull(remoteJenkinsUrl); } - public boolean getShouldNotFailBuild() { - return this.shouldNotFailBuild; + /** + * @return true, if the authorization is overridden in the job configuration, otherwise false. + * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. + */ + public boolean getOverrideAuth() { + if(auth2 == null) return false; + if(auth2 instanceof NullAuth) return false; + return true; } - public boolean getEnhancedLogging() { - return this.enhancedLogging; + /** + * @return the list of authorizations. + * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. + */ + public List getAuth(){ + Auth oldAuth = Auth.auth2ToAuth(auth2); + ArrayList list = new ArrayList(); + list.add(oldAuth); + return list; } - public boolean getPreventRemoteBuildQueue() { - return this.preventRemoteBuildQueue; + public Auth2 getAuth2() { + migrateAuthToAuth2(); + return this.auth2; } - public boolean getBlockBuildUntilComplete() { - return this.blockBuildUntilComplete; + /** + * Migrates old Auth to Auth2 if necessary. + * @deprecated since 2.3.0-SNAPSHOT - get rid once all users migrated + */ + private void migrateAuthToAuth2() { + if(auth2 == null) { + if(auth == null || auth.size() <= 0) { + auth2 = new NullAuth(); + } else { + auth2 = Auth.authToAuth2(auth); + } + } + auth = null; + } + + public boolean getShouldNotFailBuild() { + return shouldNotFailBuild; + } + + public boolean getPreventRemoteBuildQueue() { + return preventRemoteBuildQueue; } public int getPollInterval() { - return this.pollInterval; + return pollInterval; + } + + public boolean getBlockBuildUntilComplete() { + return blockBuildUntilComplete; } /** - * @return the connectionRetryLimit + * @return the configured job value. Can be a job name or full job URL. */ - public int getConnectionRetryLimit() { - return connectionRetryLimit; + public String getJob() { + return job; + } + + /** + * @return job value with expanded env vars. + * @throws IOException + * if there is an error replacing tokens. + */ + private String getJobExpanded(BuildContext context) throws IOException { + return TokenMacroUtils.applyTokenMacroReplacements(getJob(), context); } public String getToken() { - return this.token; + return token; + } + + public String getParameters() { + return parameters; + } + + public boolean getEnhancedLogging() { + return enhancedLogging; } public boolean getLoadParamsFromFile() { - return this.loadParamsFromFile; + return loadParamsFromFile; } - + public String getParameterFile() { - return this.parameterFile; + return parameterFile; } - /** - * Based on the number of parameters set (and only on params set), returns the proper URL string - * @return A string which represents a portion of the build URL - */ - private String getBuildTypeUrl() { - boolean isParameterized = (this.getParameters().length() > 0); - - if (isParameterized) { - return RemoteBuildConfiguration.paramerizedBuildUrl; - } else { - return RemoteBuildConfiguration.normalBuildUrl; - } + public int getConnectionRetryLimit() { + return connectionRetryLimit; // For now, this is a constant } - + /** * Same as above, but takes in to consideration if the remote server has any default parameters set or not * @param isRemoteJobParameterized Boolean indicating if the remote job is parameterized or not @@ -1132,80 +1379,81 @@ private String getBuildTypeUrl() { */ private String getBuildTypeUrl(boolean isRemoteJobParameterized) { boolean isParameterized = false; - + if(isRemoteJobParameterized || (this.getParameters().length() > 0)) { isParameterized = true; } if (isParameterized) { - return RemoteBuildConfiguration.paramerizedBuildUrl; + return paramerizedBuildUrl; } else { - return RemoteBuildConfiguration.normalBuildUrl; + return normalBuildUrl; } } - + + private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException { + RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + String remoteJobUrl = generateJobUrl(remoteServer, jobNameOrUrl); + remoteJobUrl += "/api/json"; + + ConnectionResponse response = sendHTTPCall( remoteJobUrl, "GET", context, 1 ); + if(response.getResponseCode() < 400 && response.getBody() != null) { + + return response.getBody(); + + } else if(response.getResponseCode() == 401 || response.getResponseCode() == 403) { + throw new AbortException("Unauthorized to trigger " + remoteJobUrl + " - status code " + response.getResponseCode()); + } else if(response.getResponseCode() == 404) { + throw new AbortException("Remote job does not exist " + remoteJobUrl + " - status code " + response.getResponseCode()); + } else { + throw new AbortException("Unexpected response from " + remoteJobUrl + " - status code " + response.getResponseCode()); + } + } + /** * Pokes the remote server to see if it has default parameters defined or not. - * - * @param jobName Name of the remote job to test - * @param build Build object - * @param listener listner object - * @return true if the remote job has default parameters set, otherwise false + * + * @param remoteJobMetadata + * from {@link #getRemoteJobMetadata(String, BuildContext)}. + * @return true if the remote job has parameters, otherwise false. + * @throws IOException + * if it is not possible to identify if the job is parameterized. */ - private boolean isRemoteJobParameterized(String jobName, AbstractBuild build, BuildListener listener) { + private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IOException + { boolean isParameterized = false; - - //build the proper URL to inspect the remote job - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - String remoteServerUrl = remoteServer.getAddress().toString(); - remoteServerUrl += "/job/" + encodeValue(jobName); - remoteServerUrl += "/api/json"; - - try { - JSONObject response = sendHTTPCall(remoteServerUrl, "GET", build, listener); - - if(response.getJSONArray("actions").size() >= 1){ - isParameterized = true; + if (remoteJobMetadata != null) { + if (remoteJobMetadata.getJSONArray("actions").size() >= 1) { + for(Object obj : remoteJobMetadata.getJSONArray("actions")) { + if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { + isParameterized = true; + } + } } - - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } - + else { + throw new AbortException("Could not identify if job is parameterized. Job metadata not accessible or with unexpected content."); + } return isParameterized; } - public boolean getOverrideAuth() { - return this.overrideAuth; - } - - public Auth[] getAuth() { - return auth.toArray(new Auth[this.auth.size()]); - - } - - public String getParameters() { - return this.parameters; - } - - private List getParameterList() { - return this.parameterList; - } - - public String getQueryString() { - return this.queryString; - } - - private void setQueryString(String string) { - this.queryString = string.trim(); - } + protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) + { + if(isEmpty(jobNameOrUrl)) throw new IllegalArgumentException("Invalid job name/url: " + jobNameOrUrl); + String remoteJobUrl; + String _jobNameOrUrl = jobNameOrUrl.trim(); + if(FormValidationUtils.isURL(_jobNameOrUrl)) { + remoteJobUrl = _jobNameOrUrl; + } else { + remoteJobUrl = remoteServer.getAddress().toString(); + while(remoteJobUrl.endsWith("/")) remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length()-1); - /** - * Convenience function for setting the query string to empty - */ - private void clearQueryString() { - this.setQueryString(""); + String[] split = _jobNameOrUrl.trim().split("/"); + for(String segment : split) { + remoteJobUrl = String.format("%s/job/%s", remoteJobUrl, encodeValue(segment)); + } + } + return remoteJobUrl; } // Overridden for better type safety. @@ -1216,28 +1464,44 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } + public static DescriptorImpl getDescriptorStatic() { + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins == null) throw new NullPointerException("Jenkins instance can not be null"); + return (RemoteBuildConfiguration.DescriptorImpl) jenkins.getDescriptor(RemoteBuildConfiguration.class); + } + // This indicates to Jenkins that this is an implementation of an extension // point. @Extension public static final class DescriptorImpl extends BuildStepDescriptor { /** * To persist global configuration information, simply store it in a field and call save(). - * + * *

diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-remoteJenkinsUrl.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-remoteJenkinsUrl.html new file mode 100644 index 00000000..73be5c66 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-remoteJenkinsUrl.html @@ -0,0 +1,3 @@ +
+ It is possible to override the Remote Jenkins URL for each Job separately. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index 95997624..5d9257d7 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -8,13 +8,7 @@ - - - - - - - + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html new file mode 100644 index 00000000..01dfc53d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html @@ -0,0 +1,18 @@ +
+Using this parameter you can configure the authentication used to connect to the selected remote Jenkins. +
    +
  • No Authentication
    + No Authorization header will be sent. +
  • +
  • Token Authentication
    + The specified user id and Jenkins API token is used. +
  • +
  • Credentials Authentication
    + The specified Jenkins Credentials are used. This can be either user/password or user/API Token. +
  • +
+ +Note: Jenkins API Tokens are recommended since, if stolen, they allow access only to a specific Jenkins +while user and password typically provide access to many systems. + +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help.html new file mode 100644 index 00000000..515c92a3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help.html @@ -0,0 +1,3 @@ +
+ The name of the remote Jenkins as configured in the Jenkins global configuration (Manage Jenkins > Configure System > Parameterized Remote Trigger Configuration > Remote Hosts). +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly new file mode 100644 index 00000000..fdb321dd --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly new file mode 100644 index 00000000..57910bb1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly new file mode 100644 index 00000000..3e3e99cb --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-auth.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-auth.html new file mode 100644 index 00000000..55495c7a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-auth.html @@ -0,0 +1,21 @@ +
+Using this parameter you can override the authentication used to connect to the selected remote Jenkins.
+
    +
  • Don't Override
    + The authentication configured in the (global) settings of the selected 'remote host' is used. +
  • +
  • Token Authentication
    + The specified user id and Jenkins API token is used. +
  • +
  • Credentials Authentication
    + The specified Jenkins Credentials are used. This can be either user/password or user/API Token. +
  • +
  • No Authentication
    + No Authorization header will be sent, independent of the global 'remote host' settings. +
  • +
+ +Note: Jenkins API Tokens are recommended since, if stolen, they allow access only to a specific Jenkins +while user and password typically provide access to many systems. + +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-blockBuildUntilComplete.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-blockBuildUntilComplete.html new file mode 100644 index 00000000..16a4b527 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-blockBuildUntilComplete.html @@ -0,0 +1,13 @@ +
+
+ Wait/Block Until Remote Build Complete +
+ If enabled the remote job is called synchronously and the plugin waits until the remote job finished.
+ If disabled the plugin triggers the remote job and returns.
+
+ In both cases a handle is returned for further tracking the remote job or getting the results (see plugin main help page). +

+ mandatory: no
+ default: true +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-enhancedLogging.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-enhancedLogging.html new file mode 100644 index 00000000..c91afd25 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-enhancedLogging.html @@ -0,0 +1,10 @@ +
+
+ Enable Enhanced Logging +
+ If this option is enabled, the console output of the remote job is also logged. +

+ mandatory: no
+ default: false +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-job.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-job.html new file mode 100644 index 00000000..2c07c29b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-job.html @@ -0,0 +1,9 @@ +
+
+ Remote Job Name or full URL. +
+ The name or URL of the job on the remote Jenkins host which you would like to trigger. If the full job URL is specified the URL of the remote Jenkins host configured above will be ignored. +

+ mandatory: yes
+

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html new file mode 100644 index 00000000..365d597c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html @@ -0,0 +1,10 @@ +
+
+ Job Parameters +
+ Parameters which will be used when triggering the remote job. +
+ If no parameters are needed, then just leave this blank. +
+ Any line start with a pound-sign (#) will be treated as a comment. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-pollInterval.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-pollInterval.html new file mode 100644 index 00000000..3bc8cf82 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-pollInterval.html @@ -0,0 +1,11 @@ +
+
+ Polling Interval +
+ The plugin identifies the status of the remote build by polling. Here you can specify how often the plugin shall poll the remote status.
+ Be aware that polling too often might cause an increased load on the remote Jenkins. +

+ mandatory: no
+ default: 10 +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-preventRemoteBuildQueue.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-preventRemoteBuildQueue.html new file mode 100644 index 00000000..fa479f64 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-preventRemoteBuildQueue.html @@ -0,0 +1,10 @@ +
+
+ Prevent Remote Build Queue +
+ Wait to trigger remote builds until no other builds are running. +

+ mandatory: no
+ default: false +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsName.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsName.html new file mode 100644 index 00000000..fbba359f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsName.html @@ -0,0 +1,9 @@ +
+
+ Remote Jenkins Name +
+ The name of the remote Jenkins as configured in the Jenkins global configuration (Manage Jenkins > Configure System > Parameterized Remote Trigger Configuration > Remote Hosts). +

+ mandatory: yes +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsUrl.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsUrl.html new file mode 100644 index 00000000..c70332ee --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-remoteJenkinsUrl.html @@ -0,0 +1,6 @@ +
+ It is possible to override the Remote Jenkins URL for each Pipeline separately. +

+ mandatory: no +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-shouldNotFailBuild.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-shouldNotFailBuild.html new file mode 100644 index 00000000..33b98f3c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-shouldNotFailBuild.html @@ -0,0 +1,10 @@ +
+
+ Do Not Fail If Remote Fails +
+ If this option is enabled, the build will not fail even if the remote build fails. +

+ mandatory: no
+ default: false +

+
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-token.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-token.html new file mode 100644 index 00000000..09dbb106 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-token.html @@ -0,0 +1,12 @@ +
+
+ Remote Job Token +
+ Security token which is defined on the job of the remote Jenkins host. +
+ If no job token is needed to trigger this job, then just leave it blank +

+ mandatory: no
+ default: "" +

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help.html new file mode 100644 index 00000000..9e7d29e5 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help.html @@ -0,0 +1,24 @@ +
+The `triggerRemoteJob` pipeline step triggers a job on a remote Jenkins.
+The full documentation is
available in GitHub.
+
+Example: +
+//Trigger remote job
+def handle = triggerRemoteJob(remoteJenkinsName: 'remoteJenkins', job: 'RemoteJob')
+
+//Get information from the handle
+def status = handle.getBuildStatus()
+def buildUrl = handle.getBuildUrl()
+echo buildUrl.toString() + " finished with " + status.toString()
+
+//Download and parse the archived "build-results.json" (if generated and archived by remote build)
+def results = handle.readJsonFileFromBuildArchive('build-results.json')
+echo results.urlToTestResults //only example
+
+
+//List other available methods
+echo handle.help()
+
+ +
diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index c7e14599..f4d808b3 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -1,10 +1,29 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import hudson.model.FreeStyleProject; -import net.sf.json.JSONObject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.MalformedURLException; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; + +import hudson.AbortException; +import hudson.model.FreeStyleProject; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.StringParameterDefinition; +import hudson.security.csrf.DefaultCrumbIssuer; public class RemoteBuildConfigurationTest { @Rule @@ -13,27 +32,326 @@ public class RemoteBuildConfigurationTest { @Test public void testRemoteBuild() throws Exception { jenkinsRule.jenkins.setCrumbIssuer(null); + _testRemoteBuild(); + } - JSONObject authenticationMode = new JSONObject(); - authenticationMode.put("value", "none"); - JSONObject auth = new JSONObject(); - auth.put("authenticationMode", authenticationMode); + @Test + public void testRemoteBuildWithCrumb() throws Exception { + jenkinsRule.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); + _testRemoteBuild(); + } + + private void _testRemoteBuild() throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); - RemoteJenkinsServer remoteJenkinsServer = - new RemoteJenkinsServer(remoteUrl, "JENKINS", false, auth); + RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); + remoteJenkinsServer.setDisplayName("JENKINS"); + remoteJenkinsServer.setAddress(remoteUrl); RemoteBuildConfiguration.DescriptorImpl descriptor = jenkinsRule.jenkins.getDescriptorByType(RemoteBuildConfiguration.DescriptorImpl.class); descriptor.setRemoteSites(remoteJenkinsServer); FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); + remoteProject.addProperty(new ParametersDefinitionProperty( + new StringParameterDefinition("parameterName1", "value1"), + new StringParameterDefinition("parameterName2", "value2"))); FreeStyleProject project = jenkinsRule.createFreeStyleProject(); - RemoteBuildConfiguration remoteBuildConfiguration = new RemoteBuildConfiguration( - remoteJenkinsServer.getDisplayName(), false, remoteProject.getFullName(), "", - "", true, null, null, false, true, 1); - project.getBuildersList().add(remoteBuildConfiguration); + RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); + configuration.setJob(remoteProject.getFullName()); + configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); + configuration.setPreventRemoteBuildQueue(false); + configuration.setPollInterval(1); + configuration.setEnhancedLogging(true); + configuration.setParameters(""); + + project.getBuildersList().add(configuration); + jenkinsRule.waitUntilNoActivity(); jenkinsRule.buildAndAssertSuccess(project); } + + @Test @WithoutJenkins + public void testDefaults() throws IOException { + + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("job"); + + assertEquals(1, config.getAuth().size()); + assertEquals(false, config.getBlockBuildUntilComplete()); //False in Job + assertEquals(false, config.getEnhancedLogging()); + assertEquals("job", config.getJob()); + assertEquals(false, config.getLoadParamsFromFile()); + assertEquals(false, config.getOverrideAuth()); + assertEquals("", config.getParameterFile()); + assertEquals("", config.getParameters()); + assertEquals(10, config.getPollInterval()); + assertEquals(false, config.getPreventRemoteBuildQueue()); + assertEquals(null, config.getRemoteJenkinsName()); + assertEquals(false, config.getShouldNotFailBuild()); + assertEquals("", config.getToken()); + } + + @Test @WithoutJenkins + public void testDefaultsPipelineStep() throws IOException { + + RemoteBuildPipelineStep config = new RemoteBuildPipelineStep("job"); + + assertEquals(true, config.getBlockBuildUntilComplete()); //True in Pipeline Step + assertEquals(false, config.getEnhancedLogging()); + assertEquals("job", config.getJob()); + assertEquals(false, config.getLoadParamsFromFile()); + assertTrue(config.getAuth() instanceof NullAuth); + assertEquals("", config.getParameterFile()); + assertEquals("", config.getParameters()); + assertEquals(10, config.getPollInterval()); + assertEquals(false, config.getPreventRemoteBuildQueue()); + assertEquals(null, config.getRemoteJenkinsName()); + assertEquals(false, config.getShouldNotFailBuild()); + assertEquals("", config.getToken()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withoutServer() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + assertEquals("MyJob", config.getJob()); + try { + config.findEffectiveRemoteHost(null); + fail("findRemoteHost() should throw an AbortException since server not specified"); + } catch(AbortException e) { + assertEquals("Configuration of the remote Jenkins host is missing.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("http://test:8080"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("A/B/C/D/MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("A/B/C/D/MyJob", config.getJob()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrl() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://test:8080/job/folder/job/MyJob"); + assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { + //URL specified for "job" shall override specified remote host + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://testA:8080/job/folder/job/MyJobA"); + config.setRemoteJenkinsUrl("http://testB:8080"); + assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" + assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { + //URL specified for "job" shall override global setting + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://testA:8080/job/folder/job/MyJobA"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://testB:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" + assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_withoutJob() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + try { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("xxx"); + Field field = config.getClass().getDeclaredField("job"); + field.setAccessible(true); + field.set(config, ""); + config.findEffectiveRemoteHost(null); + fail("findRemoteHost() should throw an AbortException since job not specified"); + } catch(AbortException e) { + assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testRemoteUrlOverridesRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("http://globallyConfigured:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + + //Now override remote host URL + config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_jobNameMissing() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + try { + config.findEffectiveRemoteHost(null); + } + catch (AbortException e) { + assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_globalConfigMissing() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + try { + config.findEffectiveRemoteHost(null); + } + catch (AbortException e) { + assertEquals("Could get remote host with ID 'notConfiguredRemoteHost' configured in Jenkins global configuration. Please check your global configuration.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); + assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://localJobUrl:8080/job/MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + assertEquals("http://localJobUrl:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_localOverrideHostURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("http://hostname:8080"); + assertEquals("http://hostname:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + } + + @Test @WithoutJenkins + public void testFindEffectiveRemoteHost_localOverrideHostURLWrong() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("hostname:8080"); + try { + config.findEffectiveRemoteHost(null); + fail("Expected AbortException"); + } + catch (AbortException e) { + assertEquals("The 'Override remote host URL' parameter value (remoteJenkinsUrl: 'hostname:8080') is no valid URL", e.getMessage()); + } + } + + private RemoteBuildConfiguration mockGlobalRemoteHost(RemoteBuildConfiguration config, String remoteName, String remoteUrl) throws MalformedURLException { + RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); + jenkinsServer.setDisplayName(remoteName); + jenkinsServer.setAddress(remoteUrl); + + RemoteBuildConfiguration spy = spy(config); + DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); + descriptor.setRemoteSites(jenkinsServer); + doReturn(descriptor).when(spy).getDescriptor(); + + return spy; + } + + @Test @WithoutJenkins + public void testRemoveTrailingSlashes() { + assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx")); + assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx/")); + assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx//////")); + assertEquals("xxx/yy", RemoteBuildConfiguration.removeTrailingSlashes("xxx/yy//")); + assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx/ ")); + } + + @Test @WithoutJenkins + public void testRemoveQueryParameters() { + assertEquals("xxx", RemoteBuildConfiguration.removeQueryParameters("xxx")); + assertEquals("http://test:8080/MyJob", RemoteBuildConfiguration.removeQueryParameters("http://test:8080/MyJob?xy=abc")); + assertEquals("xxx", RemoteBuildConfiguration.removeQueryParameters("xxx?zzz")); + } + + @Test @WithoutJenkins + public void testRemoveHashParameters() { + assertEquals("xxx", RemoteBuildConfiguration.removeHashParameters("xxx")); + assertEquals("http://test:8080/MyJob", RemoteBuildConfiguration.removeHashParameters("http://test:8080/MyJob#asdsad")); + assertEquals("xxx", RemoteBuildConfiguration.removeHashParameters("xxx#zzz")); + } + + @Test @WithoutJenkins + public void testGenerateJobUrl() throws MalformedURLException { + RemoteJenkinsServer remoteServer = new RemoteJenkinsServer(); + remoteServer.setAddress("https://server:8080/jenkins"); + + assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); + assertEquals("https://server:8080/jenkins/job/Folder/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "Folder/JobName")); + assertEquals("https://server:8080/jenkins/job/More/job/than/job/one/job/folder", RemoteBuildConfiguration.generateJobUrl(remoteServer, "More/than/one/folder")); + try { + RemoteBuildConfiguration.generateJobUrl(remoteServer, ""); + Assert.fail("Expected IllegalArgumentException"); + } catch(IllegalArgumentException e) {} + try { + RemoteBuildConfiguration.generateJobUrl(remoteServer, null); + Assert.fail("Expected IllegalArgumentException"); + } catch(IllegalArgumentException e) {} + try { + RemoteBuildConfiguration.generateJobUrl(null, "JobName"); + Assert.fail("Expected NullPointerException"); + } catch(NullPointerException e) {} + + //Test trailing slash + remoteServer.setAddress("https://server:8080/jenkins/"); + assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); + + try { + RemoteJenkinsServer missingUrl = new RemoteJenkinsServer(); + RemoteBuildConfiguration.generateJobUrl(missingUrl, "JobName"); + Assert.fail("Expected NullPointerException"); + } catch(NullPointerException e) {} + + } + + } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPatternTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPatternTest.java deleted file mode 100644 index 2c5c4d3e..00000000 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/SearchPatternTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger; - -import junit.framework.TestCase; - -import java.util.Iterator; - -public class SearchPatternTest extends TestCase { - - public void testSearchPattern() { - SearchPattern sp = new SearchPattern(5, 2); - // Test iterator() twice - for (int x = 0; x < 2; x++) { - Iterator it = sp.iterator(); - assertEquals(5, it.next().intValue()); - assertEquals(4, it.next().intValue()); - assertEquals(6, it.next().intValue()); - assertEquals(3, it.next().intValue()); - assertEquals(7, it.next().intValue()); - assertEquals(false, it.hasNext()); - } - } - -} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java new file mode 100644 index 00000000..eec75e0f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java @@ -0,0 +1,32 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class HandleTest +{ + + @Test + public void testHelp() { + String help = Handle.help(); + //Check only a few to see if it works in general + assertContains(help, true, "- String toString()"); + assertContains(help, true, "- BuildStatus getBuildStatus()"); + assertContains(help, true, "- URL getBuildUrl()"); + assertContains(help, true, "- int getBuildNumber()"); + assertContains(help, true, "- boolean isFinished()"); + assertContains(help, false, " set"); + } + + private void assertContains(String help, boolean assertIsContained, String checkString) + { + if(assertIsContained) + assertTrue("Help does not contain '" + checkString + "': \"" + help + "\"", help.contains(checkString)); + else + assertFalse("Help contains '" + checkString + "': \"" + help + "\"", help.contains(checkString)); + } + + +} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtilsTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtilsTest.java new file mode 100644 index 00000000..2e154c2d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtilsTest.java @@ -0,0 +1,26 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.jvnet.hudson.test.WithoutJenkins; + +public class FormValidationUtilsTest +{ + + @Test @WithoutJenkins + public void testIsUrl() { + assertEquals(true, FormValidationUtils.isURL("http://xyz")); + assertEquals(true, FormValidationUtils.isURL("https://xyz")); + assertEquals(true, FormValidationUtils.isURL("https://xyz:1234/test")); + assertEquals(false, FormValidationUtils.isURL("xyz")); + assertEquals(false, FormValidationUtils.isURL("")); + assertEquals(false, FormValidationUtils.isURL(null)); + assertEquals(false, FormValidationUtils.isURL("http://")); + assertEquals(false, FormValidationUtils.isURL("https://")); + assertEquals(false, FormValidationUtils.isURL(" http://xyz ")); + assertEquals(false, FormValidationUtils.isURL("http://xyz/$jobPath")); + assertEquals(false, FormValidationUtils.isURL("http://xyz/${jobPath}")); + } + +} From dc830f3000c2ca84ae3f77632653aea401f6733d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=B3=E5=85=89=E4=B8=8B=E7=9A=84=E8=8D=89?= <264768502@users.noreply.github.com> Date: Tue, 21 Nov 2017 19:47:35 +0800 Subject: [PATCH 013/262] fix hung at crumb when CSRF is disabled --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 79e603ae..61ce9ecd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1158,6 +1158,8 @@ private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext con throw new UnauthorizedException(crumbProviderUrl); } else if(responseCode == 403) { throw new ForbiddenException(crumbProviderUrl); + } else if(responseCode == 404) { + return null; } else { String response = readInputStream(connection); String[] split = response.split(":"); From 8836639ace2a413a311675c178c62a75b3ff18b6 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 22 Nov 2017 17:30:16 +0100 Subject: [PATCH 014/262] add logging to getCrumb --- .../RemoteBuildConfiguration.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 61ce9ecd..3597daa2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1159,14 +1159,18 @@ private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext con } else if(responseCode == 403) { throw new ForbiddenException(crumbProviderUrl); } else if(responseCode == 404) { + context.logger.println("CSRF protection is disabled on the remote server."); return null; - } else { + } else if(responseCode == 200){ + context.logger.println("CSRF protection is enabled on the remote server."); String response = readInputStream(connection); String[] split = response.split(":"); return new JenkinsCrumb(split[0], split[1]); + } else { + throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", responseCode, connection.getResponseMessage())); } } catch (FileNotFoundException e) { - //Crumb not activated in Jenkins + context.logger.println("CSRF protection is disabled on the remote server."); return null; } } From 66a03ce533f5be6ae4505260aabc1c3baf6dce5d Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 6 Dec 2017 10:22:24 +0100 Subject: [PATCH 015/262] Javadoc & crumb only for POST needed Change-Id: I927ddd6349384fd2cdaf5fda221894d442316567 --- .../JenkinsCrumb.java | 31 +++++++++++++ .../RemoteBuildConfiguration.java | 43 ++++++++++++------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java index dc724fe4..61862ad2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java @@ -10,20 +10,51 @@ public class JenkinsCrumb { String headerId; String crumbValue; + boolean isEnabledOnRemote; + /** + * New JenkinsCrumb object indicating that CSRF is disabled in the remote Jenkins (no crumb needed). + */ + public JenkinsCrumb() + { + this.headerId = null; + this.crumbValue = null; + this.isEnabledOnRemote = false; + } + + /** + * New JenkinsCrumb object with the header ID and crumb value to use in subsequent requests. + * @param headerId + * @param crumbValue + */ public JenkinsCrumb(String headerId, String crumbValue) { this.headerId = headerId; this.crumbValue = crumbValue; + this.isEnabledOnRemote = true; } + /** + * @return the header ID to be used in the subsequent requests. Null if CSRF is disabled in the remote Jenkins. + */ public String getHeaderId() { return headerId; } + /** + * @return the crumb value to be used in the header of subsequent requests. Null if CSRF is disabled in the remote Jenkins. + */ public String getCrumbValue() { return crumbValue; } + + /** + * @return true if CSRF is enabled on the remote Jenkins, false otherwise. + */ + public boolean isEnabledOnRemote() + { + return isEnabledOnRemote; + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 3597daa2..b30b5bc9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -751,7 +751,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildStatus); if (this.getEnhancedLogging()) { - String consoleOutput = getConsoleOutput(jobURL, "GET", context); + String consoleOutput = getConsoleOutput(jobURL, context); context.logger.println(); context.logger.println("Console output of remote job:"); @@ -884,10 +884,10 @@ public BuildStatus getBuildStatus(String buildUrlString, BuildContext context) t return buildStatus; } - private String getConsoleOutput(URL url, String requestType, BuildContext context) + private String getConsoleOutput(URL url, BuildContext context) throws IOException { - return getConsoleOutput( url, requestType, context, 1 ); + return getConsoleOutput( url, context, 1 ); } /** @@ -913,7 +913,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, BuildContex return sendHTTPCall( urlString, requestType, context, 1 ).getBody(); } - private String getConsoleOutput(URL url, String requestType, BuildContext context, int numberOfAttempts) + private String getConsoleOutput(URL url, BuildContext context, int numberOfAttempts) throws IOException { RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); int retryLimit = this.getConnectionRetryLimit(); @@ -928,8 +928,7 @@ private String getConsoleOutput(URL url, String requestType, BuildContext contex try { connection.setDoInput(true); connection.setRequestProperty("Accept", "application/json"); - addCrumbToConnection(getCrumb(remoteServer, context), connection); - connection.setRequestMethod(requestType); + connection.setRequestMethod("GET"); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); connection.connect(); @@ -967,7 +966,7 @@ private String getConsoleOutput(URL url, String requestType, BuildContext contex context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); numberOfAttempts++; - consoleOutput = getConsoleOutput(url, requestType, context, numberOfAttempts); + consoleOutput = getConsoleOutput(url, context, numberOfAttempts); } else if(numberOfAttempts > retryLimit){ //reached the maximum number of retries, time to fail this.failBuild(new Exception("Max number of connection retries have been exeeded."), context.logger); @@ -1022,8 +1021,8 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu try { connection.setDoInput(true); connection.setRequestProperty("Accept", "application/json"); - addCrumbToConnection(getCrumb(remoteServer, context), connection); connection.setRequestMethod(requestType); + addCrumbToConnection(connection, context); // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); connection.connect(); @@ -1100,10 +1099,22 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu return new ConnectionResponse(responseHeader, responseObject, responseCode); } - private void addCrumbToConnection(JenkinsCrumb crumb, HttpURLConnection connection) + /** + * For POST requests a crumb is needed. This methods gets a crumb and sets it in the header. + * https://wiki.jenkins.io/display/JENKINS/Remote+access+API#RemoteaccessAPI-CSRFProtection + * + * @param connection + * @param context + * @throws IOException + */ + private void addCrumbToConnection(HttpURLConnection connection, BuildContext context) throws IOException { - if (crumb != null) { - connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); + String method = connection.getRequestMethod(); + if(method != null && method.equalsIgnoreCase("POST")) { + JenkinsCrumb crumb = getCrumb(context); + if (crumb.isEnabledOnRemote()) { + connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); + } } } @@ -1141,12 +1152,14 @@ private String readInputStream(HttpURLConnection connection) throws IOException * @param context * the context of this Builder/BuildStep. * @return {@link JenkinsCrumb} - * a JenkinsCrumb or null, if a Jenkins Crumb is not activated on the remote Jenkins server. + * a JenkinsCrumb. * @throws IOException * if the request failed. */ - private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext context) throws IOException + @Nonnull + private JenkinsCrumb getCrumb(BuildContext context) throws IOException { + RemoteJenkinsServer remoteServer = findEffectiveRemoteHost(context); URL address = remoteServer.getAddress(); URL crumbProviderUrl; try { @@ -1160,7 +1173,7 @@ private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext con throw new ForbiddenException(crumbProviderUrl); } else if(responseCode == 404) { context.logger.println("CSRF protection is disabled on the remote server."); - return null; + return new JenkinsCrumb(); } else if(responseCode == 200){ context.logger.println("CSRF protection is enabled on the remote server."); String response = readInputStream(connection); @@ -1171,7 +1184,7 @@ private JenkinsCrumb getCrumb(RemoteJenkinsServer remoteServer, BuildContext con } } catch (FileNotFoundException e) { context.logger.println("CSRF protection is disabled on the remote server."); - return null; + return new JenkinsCrumb(); } } From 747fd0be02a63faf445b24e39928272b3592bd94 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 22 Nov 2017 20:46:54 +0100 Subject: [PATCH 016/262] reduce findEffectiveRemoteHost calls --- .../RemoteBuildConfiguration.java | 40 ++++++++++++------- .../pipeline/Handle.java | 14 ++----- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b30b5bc9..55d61af1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -107,6 +107,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean loadParamsFromFile; private String parameterFile; + private transient RemoteJenkinsServer remoteServer; + @DataBoundConstructor public RemoteBuildConfiguration() { remoteJenkinsName = null; @@ -123,6 +125,8 @@ public RemoteBuildConfiguration() { enhancedLogging = false; loadParamsFromFile = false; parameterFile = ""; + + remoteServer = null; } @DataBoundSetter @@ -212,6 +216,10 @@ public List getParameterList(BuildContext context) { } } + public RemoteJenkinsServer getRemoteServer() { + return remoteServer; + } + /** * Reads a file from the jobs workspace, and loads the list of parameters from with in it. It will also call * ```getCleanedParameters``` before returning. @@ -339,7 +347,7 @@ private String buildUrlQueryString(Collection parameters) { * @throws MalformedURLException * if remoteJenkinsName no valid URL or job an URL but nor valid. */ - public @Nonnull RemoteJenkinsServer findEffectiveRemoteHost(BuildContext context) throws IOException { + protected @Nonnull RemoteJenkinsServer findEffectiveRemoteHost(BuildContext context) throws IOException { RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); RemoteJenkinsServer server = globallyConfiguredServer; String expandedJob = getJobExpanded(context); @@ -471,9 +479,9 @@ private String addToQueryString(String queryString, String item) { */ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, boolean isRemoteJobParameterized, BuildContext context) throws IOException { + String triggerUrlString; String query = ""; - RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); if (remoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server @@ -524,7 +532,7 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec * if there is an error identifying the remote host. */ private String buildGetUrl(String jobNameOrUrl, String securityToken, BuildContext context) throws IOException { - RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + String urlString = generateJobUrl(remoteServer, jobNameOrUrl); // don't try to include a security token in the URL if none is provided if (!isEmpty(securityToken)) { @@ -618,6 +626,8 @@ public Handle performTriggerAndGetQueueId(BuildContext context) logConfiguration(context, cleanedParams); + remoteServer = findEffectiveRemoteHost(context); + final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); @@ -788,9 +798,9 @@ public void performWaitForBuild(BuildContext context, Handle handle) */ @Nonnull private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) - throws IOException - { - URL remoteServerURL = findEffectiveRemoteHost(context).getAddress(); + throws IOException { + + URL remoteServerURL = remoteServer.getAddress(); String queueQuery = String.format("%s/queue/item/%s/api/json/", remoteServerURL, queueId); JSONObject queueResponse = sendHTTPCall(queueQuery, "GET", context); @@ -915,14 +925,14 @@ public JSONObject sendHTTPCall(String urlString, String requestType, BuildContex private String getConsoleOutput(URL url, BuildContext context, int numberOfAttempts) throws IOException { - RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + int retryLimit = this.getConnectionRetryLimit(); String consoleOutput = null; URL buildUrl = new URL(url, "consoleText"); - HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, buildUrl); + HttpURLConnection connection = getAuthorizedConnection(context, buildUrl); int responseCode = 0; try { @@ -1008,7 +1018,7 @@ private String getConsoleOutput(URL url, BuildContext context, int numberOfAttem */ private ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, int numberOfAttempts) throws IOException { - RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + int retryLimit = this.getConnectionRetryLimit(); JSONObject responseObject = null; @@ -1016,7 +1026,7 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu int responseCode = 0; URL url = new URL(urlString); - HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, url); + HttpURLConnection connection = getAuthorizedConnection(context, url); try { connection.setDoInput(true); @@ -1159,13 +1169,12 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - RemoteJenkinsServer remoteServer = findEffectiveRemoteHost(context); URL address = remoteServer.getAddress(); URL crumbProviderUrl; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); crumbProviderUrl = new URL(address.toString().concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); - HttpURLConnection connection = getAuthorizedConnection(remoteServer, context, crumbProviderUrl); + HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl); int responseCode = connection.getResponseCode(); if(responseCode == 401) { throw new UnauthorizedException(crumbProviderUrl); @@ -1188,7 +1197,7 @@ private JenkinsCrumb getCrumb(BuildContext context) throws IOException } } - private HttpURLConnection getAuthorizedConnection(RemoteJenkinsServer remoteServer, BuildContext context, URL url) throws IOException + private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) throws IOException { URLConnection connection = url.openConnection(); @@ -1206,7 +1215,8 @@ private HttpURLConnection getAuthorizedConnection(RemoteJenkinsServer remoteServ } private void logAuthInformation(BuildContext context) throws IOException { - Auth2 serverAuth = this.findEffectiveRemoteHost(context).getAuth2(); + + Auth2 serverAuth = remoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if(localAuth != null && !(localAuth instanceof NullAuth)) { context.logger.println(String.format(" Using job-level defined " + localAuth.toString((Item)context.run.getParent()) )); @@ -1411,7 +1421,7 @@ private String getBuildTypeUrl(boolean isRemoteJobParameterized) { } private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException { - RemoteJenkinsServer remoteServer = this.findEffectiveRemoteHost(context); + String remoteJobUrl = generateJobUrl(remoteServer, jobNameOrUrl); remoteJobUrl += "/api/json"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index b888b2c3..3b786ff6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -306,18 +306,10 @@ public void setBuildData(BuildData buildData) @Whitelisted @Override - public String toString() - { + public String toString() { + StringBuilder sb = new StringBuilder(); - - String remoteServerURL; - try { - remoteServerURL = remoteBuildConfiguration.findEffectiveRemoteHost(null).getAddress().toString(); - } - catch (IOException e) { - remoteServerURL = e.getMessage(); - } - + String remoteServerURL = remoteBuildConfiguration.getRemoteServer().getAddress().toString(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); From 6a85b5f9782592cce94b527392ee147a3dbb8a47 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Thu, 7 Dec 2017 17:56:29 +0100 Subject: [PATCH 017/262] add annotations to RemoteJenkinsServer --- .../RemoteJenkinsServer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index ba9a36e7..cebb0c1e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -7,6 +7,8 @@ import java.net.URL; import java.util.List; +import javax.annotation.CheckForNull; + import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; @@ -31,11 +33,15 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl auth; + @CheckForNull private String displayName; private boolean hasBuildTokenRootSupport; + @CheckForNull private Auth2 auth2; + @CheckForNull private URL address; @DataBoundConstructor @@ -66,9 +72,10 @@ public void setAddress(String address) throws MalformedURLException { this.address = new URL(address); } - + // Getters + @CheckForNull public String getDisplayName() { String displayName = null; @@ -83,7 +90,8 @@ public String getDisplayName() { public boolean getHasBuildTokenRootSupport() { return hasBuildTokenRootSupport; } - + + @CheckForNull public Auth2 getAuth2() { migrateAuthToAuth2(); return auth2; @@ -104,6 +112,7 @@ private void migrateAuthToAuth2() { auth = null; } + @CheckForNull public URL getAddress() { return address; } From ca06126afa858951c075a159b7abed105a9ceec1 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 8 Dec 2017 09:51:05 +0100 Subject: [PATCH 018/262] fix bugs found with findbugs After introducing annotations, there is bugs found by findbugs. This commit fixes this bugs. --- .../RemoteBuildConfiguration.java | 11 ++++---- .../RemoteJenkinsServer.java | 18 ++++++++++++- .../pipeline/Handle.java | 2 +- .../RemoteBuildConfigurationTest.java | 26 +++++++++---------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 55d61af1..97ad65f5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -485,7 +485,7 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec if (remoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server - triggerUrlString = remoteServer.getAddress().toString(); + triggerUrlString = remoteServer.getRemoteAddress(); triggerUrlString += buildTokenRootUrl; triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? @@ -800,8 +800,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { - URL remoteServerURL = remoteServer.getAddress(); - String queueQuery = String.format("%s/queue/item/%s/api/json/", remoteServerURL, queueId); + String queueQuery = String.format("%s/queue/item/%s/api/json/", remoteServer.getRemoteAddress(), queueId); JSONObject queueResponse = sendHTTPCall(queueQuery, "GET", context); if (queueResponse.isNullObject()) @@ -1169,11 +1168,11 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - URL address = remoteServer.getAddress(); + String address = remoteServer.getRemoteAddress(); URL crumbProviderUrl; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); - crumbProviderUrl = new URL(address.toString().concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); + crumbProviderUrl = new URL(address.concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl); int responseCode = connection.getResponseCode(); if(responseCode == 401) { @@ -1474,7 +1473,7 @@ protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String if(FormValidationUtils.isURL(_jobNameOrUrl)) { remoteJobUrl = _jobNameOrUrl; } else { - remoteJobUrl = remoteServer.getAddress().toString(); + remoteJobUrl = remoteServer.getRemoteAddress(); while(remoteJobUrl.endsWith("/")) remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length()-1); String[] split = _jobNameOrUrl.trim().split("/"); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index cebb0c1e..aa626bce 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -8,6 +8,7 @@ import java.util.List; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; @@ -80,7 +81,8 @@ public String getDisplayName() { String displayName = null; if (this.displayName == null || this.displayName.trim().equals("")) { - displayName = this.getAddress().toString(); + if (address != null) displayName = address.toString(); + else displayName = null; } else { displayName = this.displayName; } @@ -122,6 +124,20 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } + /** + * @return the remote server address + * @throws RuntimeException + * if the address of the remote server was not set + */ + @Nonnull + public String getRemoteAddress() throws RuntimeException { + if (address == null) { + throw new RuntimeException("The remote address can not be empty."); + } else { + return address.toString(); + } + } + @Extension public static class DescriptorImpl extends Descriptor { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 3b786ff6..e7403999 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -309,7 +309,7 @@ public void setBuildData(BuildData buildData) public String toString() { StringBuilder sb = new StringBuilder(); - String remoteServerURL = remoteBuildConfiguration.getRemoteServer().getAddress().toString(); + String remoteServerURL = remoteBuildConfiguration.getRemoteServer().getRemoteAddress(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index f4d808b3..8e690293 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -130,7 +130,7 @@ public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://test:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -141,7 +141,7 @@ public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -152,7 +152,7 @@ public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOEx config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("A/B/C/D/MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -160,7 +160,7 @@ public void testJobUrlHandling_withJobUrl() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("http://test:8080/job/folder/job/MyJob"); assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -170,7 +170,7 @@ public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { config.setJob("http://testA:8080/job/folder/job/MyJobA"); config.setRemoteJenkinsUrl("http://testB:8080"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -182,7 +182,7 @@ public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -207,12 +207,12 @@ public void testRemoteUrlOverridesRemoteName() throws IOException { config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("http://globallyConfigured:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://globallyConfigured:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); //Now override remote host URL config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -249,7 +249,7 @@ public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL config.setRemoteJenkinsName("notConfiguredRemoteHost"); config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); - assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -259,7 +259,7 @@ public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL( config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("notConfiguredRemoteHost"); - assertEquals("http://localJobUrl:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://localJobUrl:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -267,7 +267,7 @@ public void testFindEffectiveRemoteHost_localOverrideHostURL() throws IOExceptio RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://hostname:8080"); - assertEquals("http://hostname:8080", config.findEffectiveRemoteHost(null).getAddress().toString()); + assertEquals("http://hostname:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -348,8 +348,8 @@ public void testGenerateJobUrl() throws MalformedURLException { try { RemoteJenkinsServer missingUrl = new RemoteJenkinsServer(); RemoteBuildConfiguration.generateJobUrl(missingUrl, "JobName"); - Assert.fail("Expected NullPointerException"); - } catch(NullPointerException e) {} + Assert.fail("Expected RuntimeException"); + } catch(RuntimeException e) {} } From 37f2b5268e4a576c71ef86ea4e561f221f302619 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 8 Dec 2017 12:42:26 +0100 Subject: [PATCH 019/262] always check remote server address The remote server address is only checked when the user press the validation button, and the remote server configuration can be saved without address, this can cause a NullPointerException. The validation should be always done, and the user should know that if the adress is not configured, it must be overriden in the job configuration. --- .../RemoteJenkinsServer.java | 10 +++++----- .../RemoteJenkinsServer/config.jelly | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index aa626bce..821d588b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -152,13 +152,13 @@ public String getDisplayName() { * Remote address to be validated * @return FormValidation object */ - public FormValidation doValidateAddress(@QueryParameter String address) { + public FormValidation doCheckAddress(@QueryParameter String address) { URL host = null; // no empty addresses allowed if (address == null || address.trim().equals("")) { - return FormValidation.error("The remote address can not be left empty."); + return FormValidation.warning("The remote address can not be empty, or it must be overridden on the job configuration."); } // check if we have a valid, well-formed URL @@ -166,7 +166,7 @@ public FormValidation doValidateAddress(@QueryParameter String address) { host = new URL(address); host.toURI(); } catch (Exception e) { - return FormValidation.error("Malformed address (" + address + "), please double-check it."); + return FormValidation.error("Malformed address (" + address + "). Remember to indicate the protocol, i.e. http, https, etc."); } // check that the host is reachable @@ -175,10 +175,10 @@ public FormValidation doValidateAddress(@QueryParameter String address) { connection.setConnectTimeout(5000); connection.connect(); } catch (Exception e) { - return FormValidation.warning("Address looks good, but we were not able to connect to it"); + return FormValidation.warning("Address looks good, but a connection could not be stablished."); } - return FormValidation.okWithMarkup("Address looks good"); + return FormValidation.ok(); } public static List getAuth2Descriptors() { diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index 5d9257d7..13cd4a83 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -11,11 +11,9 @@ - + - -
From 3fc66776c8e75ae79d4957ecab78489b06a8a5cb Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 8 Dec 2017 13:42:40 +0100 Subject: [PATCH 020/262] bugfix remote server address not saved In Jenkins 1.6 the remote server address is not saved. This commit fixes this bug. --- .../RemoteJenkinsServer.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 821d588b..d32b489f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -43,11 +43,15 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl Date: Mon, 11 Dec 2017 12:50:16 +0100 Subject: [PATCH 021/262] fix javadoc warnings --- .../plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java index 61862ad2..1ccff782 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java @@ -24,8 +24,11 @@ public JenkinsCrumb() /** * New JenkinsCrumb object with the header ID and crumb value to use in subsequent requests. + * * @param headerId + * the header ID to be used in the subsequent requests. * @param crumbValue + * the crumb value to be used in the header of subsequent requests. */ public JenkinsCrumb(String headerId, String crumbValue) { From c5e5c4a740f6702be33a1b67e1a7aa6f0104d83d Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Mon, 11 Dec 2017 12:53:33 +0100 Subject: [PATCH 022/262] rename getRemoteServer as suggested --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 +- .../plugins/ParameterizedRemoteTrigger/pipeline/Handle.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 97ad65f5..e8a9a237 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -216,7 +216,7 @@ public List getParameterList(BuildContext context) { } } - public RemoteJenkinsServer getRemoteServer() { + public RemoteJenkinsServer getEffectiveRemoteServer() { return remoteServer; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index e7403999..50ac381a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -309,7 +309,7 @@ public void setBuildData(BuildData buildData) public String toString() { StringBuilder sb = new StringBuilder(); - String remoteServerURL = remoteBuildConfiguration.getRemoteServer().getRemoteAddress(); + String remoteServerURL = remoteBuildConfiguration.getEffectiveRemoteServer().getRemoteAddress(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); From 5d6ede6f859a3ecc21a96d13fab8801c6243462a Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Mon, 11 Dec 2017 15:10:03 +0100 Subject: [PATCH 023/262] Renamed remoteServer to effectiveRemoteServer --- .../RemoteBuildConfiguration.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index e8a9a237..eca8fb8e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -107,7 +107,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean loadParamsFromFile; private String parameterFile; - private transient RemoteJenkinsServer remoteServer; + private transient RemoteJenkinsServer effectiveRemoteServer; @DataBoundConstructor public RemoteBuildConfiguration() { @@ -126,7 +126,7 @@ public RemoteBuildConfiguration() { loadParamsFromFile = false; parameterFile = ""; - remoteServer = null; + effectiveRemoteServer = null; } @DataBoundSetter @@ -217,7 +217,7 @@ public List getParameterList(BuildContext context) { } public RemoteJenkinsServer getEffectiveRemoteServer() { - return remoteServer; + return effectiveRemoteServer; } /** @@ -483,15 +483,15 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec String triggerUrlString; String query = ""; - if (remoteServer.getHasBuildTokenRootSupport()) { + if (effectiveRemoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server - triggerUrlString = remoteServer.getRemoteAddress(); + triggerUrlString = effectiveRemoteServer.getRemoteAddress(); triggerUrlString += buildTokenRootUrl; triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? } else { - triggerUrlString = generateJobUrl(remoteServer, jobNameOrUrl); + triggerUrlString = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); } @@ -533,7 +533,7 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec */ private String buildGetUrl(String jobNameOrUrl, String securityToken, BuildContext context) throws IOException { - String urlString = generateJobUrl(remoteServer, jobNameOrUrl); + String urlString = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); // don't try to include a security token in the URL if none is provided if (!isEmpty(securityToken)) { urlString += "?token=" + encodeValue(securityToken); @@ -626,7 +626,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) logConfiguration(context, cleanedParams); - remoteServer = findEffectiveRemoteHost(context); + effectiveRemoteServer = findEffectiveRemoteHost(context); final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); @@ -800,7 +800,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { - String queueQuery = String.format("%s/queue/item/%s/api/json/", remoteServer.getRemoteAddress(), queueId); + String queueQuery = String.format("%s/queue/item/%s/api/json/", effectiveRemoteServer.getRemoteAddress(), queueId); JSONObject queueResponse = sendHTTPCall(queueQuery, "GET", context); if (queueResponse.isNullObject()) @@ -1156,7 +1156,7 @@ private String readInputStream(HttpURLConnection connection) throws IOException /** * Tries to obtain a Jenkins Crumb from the remote Jenkins server. * - * @param remoteServer + * @param effectiveRemoteServer * the remote Jenkins server. * @param context * the context of this Builder/BuildStep. @@ -1168,7 +1168,7 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - String address = remoteServer.getRemoteAddress(); + String address = effectiveRemoteServer.getRemoteAddress(); URL crumbProviderUrl; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); @@ -1201,7 +1201,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) URLConnection connection = url.openConnection(); //Set Authorization Header configured globally for remoteServer - Auth2 serverAuth = remoteServer.getAuth2(); + Auth2 serverAuth = effectiveRemoteServer.getAuth2(); if (serverAuth != null) serverAuth.setAuthorizationHeader(connection, context); //Override Authorization Header if configured locally @@ -1215,7 +1215,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) private void logAuthInformation(BuildContext context) throws IOException { - Auth2 serverAuth = remoteServer.getAuth2(); + Auth2 serverAuth = effectiveRemoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if(localAuth != null && !(localAuth instanceof NullAuth)) { context.logger.println(String.format(" Using job-level defined " + localAuth.toString((Item)context.run.getParent()) )); @@ -1421,7 +1421,7 @@ private String getBuildTypeUrl(boolean isRemoteJobParameterized) { private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException { - String remoteJobUrl = generateJobUrl(remoteServer, jobNameOrUrl); + String remoteJobUrl = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); remoteJobUrl += "/api/json"; ConnectionResponse response = sendHTTPCall( remoteJobUrl, "GET", context, 1 ); From 0145991e38e42d4a371daa2bdae3d731842771af Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Mon, 26 Feb 2018 14:32:51 +0100 Subject: [PATCH 024/262] Prevent ConcurrentModificationException in BuildInfoExporterAction.builds --- .../remoteJob/BuildInfoExporterAction.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index 931e6e4e..ec92604c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -51,8 +51,10 @@ public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run getBuildRefs(String project) { List refs = new ArrayList(); - for (BuildReference br : builds) { - if (br.projectName.equals(project)) refs.add(br); + synchronized (builds) { + for (BuildReference br : builds) { + if (br.projectName.equals(project)) refs.add(br); + } } return refs; } @@ -185,10 +189,12 @@ private String getBuildNumbersString(List refs, String separator */ private Set getProjectsWithBuilds() { Set projects = new LinkedHashSet(); - for (BuildReference br : this.builds) { - if (br.buildNumber != 0) { - if(projects.contains(br.projectName)) projects.remove(br.projectName); //Move to the end - projects.add(br.projectName); + synchronized (builds) { + for (BuildReference br : this.builds) { + if (br.buildNumber != 0) { + if(projects.contains(br.projectName)) projects.remove(br.projectName); //Move to the end + projects.add(br.projectName); + } } } return projects; From 3a504dc4aeb3168a5544ce43a6b2b828fc76fa1c Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 28 Feb 2018 10:38:20 +0100 Subject: [PATCH 025/262] Added concurrent tests & fixed another synchronization issue --- .../remoteJob/BuildInfoExporterAction.java | 17 +- .../BuildInfoExporterActionTest.java | 154 ++++++++++++++++++ 2 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index ec92604c..92a0da31 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -36,12 +36,15 @@ public BuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, BuildStatus buildResult) { BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildResult); - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - if (action == null) { - action = new BuildInfoExporterAction(parentBuild, reference); - parentBuild.addAction(action); - } else { - action.addBuildReference(reference); + BuildInfoExporterAction action; + synchronized(parentBuild) { + action = parentBuild.getAction(BuildInfoExporterAction.class); + if (action == null) { + action = new BuildInfoExporterAction(parentBuild, reference); + parentBuild.addAction(action); + } else { + action.addBuildReference(reference); + } } return action; } @@ -187,7 +190,7 @@ private String getBuildNumbersString(List refs, String separator * * @return Set of project names that have at least one build linked. */ - private Set getProjectsWithBuilds() { + protected Set getProjectsWithBuilds() { Set projects = new LinkedHashSet(); synchronized (builds) { for (BuildReference br : this.builds) { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java new file mode 100644 index 00000000..50f7c697 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -0,0 +1,154 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import hudson.EnvVars; +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.ItemGroup; +import hudson.model.Run; +import hudson.model.TopLevelItem; +import jenkins.model.Jenkins; + +public class BuildInfoExporterActionTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private final static int PARALLEL_JOBS = 100; + private final static int POOL_SIZE = 50; + + @Test + public void testAddBuildInfoExporterAction_sequential() throws IOException { + Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); + for (int i = 1; i <= PARALLEL_JOBS; i++) { + BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + } + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + EnvVars env = new EnvVars(); + action.buildEnvVars(null, env); + checkEnv(env); + } + + /** + * We had ConcurrentModificationExceptions in the past. + */ + @Test + public void testAddBuildInfoExporterAction_parallel() throws IOException, InterruptedException, ExecutionException { + Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); + ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE); + + //Start parallel threads adding BuildInfoExporterActions AND one thread reading in parallel + Future[] addFutures = new Future[PARALLEL_JOBS]; + for (int i = 1; i <= PARALLEL_JOBS; i++) { + addFutures[i-1] = executor.submit(new AddActionCallable(parentBuild, i)); + } + Future envFuture = executor.submit(new BuildEnvVarsCallable(parentBuild)); + + //Wait until all finished + while(!isDone(addFutures) && !envFuture.isDone()) sleep(100); + + //Check result + EnvVars env = (EnvVars)envFuture.get(); + checkEnv(env); + } + + + private void sleep(int i) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private boolean isDone(Future[] addFutures) throws InterruptedException, ExecutionException { + boolean done = true; + for(Future addFuture : addFutures) { + if(!addFuture.isDone()) { + done = false; + } else { + //Test get to check for exceptions + addFuture.get(); + } + } + return done; + } + + private void checkEnv(EnvVars env) { + Assert.assertEquals("Job"+PARALLEL_JOBS, env.get("LAST_TRIGGERED_JOB_NAME")); + for(int i = 1; i <= PARALLEL_JOBS; i++) { + Assert.assertEquals(""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals(""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals("SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job"+i)); + Assert.assertEquals("SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i)); + Assert.assertEquals("1", env.get("TRIGGERED_BUILD_RUN_COUNT_Job"+i)); + Assert.assertEquals("http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); + } + } + + private static class AddActionCallable implements Callable { + Run parentBuild; + private int i; + + public AddActionCallable(Run parentBuild, int i) { + this.parentBuild = parentBuild; + this.i = i; + } + + public Boolean call() throws MalformedURLException { + String jobName = "Job" + i; + BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, + new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + System.out.println("AddActionCallable finished for Job" + i); + + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + boolean success = action.getProjectsWithBuilds().contains(jobName); + System.out.println("AddActionCallable was " + (success ? "" : "NOT ") + "successful for Job" + i); + if(!success) Assert.fail("AddActionCallable was " + (success ? "" : "NOT ") + "successful for Job" + i); + return success; + } + } + + private static class BuildEnvVarsCallable implements Callable { + Run parentBuild; + + public BuildEnvVarsCallable(Run parentBuild) { + this.parentBuild = parentBuild; + } + + public EnvVars call() throws MalformedURLException, InterruptedException, TimeoutException { + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + EnvVars env = new EnvVars(); + long startTime = System.currentTimeMillis(); + while (action == null || action.getProjectsWithBuilds().size() < PARALLEL_JOBS) { + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + action = parentBuild.getAction(BuildInfoExporterAction.class); + if (action != null) { + //Provoke ConcurrentModificationException + action.buildEnvVars(null, env); + } + if(System.currentTimeMillis() - startTime > 120000) throw new TimeoutException("Only " + action.getProjectsWithBuilds().size() + " of " + PARALLEL_JOBS + " jobs"); + } + action.buildEnvVars(null, env); + return env; + } + } +} From 7736a1b8e95c3d7d89076a9421045bb45ff2b313 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 28 Feb 2018 11:55:17 +0100 Subject: [PATCH 026/262] improved test --- .../BuildInfoExporterActionTest.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 50f7c697..3b76888e 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -1,10 +1,9 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -93,12 +92,12 @@ private boolean isDone(Future[] addFutures) throws InterruptedException, Exec private void checkEnv(EnvVars env) { Assert.assertEquals("Job"+PARALLEL_JOBS, env.get("LAST_TRIGGERED_JOB_NAME")); for(int i = 1; i <= PARALLEL_JOBS; i++) { - Assert.assertEquals(""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); - Assert.assertEquals(""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); - Assert.assertEquals("SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job"+i)); - Assert.assertEquals("SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i)); - Assert.assertEquals("1", env.get("TRIGGERED_BUILD_RUN_COUNT_Job"+i)); - Assert.assertEquals("http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RUN_COUNT_Job"+i, "1", env.get("TRIGGERED_BUILD_RUN_COUNT_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_URL_Job"+i, "http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); } } @@ -118,9 +117,12 @@ public Boolean call() throws MalformedURLException { System.out.println("AddActionCallable finished for Job" + i); BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - boolean success = action.getProjectsWithBuilds().contains(jobName); - System.out.println("AddActionCallable was " + (success ? "" : "NOT ") + "successful for Job" + i); - if(!success) Assert.fail("AddActionCallable was " + (success ? "" : "NOT ") + "successful for Job" + i); + Set projectsWithBuilds = action.getProjectsWithBuilds(); + boolean success = projectsWithBuilds.contains(jobName); + String message = String.format("AddActionCallable %s for %s (projects in list: %s)", + (success ? "was successful " : "failed"), "Job"+i, projectsWithBuilds.size()) ; + System.out.println(message); + if(!success) Assert.fail(message); return success; } } From bb43902b7dfa3059b41dd5831515aa9b2a424712 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 28 Feb 2018 12:01:27 +0100 Subject: [PATCH 027/262] fixed test --- .../remoteJob/BuildInfoExporterActionTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 3b76888e..36ffa77c 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -90,7 +90,6 @@ private boolean isDone(Future[] addFutures) throws InterruptedException, Exec } private void checkEnv(EnvVars env) { - Assert.assertEquals("Job"+PARALLEL_JOBS, env.get("LAST_TRIGGERED_JOB_NAME")); for(int i = 1; i <= PARALLEL_JOBS; i++) { Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); From aac78184d04af75519d22ae44bbf440f23c27170 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 28 Feb 2018 13:39:03 +0100 Subject: [PATCH 028/262] javadoc --- .../BuildInfoExporterActionTest.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 36ffa77c..d559b354 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -32,6 +32,10 @@ public class BuildInfoExporterActionTest { private final static int PARALLEL_JOBS = 100; private final static int POOL_SIZE = 50; + /** + * Same as {@link #testAddBuildInfoExporterAction_parallel()} but sequentially. + * @throws IOException + */ @Test public void testAddBuildInfoExporterAction_sequential() throws IOException { Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); @@ -45,7 +49,8 @@ public void testAddBuildInfoExporterAction_sequential() throws IOException { } /** - * We had ConcurrentModificationExceptions in the past. + * We had ConcurrentModificationExceptions in the past. This test executes {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} + * and {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} in parallel to provoke a ConcurrentModificationException (which should not occur anymore). */ @Test public void testAddBuildInfoExporterAction_parallel() throws IOException, InterruptedException, ExecutionException { @@ -68,7 +73,11 @@ public void testAddBuildInfoExporterAction_parallel() throws IOException, Interr } - private void sleep(int i) { + /** + * Sleeps millis millisseconds and swallows any InterruptedExceptions. + * @param millis + */ + private void sleep(int millis) { try { Thread.sleep(100); } catch (InterruptedException e) { @@ -76,6 +85,13 @@ private void sleep(int i) { } } + /** + * Checks if all futures are done. Additionally calls {@link Future#get()} to check if an Exception occured. + * @param addFutures + * @return + * @throws InterruptedException + * @throws ExecutionException + */ private boolean isDone(Future[] addFutures) throws InterruptedException, ExecutionException { boolean done = true; for(Future addFuture : addFutures) { @@ -89,6 +105,10 @@ private boolean isDone(Future[] addFutures) throws InterruptedException, Exec return done; } + /** + * Checks if the env contains all expected variables + * @param env + */ private void checkEnv(EnvVars env) { for(int i = 1; i <= PARALLEL_JOBS; i++) { Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); @@ -100,6 +120,10 @@ private void checkEnv(EnvVars env) { } } + /** + * Calls {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. + * This Callable is typically executed multiple tiles in parallel to provoke a ConcurrentModificationException (which should not occur anymore). + */ private static class AddActionCallable implements Callable { Run parentBuild; private int i; @@ -126,6 +150,10 @@ public Boolean call() throws MalformedURLException { } } + /** + * Calls {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} repeatedly until all AddActionCallables finished. + * This way we try to provoke a ConcurrentModificationException (which should not occur anymore). + */ private static class BuildEnvVarsCallable implements Callable { Run parentBuild; From 11146186477bfb6710745cec86bb733c60675233 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 2 Mar 2018 12:52:30 +0100 Subject: [PATCH 029/262] Improved logging for queue request --- .../RemoteBuildConfiguration.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index eca8fb8e..727c5cba 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -801,10 +801,12 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo throws IOException { String queueQuery = String.format("%s/queue/item/%s/api/json/", effectiveRemoteServer.getRemoteAddress(), queueId); - JSONObject queueResponse = sendHTTPCall(queueQuery, "GET", context); + ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); + JSONObject queueResponse = response.getBody(); - if (queueResponse.isNullObject()) - throw new AbortException("Unexpected queue item response."); + if (queueResponse == null || queueResponse.isNullObject()) { + throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", response.getResponseCode(), queueQuery)); + } QueueItemData queueItem = new QueueItemData(queueResponse); From 40884bdeed5ede8399638c2ce83d7519e4b5f552 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Mon, 5 Mar 2018 12:48:47 +0100 Subject: [PATCH 030/262] Do not modify globally configured RemoteJenkinsServer Instance of RemoteJenkinsServer as it is configured globally was modified by the plugin. This was also visible to others keeping a reference to that server, and of course is was also visible within the same plugin later on. Credits to MW who first understood and explain the bug. --- .../RemoteBuildConfiguration.java | 9 +++- .../RemoteJenkinsServer.java | 52 ++++++++++++++++++- .../RemoteJenkinsServerTest.java | 23 ++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 727c5cba..b9e62bf2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -404,8 +404,13 @@ private RemoteJenkinsServer findRemoteHost(String displayName) { for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { // if we find a match, then stop looping if (displayName.equals(host.getDisplayName())) { - server = host; - break; + try { + server = (RemoteJenkinsServer)host.clone(); + break; + } catch(CloneNotSupportedException e) { + // Clone is supported by RemoteJenkinsServer + throw new RuntimeException(e); + } } } return server; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index d32b489f..edd6b2d0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -28,7 +28,7 @@ * @author Maurice W. * */ -public class RemoteJenkinsServer extends AbstractDescribableImpl { +public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable { /** * We need to keep this for compatibility - old config deserialization! @@ -198,4 +198,54 @@ public static Auth2Descriptor getDefaultAuth2Descriptor() { return NoneAuth.DESCRIPTOR; } } + + @Override + public Object clone() throws CloneNotSupportedException { + RemoteJenkinsServer clone = new RemoteJenkinsServer(); + clone.setAddress(address); + clone.setAuth2(auth2); + clone.setDisplayName(displayName); + clone.setHasBuildTokenRootSupport(hasBuildTokenRootSupport); + return clone; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((address == null) ? 0 : address.hashCode()); + result = prime * result + ((auth2 == null) ? 0 : auth2.hashCode()); + result = prime * result + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + (hasBuildTokenRootSupport ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (! (obj instanceof RemoteJenkinsServer)) + return false; + RemoteJenkinsServer other = (RemoteJenkinsServer) obj; + if (address == null) { + if (other.address != null) + return false; + } else if (!address.equals(other.address)) + return false; + if (auth2 == null) { + if (other.auth2 != null) + return false; + } else if (!auth2.equals(other.auth2)) + return false; + if (displayName == null) { + if (other.displayName != null) + return false; + } else if (!displayName.equals(other.displayName)) + return false; + if (hasBuildTokenRootSupport != other.hasBuildTokenRootSupport) + return false; + return true; + } } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java new file mode 100644 index 00000000..ef0359e0 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -0,0 +1,23 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + + +public class RemoteJenkinsServerTest { + + @Test + public void payAttentionToCloneContract() throws Exception { + RemoteJenkinsServer server = new RemoteJenkinsServer(); + server.setAddress("http://www.example.org:8443"); + server.setDisplayName("My example server."); + server.setHasBuildTokenRootSupport(false); + + Object clone = server.clone(); + + assertTrue(clone.equals(server)); + assertFalse(System.identityHashCode(server) == System.identityHashCode(clone)); + } +} From dd7a3f9f301c45a39aab8b44735d097ff3c0ba60 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 6 Mar 2018 09:37:14 +0100 Subject: [PATCH 031/262] Introduced deep-clone in RemoteJenkinsServer --- .../RemoteBuildConfiguration.java | 6 +-- .../RemoteJenkinsServer.java | 10 ++--- .../auth2/Auth2.java | 13 ++++++- .../auth2/CredentialsAuth.java | 34 ++++++++++++++++ .../auth2/NoneAuth.java | 23 ++++++++++- .../auth2/NullAuth.java | 22 +++++++++++ .../auth2/TokenAuth.java | 39 +++++++++++++++++++ 7 files changed, 137 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b9e62bf2..bc98e6b2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -392,11 +392,11 @@ private String buildUrlQueryString(Collection parameters) { } /** - * Lookup up a Remote Jenkins Server based on display name + * Lookup up the globally configured Remote Jenkins Server based on display name * * @param displayName * Name of the configuration you are looking for - * @return A RemoteJenkinsServer object + * @return A deep-copy of the RemoteJenkinsServer object configured globally */ private RemoteJenkinsServer findRemoteHost(String displayName) { if(isEmpty(displayName)) return null; @@ -405,7 +405,7 @@ private RemoteJenkinsServer findRemoteHost(String displayName) { // if we find a match, then stop looping if (displayName.equals(host.getDisplayName())) { try { - server = (RemoteJenkinsServer)host.clone(); + server = host.clone(); break; } catch(CloneNotSupportedException e) { // Clone is supported by RemoteJenkinsServer diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index edd6b2d0..7df848db 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -200,12 +200,12 @@ public static Auth2Descriptor getDefaultAuth2Descriptor() { } @Override - public Object clone() throws CloneNotSupportedException { + public RemoteJenkinsServer clone() throws CloneNotSupportedException { RemoteJenkinsServer clone = new RemoteJenkinsServer(); - clone.setAddress(address); - clone.setAuth2(auth2); - clone.setDisplayName(displayName); - clone.setHasBuildTokenRootSupport(hasBuildTokenRootSupport); + clone.address = address; + clone.auth2 = auth2.clone(); + clone.displayName = displayName; + clone.hasBuildTokenRootSupport = hasBuildTokenRootSupport; return clone; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index d28c97a5..6df2b604 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -5,6 +5,7 @@ import java.net.URLConnection; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; import hudson.DescriptorExtensionList; @@ -13,7 +14,7 @@ import hudson.model.Item; import jenkins.model.Jenkins; -public abstract class Auth2 extends AbstractDescribableImpl implements Serializable { +public abstract class Auth2 extends AbstractDescribableImpl implements Serializable, Cloneable { private static final long serialVersionUID = -3217381962636283564L; @@ -82,4 +83,14 @@ else if (auth instanceof CredentialsAuth) { */ public abstract String toString(Item item); + + @Override + public abstract Auth2 clone() throws CloneNotSupportedException; + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 4277d7d8..b1dd644b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -161,4 +161,38 @@ public static ListBoxModel doFillCredentialsItems() { } } + + + @Override + public Auth2 clone() throws CloneNotSupportedException { + CredentialsAuth clone = new CredentialsAuth(); + clone.credentials = credentials; + return clone; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((credentials == null) ? 0 : credentials.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CredentialsAuth other = (CredentialsAuth) obj; + if (credentials == null) { + if (other.credentials != null) + return false; + } else if (!credentials.equals(other.credentials)) + return false; + return true; + } + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index 546b7baa..edd91089 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -49,4 +49,25 @@ public String getDisplayName() { } } -} + @Override + public Auth2 clone() throws CloneNotSupportedException { + return new NoneAuth(); + } + + @Override + public int hashCode() { + return "NoneAuth".hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 2573053d..27966a31 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -49,4 +49,26 @@ public String getDisplayName() { } } + + @Override + public Auth2 clone() throws CloneNotSupportedException { + return new NullAuth(); + } + + @Override + public int hashCode() { + return "NullAuth".hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + return true; + } + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index ac346e5f..93160bce 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -77,4 +77,43 @@ public String getDisplayName() { } } + @Override + public Auth2 clone() throws CloneNotSupportedException { + TokenAuth clone = new TokenAuth(); + clone.apiToken = apiToken; + clone.userName = userName; + return clone; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((apiToken == null) ? 0 : apiToken.hashCode()); + result = prime * result + ((userName == null) ? 0 : userName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TokenAuth other = (TokenAuth) obj; + if (apiToken == null) { + if (other.apiToken != null) + return false; + } else if (!apiToken.equals(other.apiToken)) + return false; + if (userName == null) { + if (other.userName != null) + return false; + } else if (!userName.equals(other.userName)) + return false; + return true; + } + } From 76de305cfb339f89de7b1d358faa0dc21da7ed86 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 6 Mar 2018 10:22:06 +0100 Subject: [PATCH 032/262] Added more tests --- .../auth2/CredentialsAuth.java | 2 +- .../auth2/NoneAuth.java | 2 +- .../auth2/NullAuth.java | 2 +- .../auth2/TokenAuth.java | 2 +- .../RemoteJenkinsServerTest.java | 81 +++++++++++++++++-- .../auth2/Auth2Test.java | 72 +++++++++++++++++ 6 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index b1dd644b..5dc1ab8a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -164,7 +164,7 @@ public static ListBoxModel doFillCredentialsItems() { @Override - public Auth2 clone() throws CloneNotSupportedException { + public CredentialsAuth clone() throws CloneNotSupportedException { CredentialsAuth clone = new CredentialsAuth(); clone.credentials = credentials; return clone; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index edd91089..fc5542bc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -50,7 +50,7 @@ public String getDisplayName() { } @Override - public Auth2 clone() throws CloneNotSupportedException { + public NoneAuth clone() throws CloneNotSupportedException { return new NoneAuth(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 27966a31..e437f44f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -51,7 +51,7 @@ public String getDisplayName() { @Override - public Auth2 clone() throws CloneNotSupportedException { + public NullAuth clone() throws CloneNotSupportedException { return new NullAuth(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index 93160bce..d6b579d3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -78,7 +78,7 @@ public String getDisplayName() { } @Override - public Auth2 clone() throws CloneNotSupportedException { + public TokenAuth clone() throws CloneNotSupportedException { TokenAuth clone = new TokenAuth(); clone.apiToken = apiToken; clone.userName = userName; diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index ef0359e0..e817288f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -3,21 +3,86 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.CredentialsAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + import org.junit.Test; public class RemoteJenkinsServerTest { + private final static String TOKEN = "myToken"; + private final static String USER = "myUser"; + private final static String ADDRESS = "http://www.example.org:8443"; + private final static String DISPLAY_NAME = "My example server."; + private final static boolean HAS_BUILD_TOKEN_ROOT_SUPPORT = true; + @Test - public void payAttentionToCloneContract() throws Exception { + public void testCloneBehaviour() throws Exception { + TokenAuth auth = new TokenAuth(); + auth.setApiToken(TOKEN); + auth.setUserName(USER); + RemoteJenkinsServer server = new RemoteJenkinsServer(); - server.setAddress("http://www.example.org:8443"); - server.setDisplayName("My example server."); - server.setHasBuildTokenRootSupport(false); - - Object clone = server.clone(); + server.setAddress(ADDRESS); + server.setDisplayName(DISPLAY_NAME); + server.setAuth2(auth); + server.setHasBuildTokenRootSupport(HAS_BUILD_TOKEN_ROOT_SUPPORT); + + RemoteJenkinsServer clone = server.clone(); - assertTrue(clone.equals(server)); - assertFalse(System.identityHashCode(server) == System.identityHashCode(clone)); + //Test if still equal after cloning + verifyEqualsHashCode(server, clone); + assertEquals("address", ADDRESS, clone.getAddress()); + assertEquals("address", server.getAddress(), clone.getAddress()); + assertEquals("remoteAddress", ADDRESS, clone.getRemoteAddress()); + assertEquals("remoteAddress", server.getRemoteAddress(), clone.getRemoteAddress()); + assertEquals("auth2", server.getAuth2(), clone.getAuth2()); + assertEquals("displayName", DISPLAY_NAME, clone.getDisplayName()); + assertEquals("displayName", server.getDisplayName(), clone.getDisplayName()); + assertEquals("hasBuildTokenRootSupport", HAS_BUILD_TOKEN_ROOT_SUPPORT, clone.getHasBuildTokenRootSupport()); + assertEquals("hasBuildTokenRootSupport", server.getHasBuildTokenRootSupport(), clone.getHasBuildTokenRootSupport()); + + //Test if original object affected by clone modifications + clone.setAddress("http://www.changed.org:8443"); + clone.setDisplayName("Changed"); + clone.setHasBuildTokenRootSupport(false); + verifyEqualsHashCode(server, clone, false); + assertEquals("address", ADDRESS, server.getAddress()); + assertEquals("displayName", DISPLAY_NAME, server.getDisplayName()); + assertEquals("hasBuildTokenRootSupport", HAS_BUILD_TOKEN_ROOT_SUPPORT, server.getHasBuildTokenRootSupport()); + + //Test if clone is deep-copy or if server fields can be modified + TokenAuth cloneAuth = (TokenAuth)clone.getAuth2(); + cloneAuth.setApiToken("changed"); + cloneAuth.setUserName("changed"); + TokenAuth serverAuth = (TokenAuth)server.getAuth2(); + assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken()); + assertEquals("auth.userName", USER, serverAuth.getUserName()); + + //Test if clone.setAuth() affects original object + CredentialsAuth credAuth = new CredentialsAuth(); + clone.setAuth2(credAuth); + assertEquals("auth", auth, server.getAuth2()); } + + private void verifyEqualsHashCode(RemoteJenkinsServer server, RemoteJenkinsServer clone) throws CloneNotSupportedException { + verifyEqualsHashCode(server, clone, true); + } + + private void verifyEqualsHashCode(RemoteJenkinsServer server, RemoteJenkinsServer clone, boolean expectToBeSame) throws CloneNotSupportedException { + assertNotEquals("Still same object after clone", System.identityHashCode(server), System.identityHashCode(clone)); + if(expectToBeSame) { + assertTrue("clone not equals() server", clone.equals(server)); + assertEquals("clone has different hashCode() than server", server.hashCode(), clone.hashCode()); + } else { + assertFalse("clone still equals() server", clone.equals(server)); + assertNotEquals("clone still has same hashCode() than server", server.hashCode(), clone.hashCode()); + } + } + } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java new file mode 100644 index 00000000..ba9fa7a6 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -0,0 +1,72 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class Auth2Test { + + @Test + public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedException { + CredentialsAuth original = new CredentialsAuth(); + original.setCredentials("original"); + CredentialsAuth clone = original.clone(); + verifyEqualsHashCode(original, clone); + + //Test changing clone + clone.setCredentials("changed"); + verifyEqualsHashCode(original, clone, false); + assertEquals("original", original.getCredentials()); + assertEquals("changed", clone.getCredentials()); + } + + @Test + public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { + TokenAuth original = new TokenAuth(); + original.setApiToken("original"); + original.setUserName("original"); + TokenAuth clone = original.clone(); + verifyEqualsHashCode(original, clone); + + //Test changing clone + clone.setApiToken("changed"); + clone.setUserName("changed"); + verifyEqualsHashCode(original, clone, false); + assertEquals("original", original.getApiToken()); + assertEquals("original", original.getUserName()); + assertEquals("changed", clone.getApiToken()); + assertEquals("changed", clone.getUserName()); + } + + @Test + public void testNullAuthCloneBehaviour() throws CloneNotSupportedException { + NullAuth original = new NullAuth(); + NullAuth clone = original.clone(); + verifyEqualsHashCode(original, clone); + } + + @Test + public void testNoneAuthCloneBehaviour() throws CloneNotSupportedException { + NoneAuth original = new NoneAuth(); + NoneAuth clone = original.clone(); + verifyEqualsHashCode(original, clone); + } + + private void verifyEqualsHashCode(Auth2 original, Auth2 clone) throws CloneNotSupportedException { + verifyEqualsHashCode(original, clone, true); + } + + private void verifyEqualsHashCode(Auth2 server, Auth2 clone, boolean expectToBeSame) throws CloneNotSupportedException { + assertNotEquals("Still same object after clone", System.identityHashCode(server), System.identityHashCode(clone)); + if(expectToBeSame) { + assertTrue("clone not equals() server", clone.equals(server)); + assertEquals("clone has different hashCode() than server", server.hashCode(), clone.hashCode()); + } else { + assertFalse("clone still equals() server", clone.equals(server)); + assertNotEquals("clone still has same hashCode() than server", server.hashCode(), clone.hashCode()); + } + } +} From 904e1b8a2d6d305d0f03a02306d9f77d33d55063 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 7 Mar 2018 13:08:19 +0100 Subject: [PATCH 033/262] changed equals() to instanceof --- .../auth2/CredentialsAuth.java | 3 ++- .../ParameterizedRemoteTrigger/auth2/NoneAuth.java | 2 +- .../ParameterizedRemoteTrigger/auth2/NullAuth.java | 2 +- .../ParameterizedRemoteTrigger/auth2/TokenAuth.java | 2 +- .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 12 ++++++------ 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 5dc1ab8a..4a05c738 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -15,6 +15,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.jelly.ThisTagLibrary; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; @@ -184,7 +185,7 @@ public boolean equals(Object obj) { return true; if (obj == null) return false; - if (getClass() != obj.getClass()) + if (!this.getClass().isInstance(obj)) return false; CredentialsAuth other = (CredentialsAuth) obj; if (credentials == null) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index fc5542bc..4903e458 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -65,7 +65,7 @@ public boolean equals(Object obj) { return true; if (obj == null) return false; - if (getClass() != obj.getClass()) + if (!this.getClass().isInstance(obj)) return false; return true; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index e437f44f..4245d758 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -66,7 +66,7 @@ public boolean equals(Object obj) { return true; if (obj == null) return false; - if (getClass() != obj.getClass()) + if (!this.getClass().isInstance(obj)) return false; return true; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index d6b579d3..bd3c7700 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -100,7 +100,7 @@ public boolean equals(Object obj) { return true; if (obj == null) return false; - if (getClass() != obj.getClass()) + if (!this.getClass().isInstance(obj)) return false; TokenAuth other = (TokenAuth) obj; if (apiToken == null) { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index ba9fa7a6..ca11ac3b 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -59,14 +59,14 @@ private void verifyEqualsHashCode(Auth2 original, Auth2 clone) throws CloneNotSu verifyEqualsHashCode(original, clone, true); } - private void verifyEqualsHashCode(Auth2 server, Auth2 clone, boolean expectToBeSame) throws CloneNotSupportedException { - assertNotEquals("Still same object after clone", System.identityHashCode(server), System.identityHashCode(clone)); + private void verifyEqualsHashCode(Auth2 original, Auth2 clone, boolean expectToBeSame) throws CloneNotSupportedException { + assertNotEquals("Still same object after clone", System.identityHashCode(original), System.identityHashCode(clone)); if(expectToBeSame) { - assertTrue("clone not equals() server", clone.equals(server)); - assertEquals("clone has different hashCode() than server", server.hashCode(), clone.hashCode()); + assertTrue("clone not equals() original", clone.equals(original)); + assertEquals("clone has different hashCode() than original", original.hashCode(), clone.hashCode()); } else { - assertFalse("clone still equals() server", clone.equals(server)); - assertNotEquals("clone still has same hashCode() than server", server.hashCode(), clone.hashCode()); + assertFalse("clone still equals() original", clone.equals(original)); + assertNotEquals("clone still has same hashCode() than original", original.hashCode(), clone.hashCode()); } } } From 5accce7aa3bf09817c9fa907d0e11743eb737c58 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 8 Mar 2018 11:38:44 +0100 Subject: [PATCH 034/262] remove unused imports --- .../plugins/ParameterizedRemoteTrigger/auth2/Auth2.java | 1 - .../ParameterizedRemoteTrigger/auth2/CredentialsAuth.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index 6df2b604..71b11fb6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -5,7 +5,6 @@ import java.net.URLConnection; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; import hudson.DescriptorExtensionList; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 4a05c738..34adac08 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -15,7 +15,6 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.jelly.ThisTagLibrary; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; From 68de68ea4806b763964b3fbba5094fb9eaa62aca Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 9 Mar 2018 09:44:05 +0100 Subject: [PATCH 035/262] Fixed potential parallelization issue with effectiveRemoteServer --- .../BuildContext.java | 4 +++ .../RemoteBuildConfiguration.java | 30 +++++++------------ .../pipeline/Handle.java | 7 +++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java index dcd39e74..e141a9e0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -38,6 +38,10 @@ public class BuildContext @Nonnull public final PrintStream logger; + @Nullable + public RemoteJenkinsServer effectiveRemoteServer; + + /** * The current Item (job, pipeline,...) where the plugin is used from. * diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index bc98e6b2..eace3f78 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -107,8 +107,6 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean loadParamsFromFile; private String parameterFile; - private transient RemoteJenkinsServer effectiveRemoteServer; - @DataBoundConstructor public RemoteBuildConfiguration() { remoteJenkinsName = null; @@ -125,8 +123,6 @@ public RemoteBuildConfiguration() { enhancedLogging = false; loadParamsFromFile = false; parameterFile = ""; - - effectiveRemoteServer = null; } @DataBoundSetter @@ -216,10 +212,6 @@ public List getParameterList(BuildContext context) { } } - public RemoteJenkinsServer getEffectiveRemoteServer() { - return effectiveRemoteServer; - } - /** * Reads a file from the jobs workspace, and loads the list of parameters from with in it. It will also call * ```getCleanedParameters``` before returning. @@ -488,15 +480,15 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec String triggerUrlString; String query = ""; - if (effectiveRemoteServer.getHasBuildTokenRootSupport()) { + if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server - triggerUrlString = effectiveRemoteServer.getRemoteAddress(); + triggerUrlString = context.effectiveRemoteServer.getRemoteAddress(); triggerUrlString += buildTokenRootUrl; triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? } else { - triggerUrlString = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); + triggerUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); } @@ -538,7 +530,7 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec */ private String buildGetUrl(String jobNameOrUrl, String securityToken, BuildContext context) throws IOException { - String urlString = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); + String urlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); // don't try to include a security token in the URL if none is provided if (!isEmpty(securityToken)) { urlString += "?token=" + encodeValue(securityToken); @@ -631,7 +623,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) logConfiguration(context, cleanedParams); - effectiveRemoteServer = findEffectiveRemoteHost(context); + context.effectiveRemoteServer = findEffectiveRemoteHost(context); final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); @@ -679,7 +671,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); - Handle handle = new Handle(this, queueItem.getId(), context.currentItem); + Handle handle = new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer); handle.setJobMetadata(remoteJobMetadata); return handle; } @@ -805,7 +797,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { - String queueQuery = String.format("%s/queue/item/%s/api/json/", effectiveRemoteServer.getRemoteAddress(), queueId); + String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getRemoteAddress(), queueId); ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); JSONObject queueResponse = response.getBody(); @@ -1175,7 +1167,7 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - String address = effectiveRemoteServer.getRemoteAddress(); + String address = context.effectiveRemoteServer.getRemoteAddress(); URL crumbProviderUrl; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); @@ -1208,7 +1200,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) URLConnection connection = url.openConnection(); //Set Authorization Header configured globally for remoteServer - Auth2 serverAuth = effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); if (serverAuth != null) serverAuth.setAuthorizationHeader(connection, context); //Override Authorization Header if configured locally @@ -1222,7 +1214,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) private void logAuthInformation(BuildContext context) throws IOException { - Auth2 serverAuth = effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if(localAuth != null && !(localAuth instanceof NullAuth)) { context.logger.println(String.format(" Using job-level defined " + localAuth.toString((Item)context.run.getParent()) )); @@ -1428,7 +1420,7 @@ private String getBuildTypeUrl(boolean isRemoteJobParameterized) { private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException { - String remoteJobUrl = generateJobUrl(effectiveRemoteServer, jobNameOrUrl); + String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); remoteJobUrl += "/api/json"; ConnectionResponse response = sendHTTPCall( remoteJobUrl, "GET", context, 1 ); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 50ac381a..98b771d9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -15,6 +15,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; @@ -46,6 +47,7 @@ public class Handle implements Serializable { private String jobDisplayName; private String jobFullDisplayName; private String jobUrl; + private String remoteServerURL; /** * The current local Item (Job, Pipeline,...) where this plugin is currently used. @@ -61,7 +63,8 @@ public class Handle implements Serializable { */ private String lastLog; - public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, @Nonnull String currentItem) + + public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, @Nonnull String currentItem, @Nonnull RemoteJenkinsServer effectiveRemoteServer) { this.remoteBuildConfiguration = remoteBuildConfiguration; this.queueId = queueId; @@ -69,6 +72,7 @@ public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, this.buildStatus = null; this.lastLog = ""; this.currentItem = currentItem; + this.remoteServerURL = effectiveRemoteServer.getRemoteAddress(); if(trimToNull(currentItem) == null) throw new IllegalArgumentException("currentItem null"); } @@ -309,7 +313,6 @@ public void setBuildData(BuildData buildData) public String toString() { StringBuilder sb = new StringBuilder(); - String remoteServerURL = remoteBuildConfiguration.getEffectiveRemoteServer().getRemoteAddress(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); From 0587122594afb94f9943693de4c796fd9bbc6adf Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 9 Mar 2018 13:18:43 +0100 Subject: [PATCH 036/262] Added Jenkinsfile to build on ci.jenkins.io --- Jenkinsfile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..75a5ba9c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,2 @@ +// Builds a module using https://github.com/jenkins-infra/pipeline-library +buildPlugin() \ No newline at end of file From 6d02a42fb045127d1ef2c21f970bb48fa2b2fdfe Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 9 Mar 2018 13:48:32 +0100 Subject: [PATCH 037/262] bump core version to 1.642.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 57f9b874..fe781352 100644 --- a/pom.xml +++ b/pom.xml @@ -7,14 +7,14 @@ - 1.577 + 1.642.3 7 2.1 false Parameterized-Remote-Trigger - 2.3.0-SNAPSHOT + 3.0.0-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. From 056fbbc695b5150f91068fd290d12080ed60c7b7 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Mon, 12 Mar 2018 15:55:37 +0100 Subject: [PATCH 038/262] Added Jenkinsfile requirements description --- Jenkinsfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 75a5ba9c..7c0976c2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,2 +1,8 @@ // Builds a module using https://github.com/jenkins-infra/pipeline-library +// Requirements: +// - agents with label 'linux' and 'windows' +// - tools with label 'jdk8' and 'mvn' +// - latest Pipeline plugins, 'Timestamper' plugin +// - recommended to use this Jenkinsfile with 'Multibranch Pipeline' plugin + buildPlugin() \ No newline at end of file From 1f001e4e7ae67fb3d336686b1571ba029287a5ea Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Mon, 12 Mar 2018 18:16:36 +0100 Subject: [PATCH 039/262] Changes based on review feedback https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/32#pullrequestreview-100987235 --- .../plugins/ParameterizedRemoteTrigger/Auth.java | 7 +++++-- .../RemoteBuildConfiguration.java | 9 ++++++++- .../ParameterizedRemoteTrigger/RemoteJenkinsServer.java | 8 +++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index 79f7cd21..77424db2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -39,6 +39,9 @@ public class Auth extends AbstractDescribableImpl implements Serializable public static final String API_TOKEN = "apiToken"; public static final String CREDENTIALS_PLUGIN = "credentialsPlugin"; + private static final Auth2 NONE_AUTH = new NoneAuth(); + private static final Auth2 NULL_AUTH = new NullAuth(); + private final String authType; private final String username; private final String apiToken; @@ -185,7 +188,7 @@ public static Auth2 authToAuth2(List oldAuth) { public static Auth2 authToAuth2(Auth oldAuth) { String authType = oldAuth.getAuthType(); if (Auth.NONE.equals(authType)) { - return new NoneAuth(); + return NONE_AUTH; } else if (Auth.API_TOKEN.equals(authType)) { TokenAuth newAuth = new TokenAuth(); newAuth.setUserName(oldAuth.getUsername()); @@ -196,7 +199,7 @@ public static Auth2 authToAuth2(Auth oldAuth) { newAuth.setCredentials(oldAuth.getCreds()); return newAuth; } else { - return new NullAuth(); + return NULL_AUTH; } } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index eace3f78..611f48f4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; import static org.apache.commons.io.IOUtils.closeQuietly; +import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; @@ -45,6 +46,8 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; @@ -452,7 +455,7 @@ private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException * @param item */ private String addToQueryString(String queryString, String item) { - if (queryString == null || queryString.equals("")) { + if (isBlank(queryString)) { return item; } else { return queryString + "&" + item; @@ -1561,6 +1564,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc return super.configure(req, formData); } + @Restricted(NoExternalUse.class) public FormValidation doCheckJob( @QueryParameter("job") final String value, @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @@ -1570,6 +1574,7 @@ public FormValidation doCheckJob( return FormValidation.ok(); } + @Restricted(NoExternalUse.class) public FormValidation doCheckRemoteJenkinsUrl( @QueryParameter("remoteJenkinsUrl") final String value, @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, @@ -1579,6 +1584,7 @@ public FormValidation doCheckRemoteJenkinsUrl( return FormValidation.ok(); } + @Restricted(NoExternalUse.class) public FormValidation doCheckRemoteJenkinsName( @QueryParameter("remoteJenkinsName") final String value, @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @@ -1588,6 +1594,7 @@ public FormValidation doCheckRemoteJenkinsName( return FormValidation.ok(); } + @Restricted(NoExternalUse.class) public ListBoxModel doFillRemoteJenkinsNameItems() { ListBoxModel model = new ListBoxModel(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 7df848db..b7b9b7c4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -13,6 +13,8 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; @@ -47,11 +49,6 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl Date: Mon, 12 Mar 2018 18:18:13 +0100 Subject: [PATCH 040/262] unused method Auth2.identifyUser --- .../auth2/Auth2.java | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index 71b11fb6..26fbdc27 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -29,31 +29,6 @@ public static abstract class Auth2Descriptor extends Descriptor { } - /** - * Tries to identify a user. Depending on the Auth2 type it might be null. - * - * @param auth - * authorization to trigger jobs in the remote server - * @return the user name - * @throws CredentialsNotFoundException - * if the credentials are not found - */ - public static String identifyUser(Auth2 auth) throws CredentialsNotFoundException - { - if (auth == null) { - return null; - } - else if (auth instanceof TokenAuth) { - return ((TokenAuth) auth).getUserName(); - } - else if (auth instanceof CredentialsAuth) { - return ((CredentialsAuth) auth).getUserName(null); - } - else { - return null; - } - } - /** * Depending on the purpose the Auth2 implementation has to override the * Authorization header of the connection appropriately. It might also ignore this From 293c9907c42ec118e4474a86d8e379181515529c Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Mon, 12 Mar 2018 18:28:48 +0100 Subject: [PATCH 041/262] Further review changes https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/32#pullrequestreview-100987235 --- .../ParameterizedRemoteTrigger/pipeline/Handle.java | 7 ------- .../utils/FormValidationUtils.java | 4 ++++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 98b771d9..b051646c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -157,13 +157,6 @@ public String getQueueId() { return queueId; } -// /** -// * @return The full name of the item (Job, Pipeline,...) where we are currently running in locally. -// */ -// public String getCurrentItem() { -// return currentItem; -// } - /** * Get the build URL of the remote build. * diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java index d4acdea3..6a4653f5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -7,8 +7,12 @@ import java.net.URL; import java.util.Arrays; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + import hudson.util.FormValidation; +@Restricted(NoExternalUse.class) public class FormValidationUtils { From c1547ab5fdcc3a61394b9d341c7c0b2f1962dbc6 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 13 Mar 2018 15:06:27 +0100 Subject: [PATCH 042/262] Jenkins plugin parent 3.5 + fixes --- pom.xml | 20 +++++++++++-------- .../RemoteBuildConfiguration.java | 15 +++++++------- .../RemoteJenkinsServer.java | 2 +- .../auth2/NoneAuth.java | 4 +++- src/main/resources/index.jelly | 1 + .../Auth/config.jelly | 1 + .../RemoteBuildConfiguration/config.jelly | 1 + .../RemoteBuildConfiguration/global.jelly | 1 + .../RemoteJenkinsServer/config.jelly | 1 + .../auth2/CredentialsAuth/config.jelly | 1 + .../auth2/TokenAuth/config.jelly | 1 + .../RemoteBuildPipelineStep/config.jelly | 1 + 12 files changed, 32 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index fe781352..0f1df920 100644 --- a/pom.xml +++ b/pom.xml @@ -3,13 +3,12 @@ org.jenkins-ci.plugins plugin - 2.2 + 3.5 1.642.3 7 - 2.1 false @@ -39,15 +38,13 @@ org.jenkins-ci.tools maven-hpi-plugin - 1.95 - 2.3.0-SNAPSHOT + 3.0.0-SNAPSHOT maven-javadoc-plugin - 2.10.3 false @@ -80,7 +77,7 @@ org.jenkins-ci.plugins credentials - 1.9.4 + 2.1.4 org.jenkins-ci.plugins @@ -96,7 +93,7 @@ org.jenkins-ci.plugins.workflow workflow-cps - 2.2 + 2.18 true @@ -105,10 +102,17 @@ 2.9 true + + + org.jenkins-ci.plugins.workflow + workflow-support + 2.6 + true + org.jenkins-ci.plugins structs - 1.2 + 1.5 true diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 611f48f4..c204ecfc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1202,14 +1202,15 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) { URLConnection connection = url.openConnection(); - //Set Authorization Header configured globally for remoteServer Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); - if (serverAuth != null) serverAuth.setAuthorizationHeader(connection, context); - - //Override Authorization Header if configured locally - Auth2 auth = this.getAuth2(); - if(auth != null && !(auth instanceof NullAuth)) { - auth.setAuthorizationHeader(connection, context); + Auth2 overrideAuth = this.getAuth2(); + + if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { + //Override Authorization Header if configured locally + overrideAuth.setAuthorizationHeader(connection, context); + } else if (serverAuth != null) { + //Set Authorization Header configured globally for remoteServer + serverAuth.setAuthorizationHeader(connection, context); } return (HttpURLConnection)connection; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index b7b9b7c4..f21b5aa6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -201,7 +201,7 @@ public static Auth2Descriptor getDefaultAuth2Descriptor() { public RemoteJenkinsServer clone() throws CloneNotSupportedException { RemoteJenkinsServer clone = new RemoteJenkinsServer(); clone.address = address; - clone.auth2 = auth2.clone(); + clone.auth2 = (auth2 == null) ? null : auth2.clone(); clone.displayName = displayName; clone.hasBuildTokenRootSupport = hasBuildTokenRootSupport; return clone; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index 4903e458..48c09ffd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -23,7 +23,9 @@ public NoneAuth() { @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - connection.setRequestProperty("Authorization", null); + //TODO: Should remove potential existing header, but URLConnection does not provide means to do so. + // Setting null worked in the past, but is not valid with newer versions (of Jetty). + //connection.setRequestProperty("Authorization", null); } @Override diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly index 96b8c98d..b7ced704 100644 --- a/src/main/resources/index.jelly +++ b/src/main/resources/index.jelly @@ -1,3 +1,4 @@ +
This plugin triggers a job on a remote Jenkins host
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly index de8e2c30..12651bdc 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index 3b56ac58..a339c144 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly index d2274898..4fc4a84e 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/global.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index 13cd4a83..be38f83f 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly index fdb321dd..e9f47ae6 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly index 57910bb1..d6105e21 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly @@ -1,3 +1,4 @@ + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 3e3e99cb..2bfcb3c1 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -1,3 +1,4 @@ + From 36fbe334a4cb1ef929e4e2d5c7a6d57ef03cf67c Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 14 Mar 2018 13:25:08 +0100 Subject: [PATCH 043/262] Enum code conventions --- .../RemoteBuildConfiguration.java | 6 +++--- .../pipeline/RemoteBuildPipelineStep.java | 6 +++--- .../utils/FormValidationUtils.java | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index c204ecfc..13b5aaf9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1571,7 +1571,7 @@ public FormValidation doCheckJob( @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); - if(result.isAffected(AffectedField.jobNameOrUrl)) return result.formValidation; + if(result.isAffected(AffectedField.JOB_NAME_OR_URL)) return result.formValidation; return FormValidation.ok(); } @@ -1581,7 +1581,7 @@ public FormValidation doCheckRemoteJenkinsUrl( @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, @QueryParameter("job") final String job) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); - if(result.isAffected(AffectedField.remoteJenkinsUrl)) return result.formValidation; + if(result.isAffected(AffectedField.REMOTE_JENKINS_URL)) return result.formValidation; return FormValidation.ok(); } @@ -1591,7 +1591,7 @@ public FormValidation doCheckRemoteJenkinsName( @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @QueryParameter("job") final String job) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); - if(result.isAffected(AffectedField.remoteJenkinsName)) return result.formValidation; + if(result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) return result.formValidation; return FormValidation.ok(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index fd440b89..1272222b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -160,7 +160,7 @@ public FormValidation doCheckJob( @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); - if(result.isAffected(AffectedField.jobNameOrUrl)) return result.formValidation; + if(result.isAffected(AffectedField.JOB_NAME_OR_URL)) return result.formValidation; return FormValidation.ok(); } @@ -169,7 +169,7 @@ public FormValidation doCheckRemoteJenkinsUrl( @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, @QueryParameter("job") final String job) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); - if(result.isAffected(AffectedField.remoteJenkinsUrl)) return result.formValidation; + if(result.isAffected(AffectedField.REMOTE_JENKINS_URL)) return result.formValidation; return FormValidation.ok(); } @@ -178,7 +178,7 @@ public FormValidation doCheckRemoteJenkinsName( @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @QueryParameter("job") final String job) { RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); - if(result.isAffected(AffectedField.remoteJenkinsName)) return result.formValidation; + if(result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) return result.formValidation; return FormValidation.ok(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java index 6a4653f5..66aebfc9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -17,7 +17,7 @@ public class FormValidationUtils { public static enum AffectedField { - jobNameOrUrl, remoteJenkinsUrl, remoteJenkinsName + JOB_NAME_OR_URL, REMOTE_JENKINS_URL, REMOTE_JENKINS_NAME } public static class RemoteURLCombinationsResult { @@ -56,36 +56,36 @@ public static RemoteURLCombinationsResult checkRemoteURLCombinations(String remo if(isEmpty(jobNameOrUrl)) { return new RemoteURLCombinationsResult( FormValidation.error("'Remote Job Name or URL' ('job') not specified"), - AffectedField.jobNameOrUrl); + AffectedField.JOB_NAME_OR_URL); } else if(!isEmpty(remoteJenkinsUrl) && !isURL(remoteJenkinsUrl)) { return new RemoteURLCombinationsResult( FormValidation.error("Invalid URL in 'Override remote host URL' ('remoteJenkinsUrl')"), - AffectedField.remoteJenkinsUrl); + AffectedField.REMOTE_JENKINS_URL); } else if(!remoteUrl_setAndValidUrl && !remoteName_setAndValid && !job_setAndValidUrl) { //Root URL or full job URL not specified at all if(job_containsVariable) { - return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.JOB_NAME_OR_URL); } else { return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), - AffectedField.jobNameOrUrl, AffectedField.remoteJenkinsName, AffectedField.remoteJenkinsUrl); + AffectedField.JOB_NAME_OR_URL, AffectedField.REMOTE_JENKINS_NAME, AffectedField.REMOTE_JENKINS_URL); } } else if(job_setAndValidUrl) { return RemoteURLCombinationsResult.OK(); } else if(remoteUrl_setAndValidUrl && job_setAndNoUrl) { if(job_containsVariable) { - return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.JOB_NAME_OR_URL); } else { return RemoteURLCombinationsResult.OK(); } } else if(remoteName_setAndValid && job_setAndNoUrl) { if(job_containsVariable) { - return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.jobNameOrUrl); + return new RemoteURLCombinationsResult(FormValidation.warning(TEXT_WARNING_JOB_VARIABLE), AffectedField.JOB_NAME_OR_URL); } else { return RemoteURLCombinationsResult.OK(); } } else { return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), - AffectedField.jobNameOrUrl, AffectedField.remoteJenkinsName, AffectedField.remoteJenkinsUrl); + AffectedField.JOB_NAME_OR_URL, AffectedField.REMOTE_JENKINS_NAME, AffectedField.REMOTE_JENKINS_URL); } } From 628b6fccbb6484a59c720fd11f1518b0bdf3fc08 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 14 Mar 2018 14:23:35 +0100 Subject: [PATCH 044/262] Stopped ignoring findbugs & javadoc failures --- pom.xml | 7 ------- .../ParameterizedRemoteTrigger/RemoteJenkinsServer.java | 2 +- .../plugins/ParameterizedRemoteTrigger/auth2/Auth2.java | 4 +++- .../ParameterizedRemoteTrigger/auth2/CredentialsAuth.java | 2 +- .../plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java | 4 +++- .../plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java | 4 +++- .../ParameterizedRemoteTrigger/auth2/TokenAuth.java | 2 +- .../remoteJob/BuildInfoExporterAction.java | 4 ++-- 8 files changed, 14 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 0f1df920..90611b96 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,6 @@ 1.642.3 7 - false Parameterized-Remote-Trigger @@ -43,12 +42,6 @@ 3.0.0-SNAPSHOT
- - maven-javadoc-plugin - - false - - diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index f21b5aa6..7cc0b73f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -199,7 +199,7 @@ public static Auth2Descriptor getDefaultAuth2Descriptor() { @Override public RemoteJenkinsServer clone() throws CloneNotSupportedException { - RemoteJenkinsServer clone = new RemoteJenkinsServer(); + RemoteJenkinsServer clone = (RemoteJenkinsServer)super.clone(); clone.address = address; clone.auth2 = (auth2 == null) ? null : auth2.clone(); clone.displayName = displayName; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index 26fbdc27..d5ec7712 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -59,7 +59,9 @@ public static abstract class Auth2Descriptor extends Descriptor @Override - public abstract Auth2 clone() throws CloneNotSupportedException; + public Auth2 clone() throws CloneNotSupportedException { + return (Auth2)super.clone(); + }; @Override public abstract int hashCode(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 34adac08..f520fb86 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -165,7 +165,7 @@ public static ListBoxModel doFillCredentialsItems() { @Override public CredentialsAuth clone() throws CloneNotSupportedException { - CredentialsAuth clone = new CredentialsAuth(); + CredentialsAuth clone = (CredentialsAuth)super.clone(); clone.credentials = credentials; return clone; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index 48c09ffd..e9c1f989 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -7,6 +7,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.model.Item; @@ -53,7 +54,7 @@ public String getDisplayName() { @Override public NoneAuth clone() throws CloneNotSupportedException { - return new NoneAuth(); + return (NoneAuth)super.clone(); } @Override @@ -62,6 +63,7 @@ public int hashCode() { } @Override + @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { if (this == obj) return true; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 4245d758..6940436f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -7,6 +7,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.model.Item; @@ -52,7 +53,7 @@ public String getDisplayName() { @Override public NullAuth clone() throws CloneNotSupportedException { - return new NullAuth(); + return (NullAuth)super.clone(); } @Override @@ -61,6 +62,7 @@ public int hashCode() { } @Override + @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { if (this == obj) return true; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index bd3c7700..52a8c0a6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -79,7 +79,7 @@ public String getDisplayName() { @Override public TokenAuth clone() throws CloneNotSupportedException { - TokenAuth clone = new TokenAuth(); + TokenAuth clone = (TokenAuth)super.clone(); clone.apiToken = apiToken; clone.userName = userName; return clone; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index 92a0da31..a0988c00 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -185,8 +185,8 @@ private String getBuildNumbersString(List refs, String separator /** * Gets the unique set of project names that have a linked build.
* The later triggered jobs are later in the list. E.g.
- * C, A, B -> C, A, B
- * C, A, B, A, C -> B, A, C
+ * C, A, B -> C, A, B
+ * C, A, B, A, C -> B, A, C
* * @return Set of project names that have at least one build linked. */ From c4652e3ddd838059365c8433581291966df66fc3 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 15 Mar 2018 10:28:47 +0100 Subject: [PATCH 045/262] Formatting: Replaced \t by 4 blanks --- .../ParameterizedRemoteTrigger/Auth.java | 6 +- .../RemoteBuildConfiguration.java | 4 +- .../auth2/Auth2.java | 2 +- .../auth2/CredentialsAuth.java | 62 ++-- .../auth2/NoneAuth.java | 46 +-- .../auth2/NullAuth.java | 40 +-- .../auth2/TokenAuth.java | 76 ++--- .../pipeline/Handle.java | 2 +- .../remoteJob/BuildInfoExporterAction.java | 14 +- .../RemoteJenkinsServerTest.java | 34 +- .../auth2/Auth2Test.java | 98 +++--- .../BuildInfoExporterActionTest.java | 308 +++++++++--------- 12 files changed, 346 insertions(+), 346 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index 77424db2..efc871a4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -39,8 +39,8 @@ public class Auth extends AbstractDescribableImpl implements Serializable public static final String API_TOKEN = "apiToken"; public static final String CREDENTIALS_PLUGIN = "credentialsPlugin"; - private static final Auth2 NONE_AUTH = new NoneAuth(); - private static final Auth2 NULL_AUTH = new NullAuth(); + private static final Auth2 NONE_AUTH = new NoneAuth(); + private static final Auth2 NULL_AUTH = new NullAuth(); private final String authType; private final String username; @@ -77,7 +77,7 @@ public Boolean isMatch(String value) { public String getUser(){ if (authType.equals(API_TOKEN)){ - return username; + return username; } else if (authType.equals(CREDENTIALS_PLUGIN)){ UsernamePasswordCredentials creds = getCredentials(); return creds != null ? creds.getUsername() : ""; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 13b5aaf9..9b7c06c3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1207,10 +1207,10 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { //Override Authorization Header if configured locally - overrideAuth.setAuthorizationHeader(connection, context); + overrideAuth.setAuthorizationHeader(connection, context); } else if (serverAuth != null) { //Set Authorization Header configured globally for remoteServer - serverAuth.setAuthorizationHeader(connection, context); + serverAuth.setAuthorizationHeader(connection, context); } return (HttpURLConnection)connection; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index d5ec7712..b561f243 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -60,7 +60,7 @@ public static abstract class Auth2Descriptor extends Descriptor @Override public Auth2 clone() throws CloneNotSupportedException { - return (Auth2)super.clone(); + return (Auth2)super.clone(); }; @Override diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index f520fb86..6491dd7a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -163,36 +163,36 @@ public static ListBoxModel doFillCredentialsItems() { - @Override - public CredentialsAuth clone() throws CloneNotSupportedException { - CredentialsAuth clone = (CredentialsAuth)super.clone(); - clone.credentials = credentials; - return clone; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((credentials == null) ? 0 : credentials.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - CredentialsAuth other = (CredentialsAuth) obj; - if (credentials == null) { - if (other.credentials != null) - return false; - } else if (!credentials.equals(other.credentials)) - return false; - return true; - } + @Override + public CredentialsAuth clone() throws CloneNotSupportedException { + CredentialsAuth clone = (CredentialsAuth)super.clone(); + clone.credentials = credentials; + return clone; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((credentials == null) ? 0 : credentials.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!this.getClass().isInstance(obj)) + return false; + CredentialsAuth other = (CredentialsAuth) obj; + if (credentials == null) { + if (other.credentials != null) + return false; + } else if (!credentials.equals(other.credentials)) + return false; + return true; + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index e9c1f989..6a518b82 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -24,9 +24,9 @@ public NoneAuth() { @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - //TODO: Should remove potential existing header, but URLConnection does not provide means to do so. - // Setting null worked in the past, but is not valid with newer versions (of Jetty). - //connection.setRequestProperty("Authorization", null); + //TODO: Should remove potential existing header, but URLConnection does not provide means to do so. + // Setting null worked in the past, but is not valid with newer versions (of Jetty). + //connection.setRequestProperty("Authorization", null); } @Override @@ -52,26 +52,26 @@ public String getDisplayName() { } } - @Override - public NoneAuth clone() throws CloneNotSupportedException { - return (NoneAuth)super.clone(); - } + @Override + public NoneAuth clone() throws CloneNotSupportedException { + return (NoneAuth)super.clone(); + } - @Override - public int hashCode() { - return "NoneAuth".hashCode(); - } - - @Override - @SuppressFBWarnings("EQ_UNUSUAL") - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - return true; - } + @Override + public int hashCode() { + return "NoneAuth".hashCode(); + } + + @Override + @SuppressFBWarnings("EQ_UNUSUAL") + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!this.getClass().isInstance(obj)) + return false; + return true; + } } \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 6940436f..6fe7d9c5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -51,26 +51,26 @@ public String getDisplayName() { } - @Override - public NullAuth clone() throws CloneNotSupportedException { - return (NullAuth)super.clone(); - } + @Override + public NullAuth clone() throws CloneNotSupportedException { + return (NullAuth)super.clone(); + } - @Override - public int hashCode() { - return "NullAuth".hashCode(); - } - - @Override - @SuppressFBWarnings("EQ_UNUSUAL") - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - return true; - } + @Override + public int hashCode() { + return "NullAuth".hashCode(); + } + + @Override + @SuppressFBWarnings("EQ_UNUSUAL") + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!this.getClass().isInstance(obj)) + return false; + return true; + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index 52a8c0a6..c86f9201 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -77,43 +77,43 @@ public String getDisplayName() { } } - @Override - public TokenAuth clone() throws CloneNotSupportedException { - TokenAuth clone = (TokenAuth)super.clone(); - clone.apiToken = apiToken; - clone.userName = userName; - return clone; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((apiToken == null) ? 0 : apiToken.hashCode()); - result = prime * result + ((userName == null) ? 0 : userName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - TokenAuth other = (TokenAuth) obj; - if (apiToken == null) { - if (other.apiToken != null) - return false; - } else if (!apiToken.equals(other.apiToken)) - return false; - if (userName == null) { - if (other.userName != null) - return false; - } else if (!userName.equals(other.userName)) - return false; - return true; - } + @Override + public TokenAuth clone() throws CloneNotSupportedException { + TokenAuth clone = (TokenAuth)super.clone(); + clone.apiToken = apiToken; + clone.userName = userName; + return clone; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((apiToken == null) ? 0 : apiToken.hashCode()); + result = prime * result + ((userName == null) ? 0 : userName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!this.getClass().isInstance(obj)) + return false; + TokenAuth other = (TokenAuth) obj; + if (apiToken == null) { + if (other.apiToken != null) + return false; + } else if (!apiToken.equals(other.apiToken)) + return false; + if (userName == null) { + if (other.userName != null) + return false; + } else if (!userName.equals(other.userName)) + return false; + return true; + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index b051646c..0af19e0b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -63,7 +63,7 @@ public class Handle implements Serializable { */ private String lastLog; - + public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, @Nonnull String currentItem, @Nonnull RemoteJenkinsServer effectiveRemoteServer) { this.remoteBuildConfiguration = remoteBuildConfiguration; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index a0988c00..72396870 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -38,13 +38,13 @@ public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); - for (int i = 1; i <= PARALLEL_JOBS; i++) { - BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); - } - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - EnvVars env = new EnvVars(); - action.buildEnvVars(null, env); - checkEnv(env); - } - - /** - * We had ConcurrentModificationExceptions in the past. This test executes {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} - * and {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} in parallel to provoke a ConcurrentModificationException (which should not occur anymore). - */ - @Test - public void testAddBuildInfoExporterAction_parallel() throws IOException, InterruptedException, ExecutionException { - Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); - ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE); - - //Start parallel threads adding BuildInfoExporterActions AND one thread reading in parallel - Future[] addFutures = new Future[PARALLEL_JOBS]; - for (int i = 1; i <= PARALLEL_JOBS; i++) { - addFutures[i-1] = executor.submit(new AddActionCallable(parentBuild, i)); - } - Future envFuture = executor.submit(new BuildEnvVarsCallable(parentBuild)); - - //Wait until all finished - while(!isDone(addFutures) && !envFuture.isDone()) sleep(100); - - //Check result - EnvVars env = (EnvVars)envFuture.get(); - checkEnv(env); - } - - - /** - * Sleeps millis millisseconds and swallows any InterruptedExceptions. - * @param millis - */ - private void sleep(int millis) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - /** - * Checks if all futures are done. Additionally calls {@link Future#get()} to check if an Exception occured. - * @param addFutures - * @return - * @throws InterruptedException - * @throws ExecutionException - */ - private boolean isDone(Future[] addFutures) throws InterruptedException, ExecutionException { - boolean done = true; - for(Future addFuture : addFutures) { - if(!addFuture.isDone()) { - done = false; - } else { - //Test get to check for exceptions - addFuture.get(); - } - } - return done; - } - - /** - * Checks if the env contains all expected variables - * @param env - */ - private void checkEnv(EnvVars env) { - for(int i = 1; i <= PARALLEL_JOBS; i++) { - Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); - Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); - Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job"+i)); - Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i)); - Assert.assertEquals("TRIGGERED_BUILD_RUN_COUNT_Job"+i, "1", env.get("TRIGGERED_BUILD_RUN_COUNT_Job"+i)); - Assert.assertEquals("TRIGGERED_BUILD_URL_Job"+i, "http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); - } - } - - /** - * Calls {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. - * This Callable is typically executed multiple tiles in parallel to provoke a ConcurrentModificationException (which should not occur anymore). - */ - private static class AddActionCallable implements Callable { - Run parentBuild; - private int i; - - public AddActionCallable(Run parentBuild, int i) { - this.parentBuild = parentBuild; - this.i = i; - } - - public Boolean call() throws MalformedURLException { - String jobName = "Job" + i; - BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, - new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); - System.out.println("AddActionCallable finished for Job" + i); - - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - Set projectsWithBuilds = action.getProjectsWithBuilds(); - boolean success = projectsWithBuilds.contains(jobName); - String message = String.format("AddActionCallable %s for %s (projects in list: %s)", - (success ? "was successful " : "failed"), "Job"+i, projectsWithBuilds.size()) ; - System.out.println(message); - if(!success) Assert.fail(message); - return success; - } - } - - /** - * Calls {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} repeatedly until all AddActionCallables finished. - * This way we try to provoke a ConcurrentModificationException (which should not occur anymore). - */ - private static class BuildEnvVarsCallable implements Callable { - Run parentBuild; - - public BuildEnvVarsCallable(Run parentBuild) { - this.parentBuild = parentBuild; - } - - public EnvVars call() throws MalformedURLException, InterruptedException, TimeoutException { - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - EnvVars env = new EnvVars(); - long startTime = System.currentTimeMillis(); - while (action == null || action.getProjectsWithBuilds().size() < PARALLEL_JOBS) { - try { - Thread.sleep(100); - } catch (InterruptedException e) {} - action = parentBuild.getAction(BuildInfoExporterAction.class); - if (action != null) { - //Provoke ConcurrentModificationException - action.buildEnvVars(null, env); - } - if(System.currentTimeMillis() - startTime > 120000) throw new TimeoutException("Only " + action.getProjectsWithBuilds().size() + " of " + PARALLEL_JOBS + " jobs"); - } - action.buildEnvVars(null, env); - return env; - } - } + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private final static int PARALLEL_JOBS = 100; + private final static int POOL_SIZE = 50; + + /** + * Same as {@link #testAddBuildInfoExporterAction_parallel()} but sequentially. + * @throws IOException + */ + @Test + public void testAddBuildInfoExporterAction_sequential() throws IOException { + Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); + for (int i = 1; i <= PARALLEL_JOBS; i++) { + BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + } + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + EnvVars env = new EnvVars(); + action.buildEnvVars(null, env); + checkEnv(env); + } + + /** + * We had ConcurrentModificationExceptions in the past. This test executes {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} + * and {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} in parallel to provoke a ConcurrentModificationException (which should not occur anymore). + */ + @Test + public void testAddBuildInfoExporterAction_parallel() throws IOException, InterruptedException, ExecutionException { + Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); + ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE); + + //Start parallel threads adding BuildInfoExporterActions AND one thread reading in parallel + Future[] addFutures = new Future[PARALLEL_JOBS]; + for (int i = 1; i <= PARALLEL_JOBS; i++) { + addFutures[i-1] = executor.submit(new AddActionCallable(parentBuild, i)); + } + Future envFuture = executor.submit(new BuildEnvVarsCallable(parentBuild)); + + //Wait until all finished + while(!isDone(addFutures) && !envFuture.isDone()) sleep(100); + + //Check result + EnvVars env = (EnvVars)envFuture.get(); + checkEnv(env); + } + + + /** + * Sleeps millis millisseconds and swallows any InterruptedExceptions. + * @param millis + */ + private void sleep(int millis) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + /** + * Checks if all futures are done. Additionally calls {@link Future#get()} to check if an Exception occured. + * @param addFutures + * @return + * @throws InterruptedException + * @throws ExecutionException + */ + private boolean isDone(Future[] addFutures) throws InterruptedException, ExecutionException { + boolean done = true; + for(Future addFuture : addFutures) { + if(!addFuture.isDone()) { + done = false; + } else { + //Test get to check for exceptions + addFuture.get(); + } + } + return done; + } + + /** + * Checks if the env contains all expected variables + * @param env + */ + private void checkEnv(EnvVars env) { + for(int i = 1; i <= PARALLEL_JOBS; i++) { + Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_NUMBERS_Job"+i, ""+i, env.get("TRIGGERED_BUILD_NUMBERS_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i, "SUCCESS", env.get("TRIGGERED_BUILD_RESULT_Job" + i + "_RUN_"+i)); + Assert.assertEquals("TRIGGERED_BUILD_RUN_COUNT_Job"+i, "1", env.get("TRIGGERED_BUILD_RUN_COUNT_Job"+i)); + Assert.assertEquals("TRIGGERED_BUILD_URL_Job"+i, "http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); + } + } + + /** + * Calls {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. + * This Callable is typically executed multiple tiles in parallel to provoke a ConcurrentModificationException (which should not occur anymore). + */ + private static class AddActionCallable implements Callable { + Run parentBuild; + private int i; + + public AddActionCallable(Run parentBuild, int i) { + this.parentBuild = parentBuild; + this.i = i; + } + + public Boolean call() throws MalformedURLException { + String jobName = "Job" + i; + BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, + new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + System.out.println("AddActionCallable finished for Job" + i); + + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + Set projectsWithBuilds = action.getProjectsWithBuilds(); + boolean success = projectsWithBuilds.contains(jobName); + String message = String.format("AddActionCallable %s for %s (projects in list: %s)", + (success ? "was successful " : "failed"), "Job"+i, projectsWithBuilds.size()) ; + System.out.println(message); + if(!success) Assert.fail(message); + return success; + } + } + + /** + * Calls {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} repeatedly until all AddActionCallables finished. + * This way we try to provoke a ConcurrentModificationException (which should not occur anymore). + */ + private static class BuildEnvVarsCallable implements Callable { + Run parentBuild; + + public BuildEnvVarsCallable(Run parentBuild) { + this.parentBuild = parentBuild; + } + + public EnvVars call() throws MalformedURLException, InterruptedException, TimeoutException { + BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + EnvVars env = new EnvVars(); + long startTime = System.currentTimeMillis(); + while (action == null || action.getProjectsWithBuilds().size() < PARALLEL_JOBS) { + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + action = parentBuild.getAction(BuildInfoExporterAction.class); + if (action != null) { + //Provoke ConcurrentModificationException + action.buildEnvVars(null, env); + } + if(System.currentTimeMillis() - startTime > 120000) throw new TimeoutException("Only " + action.getProjectsWithBuilds().size() + " of " + PARALLEL_JOBS + " jobs"); + } + action.buildEnvVars(null, env); + return env; + } + } } From bbe975e09eda0efd1d3b87e39e8bef6830bc7de9 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 15 Mar 2018 10:30:19 +0100 Subject: [PATCH 046/262] Formatting: removed trailing blanks --- .../ParameterizedRemoteTrigger/BuildContext.java | 4 ++-- .../ParameterizedRemoteTrigger/JenkinsCrumb.java | 2 +- .../RemoteBuildConfiguration.java | 10 +++++----- .../RemoteJenkinsServer.java | 10 +++++----- .../ParameterizedRemoteTrigger/auth2/Auth2.java | 2 +- .../auth2/CredentialsAuth.java | 4 ++-- .../ParameterizedRemoteTrigger/auth2/NoneAuth.java | 2 +- .../ParameterizedRemoteTrigger/auth2/NullAuth.java | 4 ++-- .../exceptions/CredentialsNotFoundException.java | 4 ++-- .../exceptions/ForbiddenException.java | 2 +- .../ParameterizedRemoteTrigger/pipeline/Handle.java | 6 +++--- .../pipeline/PrintStreamWrapper.java | 4 ++-- .../pipeline/RemoteBuildPipelineStep.java | 2 +- .../remoteJob/BuildInfoExporterAction.java | 2 +- .../remoteJob/BuildStatus.java | 10 +++++----- .../utils/FormValidationUtils.java | 12 ++++++------ .../RemoteBuildConfigurationTest.java | 2 +- .../RemoteJenkinsServerTest.java | 12 ++++++------ .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 4 ++-- .../pipeline/HandleTest.java | 4 ++-- .../remoteJob/BuildInfoExporterActionTest.java | 10 +++++----- 21 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java index e141a9e0..bafd71ae 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -41,7 +41,7 @@ public class BuildContext @Nullable public RemoteJenkinsServer effectiveRemoteServer; - + /** * The current Item (job, pipeline,...) where the plugin is used from. * @@ -92,5 +92,5 @@ private String getCurrentItem(Run run, String currentItem) throw new IllegalArgumentException("Both null, Run and Current Item!"); } } - + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java index 1ccff782..ff773e9c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java @@ -54,7 +54,7 @@ public String getCrumbValue() } /** - * @return true if CSRF is enabled on the remote Jenkins, false otherwise. + * @return true if CSRF is enabled on the remote Jenkins, false otherwise. */ public boolean isEnabledOnRemote() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 9b7c06c3..5cf98015 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -802,7 +802,7 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getRemoteAddress(), queueId); ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); - JSONObject queueResponse = response.getBody(); + JSONObject queueResponse = response.getBody(); if (queueResponse == null || queueResponse.isNullObject()) { throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", response.getResponseCode(), queueQuery)); @@ -1045,11 +1045,11 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu throw new ForbiddenException(url); } else { String response = trimToNull(readInputStream(connection)); - + // JSONSerializer serializer = new JSONSerializer(); // need to parse the data we get back into struct //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); - + //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. //But in newer versions of Jenkins, it just returns an empty response. @@ -1113,7 +1113,7 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu /** * For POST requests a crumb is needed. This methods gets a crumb and sets it in the header. * https://wiki.jenkins.io/display/JENKINS/Remote+access+API#RemoteaccessAPI-CSRFProtection - * + * * @param connection * @param context * @throws IOException @@ -1369,7 +1369,7 @@ public boolean getBlockBuildUntilComplete() { public String getJob() { return job; } - + /** * @return job value with expanded env vars. * @throws IOException diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 7cc0b73f..44e5bba2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -26,9 +26,9 @@ /** * Holds everything regarding the remote server we wish to connect to, including validations and what not. - * + * * @author Maurice W. - * + * */ public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable { @@ -101,13 +101,13 @@ public Auth2 getAuth2() { } /** - * Migrates old Auth to Auth2 if necessary. + * Migrates old Auth to Auth2 if necessary. * @deprecated since 2.3.0-SNAPSHOT - get rid once all users migrated */ private void migrateAuthToAuth2() { if(auth2 == null) { if(auth == null || auth.size() <= 0) { - auth2 = new NoneAuth(); + auth2 = new NoneAuth(); } else { auth2 = Auth.authToAuth2(auth); } @@ -153,7 +153,7 @@ public String getDisplayName() { /** * Validates the given address to see that it's well-formed, and is reachable. - * + * * @param address * Remote address to be validated * @return FormValidation object diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index b561f243..1ed76d0c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -52,7 +52,7 @@ public static abstract class Auth2Descriptor extends Descriptor * the Item (Job, Pipeline,...) we are currently running in. * The item is required to also get Credentials which are defined in the items scope and not Jenkins globally. * Value can be null, but Credentials e.g. configured on a Folder will not be found in this case, - * only globally configured Credentials. + * only globally configured Credentials. * @return a string representing the authorization. */ public abstract String toString(Item item); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 6491dd7a..33ebfc9c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -161,8 +161,8 @@ public static ListBoxModel doFillCredentialsItems() { } } - - + + @Override public CredentialsAuth clone() throws CloneNotSupportedException { CredentialsAuth clone = (CredentialsAuth)super.clone(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index 6a518b82..b3ff9470 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -61,7 +61,7 @@ public NoneAuth clone() throws CloneNotSupportedException { public int hashCode() { return "NoneAuth".hashCode(); } - + @Override @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 6fe7d9c5..fb1f1c08 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -50,7 +50,7 @@ public String getDisplayName() { } } - + @Override public NullAuth clone() throws CloneNotSupportedException { return (NullAuth)super.clone(); @@ -60,7 +60,7 @@ public NullAuth clone() throws CloneNotSupportedException { public int hashCode() { return "NullAuth".hashCode(); } - + @Override @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java index bb2ceba5..9b61e7c7 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java @@ -4,7 +4,7 @@ public class CredentialsNotFoundException extends IOException { - + private static final long serialVersionUID = -2489306184948013529L; private String credentialsId; @@ -18,5 +18,5 @@ public String getMessage() { return "No Jenkins Credentials found with ID '" + credentialsId + "'"; } - + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java index 19b0a382..4960a034 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ForbiddenException.java @@ -19,5 +19,5 @@ public String getMessage() { return "Server returned 403 - Forbidden. User does not have enough permissions for this request: " + url; } - + } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 0af19e0b..d55276d6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -54,7 +54,7 @@ public class Handle implements Serializable { */ private final String currentItem; - /* + /* * The latest log entries from the last called method. * Unfortunately the TaskListener.getLogger() from the StepContext does * not write to the pipeline log anymore since the RemoteBuildPipelineStep @@ -305,7 +305,7 @@ public void setBuildData(BuildData buildData) @Override public String toString() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); @@ -383,7 +383,7 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, public void setJobMetadata(JSONObject remoteJobMetadata) { - this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); + this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); this.jobFullDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "fullDisplayName"); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java index 4e19081a..c640e3e2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/PrintStreamWrapper.java @@ -11,7 +11,7 @@ * Wrapper to provide a PrintStream for writing content to * and a corresponding getContent() method to get the content * which has been written to the PrintStream. - * + * * The reason is from the async Pipeline Handle we don't have * an active TaskListener.getLogger() anymore this means everything * written to the PrintStream (logger) will not be printed to the Pipeline log. @@ -43,7 +43,7 @@ public PrintStream getPrintStream() { public String getContent() throws IOException { String string = byteStream.toString("UTF-8"); close(); - return string; + return string; } /** diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 1272222b..e16d870b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -223,7 +223,7 @@ public String getRemoteJenkinsName() { public String getRemoteJenkinsUrl() { return remoteBuildConfig.getRemoteJenkinsUrl(); } - + public String getJob() { return remoteBuildConfig.getJob(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index 72396870..d2c7ad5d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -186,7 +186,7 @@ private String getBuildNumbersString(List refs, String separator * Gets the unique set of project names that have a linked build.
* The later triggered jobs are later in the list. E.g.
* C, A, B -> C, A, B
- * C, A, B, A, C -> B, A, C
+ * C, A, B, A, C -> B, A, C
* * @return Set of project names that have at least one build linked. */ diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java index ab39a4c2..b80b4edc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java @@ -32,27 +32,27 @@ public enum BuildStatus RUNNING("RUNNING"), /** - * Status corresponding to the Jenkins Result.ABORTED + * Status corresponding to the Jenkins Result.ABORTED */ ABORTED(Result.ABORTED), /** - * Status corresponding to the Jenkins Result.FAILURE + * Status corresponding to the Jenkins Result.FAILURE */ FAILURE(Result.FAILURE), /** - * Status corresponding to the Jenkins Result.NOT_BUILT + * Status corresponding to the Jenkins Result.NOT_BUILT */ NOT_BUILT(Result.NOT_BUILT), /** - * Status corresponding to the Jenkins Result.SUCCESS + * Status corresponding to the Jenkins Result.SUCCESS */ SUCCESS(Result.SUCCESS), /** - * Status corresponding to the Jenkins Result.UNSTABLE + * Status corresponding to the Jenkins Result.UNSTABLE */ UNSTABLE(Result.UNSTABLE); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java index 66aebfc9..6aa46c6b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -29,7 +29,7 @@ public RemoteURLCombinationsResult(FormValidation formValidation, AffectedField. this.formValidation = formValidation; this.affectedFields = affectedFields; } - + public boolean isAffected(AffectedField field) { return Arrays.asList(affectedFields).contains(field); } @@ -38,7 +38,7 @@ public static RemoteURLCombinationsResult OK() { return new RemoteURLCombinationsResult(FormValidation.ok(), AffectedField.values()); } - + } public static RemoteURLCombinationsResult checkRemoteURLCombinations(String remoteJenkinsUrl, String remoteJenkinsName, String jobNameOrUrl) { @@ -52,9 +52,9 @@ public static RemoteURLCombinationsResult checkRemoteURLCombinations(String remo boolean job_containsVariable = isEmpty(jobNameOrUrl) ? false : jobNameOrUrl.indexOf("$") >= 0; final String TEXT_WARNING_JOB_VARIABLE = "You are using a variable in the 'Remote Job Name or URL' ('job') field. You have to make sure the value at runtime results in the full job URL"; final String TEXT_ERROR_NO_URL_AT_ALL = "You have to configure either 'Select a remote host' ('remoteJenkinsName'), 'Override remote host URL' ('remoteJenkinsUrl') or specify a full job URL 'Remote Job Name or URL' ('job')"; - + if(isEmpty(jobNameOrUrl)) { - return new RemoteURLCombinationsResult( + return new RemoteURLCombinationsResult( FormValidation.error("'Remote Job Name or URL' ('job') not specified"), AffectedField.JOB_NAME_OR_URL); } else if(!isEmpty(remoteJenkinsUrl) && !isURL(remoteJenkinsUrl)) { @@ -100,10 +100,10 @@ public static boolean isURL(String string) { if(isEmpty(trimToNull(string))) return false; String stringLower = string.toLowerCase(); if(stringLower.startsWith("http://") || stringLower.startsWith("https://")) { - if(stringLower.indexOf("://") >= stringLower.length()-3) { + if(stringLower.indexOf("://") >= stringLower.length()-3) { return false; //URL ends after protocol } - if(stringLower.indexOf("$") >= 0) { + if(stringLower.indexOf("$") >= 0) { return false; //We interpret $ in URLs as variables which need to be replaced. TODO: What about URI standard which allows $? } try { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 8e690293..9b5c0dce 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -198,7 +198,7 @@ public void testFindEffectiveRemoteHost_withoutJob() throws IOException, NoSuchF } catch(AbortException e) { assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); } - } + } @Test @WithoutJenkins public void testRemoteUrlOverridesRemoteName() throws IOException { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index 2a68f83f..4b12bac4 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -20,19 +20,19 @@ public class RemoteJenkinsServerTest { private final static String ADDRESS = "http://www.example.org:8443"; private final static String DISPLAY_NAME = "My example server."; private final static boolean HAS_BUILD_TOKEN_ROOT_SUPPORT = true; - + @Test public void testCloneBehaviour() throws Exception { TokenAuth auth = new TokenAuth(); auth.setApiToken(TOKEN); auth.setUserName(USER); - + RemoteJenkinsServer server = new RemoteJenkinsServer(); server.setAddress(ADDRESS); server.setDisplayName(DISPLAY_NAME); server.setAuth2(auth); server.setHasBuildTokenRootSupport(HAS_BUILD_TOKEN_ROOT_SUPPORT); - + RemoteJenkinsServer clone = server.clone(); //Test if still equal after cloning @@ -46,7 +46,7 @@ public void testCloneBehaviour() throws Exception { assertEquals("displayName", server.getDisplayName(), clone.getDisplayName()); assertEquals("hasBuildTokenRootSupport", HAS_BUILD_TOKEN_ROOT_SUPPORT, clone.getHasBuildTokenRootSupport()); assertEquals("hasBuildTokenRootSupport", server.getHasBuildTokenRootSupport(), clone.getHasBuildTokenRootSupport()); - + //Test if original object affected by clone modifications clone.setAddress("http://www.changed.org:8443"); clone.setDisplayName("Changed"); @@ -55,7 +55,7 @@ public void testCloneBehaviour() throws Exception { assertEquals("address", ADDRESS, server.getAddress()); assertEquals("displayName", DISPLAY_NAME, server.getDisplayName()); assertEquals("hasBuildTokenRootSupport", HAS_BUILD_TOKEN_ROOT_SUPPORT, server.getHasBuildTokenRootSupport()); - + //Test if clone is deep-copy or if server fields can be modified TokenAuth cloneAuth = (TokenAuth)clone.getAuth2(); cloneAuth.setApiToken("changed"); @@ -63,7 +63,7 @@ public void testCloneBehaviour() throws Exception { TokenAuth serverAuth = (TokenAuth)server.getAuth2(); assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken()); assertEquals("auth.userName", USER, serverAuth.getUserName()); - + //Test if clone.setAuth() affects original object CredentialsAuth credAuth = new CredentialsAuth(); clone.setAuth2(credAuth); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index e1fc2751..25318eb8 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -15,7 +15,7 @@ public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedExceptio original.setCredentials("original"); CredentialsAuth clone = original.clone(); verifyEqualsHashCode(original, clone); - + //Test changing clone clone.setCredentials("changed"); verifyEqualsHashCode(original, clone, false); @@ -30,7 +30,7 @@ public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { original.setUserName("original"); TokenAuth clone = original.clone(); verifyEqualsHashCode(original, clone); - + //Test changing clone clone.setApiToken("changed"); clone.setUserName("changed"); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java index eec75e0f..7a2b9aca 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java @@ -27,6 +27,6 @@ private void assertContains(String help, boolean assertIsContained, String check else assertFalse("Help contains '" + checkString + "': \"" + help + "\"", help.contains(checkString)); } - - + + } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 1f0f392c..11d58f9e 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -58,15 +58,15 @@ public void testAddBuildInfoExporterAction_parallel() throws IOException, Interr ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE); //Start parallel threads adding BuildInfoExporterActions AND one thread reading in parallel - Future[] addFutures = new Future[PARALLEL_JOBS]; + Future[] addFutures = new Future[PARALLEL_JOBS]; for (int i = 1; i <= PARALLEL_JOBS; i++) { addFutures[i-1] = executor.submit(new AddActionCallable(parentBuild, i)); } Future envFuture = executor.submit(new BuildEnvVarsCallable(parentBuild)); - + //Wait until all finished while(!isDone(addFutures) && !envFuture.isDone()) sleep(100); - + //Check result EnvVars env = (EnvVars)envFuture.get(); checkEnv(env); @@ -104,7 +104,7 @@ private boolean isDone(Future[] addFutures) throws InterruptedException, Exec } return done; } - + /** * Checks if the env contains all expected variables * @param env @@ -119,7 +119,7 @@ private void checkEnv(EnvVars env) { Assert.assertEquals("TRIGGERED_BUILD_URL_Job"+i, "http://jenkins/jobs/Job"+i, env.get("TRIGGERED_BUILD_URL_Job"+i)); } } - + /** * Calls {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. * This Callable is typically executed multiple tiles in parallel to provoke a ConcurrentModificationException (which should not occur anymore). From cdd38ff6bca130343d1fe8e9845a7d2a3d00cb17 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 20 Mar 2018 10:24:41 +0100 Subject: [PATCH 047/262] Switched Auth compatibility to readResolve() --- .../RemoteBuildConfiguration.java | 46 ++++++++----------- .../RemoteJenkinsServer.java | 36 ++++++++------- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 5cf98015..4e95da2d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -32,6 +32,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; @@ -112,22 +113,30 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep @DataBoundConstructor public RemoteBuildConfiguration() { - remoteJenkinsName = null; - remoteJenkinsUrl = null; - auth = null; - auth2 = new NullAuth(); - shouldNotFailBuild = false; - preventRemoteBuildQueue = false; pollInterval = DEFAULT_POLLINTERVALL; - blockBuildUntilComplete = false; - job = null; token = ""; parameters = ""; - enhancedLogging = false; - loadParamsFromFile = false; parameterFile = ""; } + /** + * see https://wiki.jenkins.io/display/JENKINS/Hint+on+retaining+backward+compatibility + */ + @SuppressWarnings("deprecation") + protected Object readResolve() { + //migrate Auth To Auth2 + if(auth2 == null) { + if(auth == null || auth.size() <= 0) { + auth2 = new NullAuth(); + } else { + auth2 = Auth.authToAuth2(auth); + } + } + auth = null; + return this; + } + + @DataBoundSetter public void setRemoteJenkinsName(String remoteJenkinsName) { @@ -1317,6 +1326,7 @@ public boolean getOverrideAuth() { } /** + * TODO: Remove - only used in tests * @return the list of authorizations. * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. */ @@ -1328,25 +1338,9 @@ public List getAuth(){ } public Auth2 getAuth2() { - migrateAuthToAuth2(); return this.auth2; } - /** - * Migrates old Auth to Auth2 if necessary. - * @deprecated since 2.3.0-SNAPSHOT - get rid once all users migrated - */ - private void migrateAuthToAuth2() { - if(auth2 == null) { - if(auth == null || auth.size() <= 0) { - auth2 = new NullAuth(); - } else { - auth2 = Auth.authToAuth2(auth); - } - } - auth = null; - } - public boolean getShouldNotFailBuild() { return shouldNotFailBuild; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 44e5bba2..d4adef11 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -37,7 +37,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl auth; + private transient List auth; @CheckForNull private String displayName; @@ -51,6 +51,24 @@ public class RemoteJenkinsServer extends AbstractDescribableImplAuth to Auth2 if necessary. - * @deprecated since 2.3.0-SNAPSHOT - get rid once all users migrated - */ - private void migrateAuthToAuth2() { - if(auth2 == null) { - if(auth == null || auth.size() <= 0) { - auth2 = new NoneAuth(); - } else { - auth2 = Auth.authToAuth2(auth); - } - } - auth = null; - } - @CheckForNull public String getAddress() { return address; From 7c8e3b19b7f47266115ac47642c086688f6d6cf7 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 20 Mar 2018 11:31:07 +0100 Subject: [PATCH 048/262] fixed tests & javadoc checks --- .../plugins/ParameterizedRemoteTrigger/Auth.java | 9 +++------ .../RemoteBuildConfiguration.java | 6 +++--- .../ParameterizedRemoteTrigger/RemoteJenkinsServer.java | 9 +++++---- .../ParameterizedRemoteTrigger/auth2/NoneAuth.java | 3 +++ .../ParameterizedRemoteTrigger/auth2/NullAuth.java | 8 +++++--- .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 4 ++-- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index efc871a4..d7ae9ce3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -39,9 +39,6 @@ public class Auth extends AbstractDescribableImpl implements Serializable public static final String API_TOKEN = "apiToken"; public static final String CREDENTIALS_PLUGIN = "credentialsPlugin"; - private static final Auth2 NONE_AUTH = new NoneAuth(); - private static final Auth2 NULL_AUTH = new NullAuth(); - private final String authType; private final String username; private final String apiToken; @@ -181,14 +178,14 @@ public static Auth auth2ToAuth(Auth2 auth) { } public static Auth2 authToAuth2(List oldAuth) { - if(oldAuth == null || oldAuth.size() <= 0) return new NullAuth(); + if(oldAuth == null || oldAuth.size() <= 0) return NullAuth.INSTANCE; return authToAuth2(oldAuth.get(0)); } public static Auth2 authToAuth2(Auth oldAuth) { String authType = oldAuth.getAuthType(); if (Auth.NONE.equals(authType)) { - return NONE_AUTH; + return NoneAuth.INSTANCE; } else if (Auth.API_TOKEN.equals(authType)) { TokenAuth newAuth = new TokenAuth(); newAuth.setUserName(oldAuth.getUsername()); @@ -199,7 +196,7 @@ public static Auth2 authToAuth2(Auth oldAuth) { newAuth.setCredentials(oldAuth.getCreds()); return newAuth; } else { - return NULL_AUTH; + return NullAuth.INSTANCE; } } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 4e95da2d..04b435b1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -119,7 +119,7 @@ public RemoteBuildConfiguration() { parameterFile = ""; } - /** + /* * see https://wiki.jenkins.io/display/JENKINS/Hint+on+retaining+backward+compatibility */ @SuppressWarnings("deprecation") @@ -127,7 +127,7 @@ protected Object readResolve() { //migrate Auth To Auth2 if(auth2 == null) { if(auth == null || auth.size() <= 0) { - auth2 = new NullAuth(); + auth2 = NullAuth.INSTANCE; } else { auth2 = Auth.authToAuth2(auth); } @@ -1338,7 +1338,7 @@ public List getAuth(){ } public Auth2 getAuth2() { - return this.auth2; + return (auth2 != null) ? auth2 : NullAuth.INSTANCE; } public boolean getShouldNotFailBuild() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index d4adef11..ec34943d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -13,6 +13,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; @@ -51,7 +52,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl Date: Tue, 20 Mar 2018 14:51:43 +0100 Subject: [PATCH 049/262] Introduced DEFAULT_AUTH constants --- .../RemoteBuildConfiguration.java | 10 ++++++++-- .../RemoteJenkinsServer.java | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 04b435b1..da2d6c32 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -85,6 +85,12 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private static final long serialVersionUID = -4059001060991775146L; + /** + * Default for this class is "no auth configured" since we do not want to override potential global config + */ + private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; + + private static final int DEFAULT_POLLINTERVALL = 10; private static final String paramerizedBuildUrl = "/buildWithParameters"; private static final String normalBuildUrl = "/build"; @@ -127,7 +133,7 @@ protected Object readResolve() { //migrate Auth To Auth2 if(auth2 == null) { if(auth == null || auth.size() <= 0) { - auth2 = NullAuth.INSTANCE; + auth2 = DEFAULT_AUTH; } else { auth2 = Auth.authToAuth2(auth); } @@ -1338,7 +1344,7 @@ public List getAuth(){ } public Auth2 getAuth2() { - return (auth2 != null) ? auth2 : NullAuth.INSTANCE; + return (auth2 != null) ? auth2 : DEFAULT_AUTH; } public boolean getShouldNotFailBuild() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index ec34943d..ffee46cf 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -13,7 +13,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; @@ -33,6 +32,11 @@ */ public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable { + /** + * Default for this class is No Authentication + */ + private final static Auth2 DEFAULT_AUTH = NoneAuth.INSTANCE; + /** * We need to keep this for compatibility - old config deserialization! * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. @@ -60,7 +64,7 @@ protected Object readResolve() { //migrate Auth To Auth2 if(auth2 == null) { if(auth == null || auth.size() <= 0) { - auth2 = NoneAuth.INSTANCE; + auth2 = DEFAULT_AUTH; } else { auth2 = Auth.authToAuth2(auth); } @@ -85,7 +89,7 @@ public void setHasBuildTokenRootSupport(boolean hasBuildTokenRootSupport) @DataBoundSetter public void setAuth2(Auth2 auth2) { - this.auth2 = (auth2 != null) ? auth2 : NoneAuth.INSTANCE; + this.auth2 = (auth2 != null) ? auth2 : DEFAULT_AUTH; } @DataBoundSetter From e974125bdc177c6cee01dfa3774dd6682f2411fa Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 20 Mar 2018 15:06:55 +0100 Subject: [PATCH 050/262] Removed unnecessary clone() code --- .../RemoteJenkinsServer.java | 3 --- .../ParameterizedRemoteTrigger/auth2/Auth2.java | 6 ------ .../auth2/CredentialsAuth.java | 9 --------- .../ParameterizedRemoteTrigger/auth2/NoneAuth.java | 5 ----- .../ParameterizedRemoteTrigger/auth2/NullAuth.java | 5 ----- .../auth2/TokenAuth.java | 14 ++++---------- .../auth2/Auth2Test.java | 8 ++++---- 7 files changed, 8 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index ffee46cf..503363cd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -207,10 +207,7 @@ public static Auth2Descriptor getDefaultAuth2Descriptor() { @Override public RemoteJenkinsServer clone() throws CloneNotSupportedException { RemoteJenkinsServer clone = (RemoteJenkinsServer)super.clone(); - clone.address = address; clone.auth2 = (auth2 == null) ? null : auth2.clone(); - clone.displayName = displayName; - clone.hasBuildTokenRootSupport = hasBuildTokenRootSupport; return clone; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index 1ed76d0c..de074522 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -63,10 +63,4 @@ public Auth2 clone() throws CloneNotSupportedException { return (Auth2)super.clone(); }; - @Override - public abstract int hashCode(); - - @Override - public abstract boolean equals(Object obj); - } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 33ebfc9c..6ea5f616 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -161,15 +161,6 @@ public static ListBoxModel doFillCredentialsItems() { } } - - - @Override - public CredentialsAuth clone() throws CloneNotSupportedException { - CredentialsAuth clone = (CredentialsAuth)super.clone(); - clone.credentials = credentials; - return clone; - } - @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index 0ebdb45a..577be09f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -55,11 +55,6 @@ public String getDisplayName() { } } - @Override - public NoneAuth clone() throws CloneNotSupportedException { - return (NoneAuth)super.clone(); - } - @Override public int hashCode() { return "NoneAuth".hashCode(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index 5dce119b..d26d9203 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -53,11 +53,6 @@ public String getDisplayName() { } } - @Override - public NullAuth clone() throws CloneNotSupportedException { - return (NullAuth)super.clone(); - } - @Override public int hashCode() { return "NullAuth".hashCode(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index c86f9201..2a26640a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -77,14 +77,6 @@ public String getDisplayName() { } } - @Override - public TokenAuth clone() throws CloneNotSupportedException { - TokenAuth clone = (TokenAuth)super.clone(); - clone.apiToken = apiToken; - clone.userName = userName; - return clone; - } - @Override public int hashCode() { final int prime = 31; @@ -106,13 +98,15 @@ public boolean equals(Object obj) { if (apiToken == null) { if (other.apiToken != null) return false; - } else if (!apiToken.equals(other.apiToken)) + } else if (!apiToken.equals(other.apiToken)) { return false; + } if (userName == null) { if (other.userName != null) return false; - } else if (!userName.equals(other.userName)) + } else if (!userName.equals(other.userName)) { return false; + } return true; } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index 89c279ee..8eed4e6a 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -13,7 +13,7 @@ public class Auth2Test { public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedException { CredentialsAuth original = new CredentialsAuth(); original.setCredentials("original"); - CredentialsAuth clone = original.clone(); + CredentialsAuth clone = (CredentialsAuth)original.clone(); verifyEqualsHashCode(original, clone); //Test changing clone @@ -28,7 +28,7 @@ public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { TokenAuth original = new TokenAuth(); original.setApiToken("original"); original.setUserName("original"); - TokenAuth clone = original.clone(); + TokenAuth clone = (TokenAuth)original.clone(); verifyEqualsHashCode(original, clone); //Test changing clone @@ -44,14 +44,14 @@ public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { @Test public void testNullAuthCloneBehaviour() throws CloneNotSupportedException { NullAuth original = NullAuth.INSTANCE; - NullAuth clone = original.clone(); + NullAuth clone = (NullAuth)original.clone(); verifyEqualsHashCode(original, clone); } @Test public void testNoneAuthCloneBehaviour() throws CloneNotSupportedException { NoneAuth original = NoneAuth.INSTANCE; - NoneAuth clone = original.clone(); + NoneAuth clone = (NoneAuth)original.clone(); verifyEqualsHashCode(original, clone); } From 9ebdb4f38e8d9398e5fbcc03c56e35c9e5a961b5 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 21 Mar 2018 08:33:51 +0100 Subject: [PATCH 051/262] Removed defaults in constructor of RemoteBuildConfiguration --- .../RemoteBuildConfiguration.java | 40 ++++++------------- .../auth2/NullAuth.java | 3 +- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index da2d6c32..50dba436 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -3,6 +3,7 @@ import static org.apache.commons.io.IOUtils.closeQuietly; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; import java.io.BufferedReader; @@ -32,7 +33,6 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; @@ -120,9 +120,6 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep @DataBoundConstructor public RemoteBuildConfiguration() { pollInterval = DEFAULT_POLLINTERVALL; - token = ""; - parameters = ""; - parameterFile = ""; } /* @@ -220,13 +217,14 @@ public void setParameterFile(String parameterFile) { } public List getParameterList(BuildContext context) { - if (parameters != null && !parameters.isEmpty()){ - String[] params = parameters.split("\n"); - return new ArrayList(Arrays.asList(params)); - } else if (loadParamsFromFile){ - return loadExternalParameterFile(context); + String params = getParameters(); + if (!params.isEmpty()) { + String[] parameterArray = params.split("\n"); + return new ArrayList(Arrays.asList(parameterArray)); + } else if (loadParamsFromFile) { + return loadExternalParameterFile(context); } else { - return new ArrayList(); + return new ArrayList(); } } @@ -243,7 +241,7 @@ private List loadExternalParameterFile(BuildContext context) { List parameterList = new ArrayList(); try { - String filePath = String.format("%s/%s", context.workspace, parameterFile); + String filePath = String.format("%s/%s", context.workspace, getParameterFile()); String sCurrentLine; context.logger.println(String.format("Loading parameters from file %s", filePath)); @@ -1331,18 +1329,6 @@ public boolean getOverrideAuth() { return true; } - /** - * TODO: Remove - only used in tests - * @return the list of authorizations. - * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. - */ - public List getAuth(){ - Auth oldAuth = Auth.auth2ToAuth(auth2); - ArrayList list = new ArrayList(); - list.add(oldAuth); - return list; - } - public Auth2 getAuth2() { return (auth2 != null) ? auth2 : DEFAULT_AUTH; } @@ -1367,7 +1353,7 @@ public boolean getBlockBuildUntilComplete() { * @return the configured job value. Can be a job name or full job URL. */ public String getJob() { - return job; + return trimToEmpty(job); } /** @@ -1380,11 +1366,11 @@ private String getJobExpanded(BuildContext context) throws IOException { } public String getToken() { - return token; + return trimToEmpty(token); } public String getParameters() { - return parameters; + return trimToEmpty(parameters); } public boolean getEnhancedLogging() { @@ -1396,7 +1382,7 @@ public boolean getLoadParamsFromFile() { } public String getParameterFile() { - return parameterFile; + return trimToEmpty(parameterFile); } public int getConnectionRetryLimit() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index d26d9203..f2d5c77a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -20,11 +20,10 @@ public class NullAuth extends Auth2 { public static final NullAuth INSTANCE = new NullAuth(); - @DataBoundConstructor public NullAuth() { } - + @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { //Ignore From 62060cfac5b410d861180559be4b3ac08e2c8aa4 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 16 Mar 2018 11:24:21 +0100 Subject: [PATCH 052/262] removed unnecessary getDescriptorStatic() --- .../RemoteBuildConfiguration.java | 6 ------ .../pipeline/RemoteBuildPipelineStep.java | 6 +++++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 5cf98015..74b3adbb 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1495,12 +1495,6 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } - public static DescriptorImpl getDescriptorStatic() { - Jenkins jenkins = Jenkins.getInstance(); - if (jenkins == null) throw new NullPointerException("Jenkins instance can not be null"); - return (RemoteBuildConfiguration.DescriptorImpl) jenkins.getDescriptor(RemoteBuildConfiguration.class); - } - // This indicates to Jenkins that this is an implementation of an extension // point. @Extension diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index e16d870b..a024ed01 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -51,6 +51,7 @@ import hudson.model.TaskListener; import hudson.util.FormValidation; import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; public class RemoteBuildPipelineStep extends Step { @@ -152,7 +153,10 @@ public static final class DescriptorImpl extends StepDescriptor { } public ListBoxModel doFillRemoteJenkinsNameItems() { - return RemoteBuildConfiguration.getDescriptorStatic().doFillRemoteJenkinsNameItems(); + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins == null) throw new NullPointerException("Jenkins instance can not be null"); + RemoteBuildConfiguration.DescriptorImpl descriptor = (RemoteBuildConfiguration.DescriptorImpl) jenkins.getDescriptor(RemoteBuildConfiguration.class); + return descriptor.doFillRemoteJenkinsNameItems(); } public FormValidation doCheckJob( From efcef1079bb1f7058a4b0b5a40d2795088003de2 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Fri, 16 Mar 2018 12:36:40 +0100 Subject: [PATCH 053/262] Fixed findbugs. Added @Restricted --- .../RemoteBuildConfiguration.java | 1 + .../pipeline/RemoteBuildPipelineStep.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 74b3adbb..6adc4bbd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1590,6 +1590,7 @@ public FormValidation doCheckRemoteJenkinsName( } @Restricted(NoExternalUse.class) + @Nonnull public ListBoxModel doFillRemoteJenkinsNameItems() { ListBoxModel model = new ListBoxModel(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index a024ed01..65ab90b0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -27,6 +27,8 @@ import java.util.List; import java.util.Set; +import javax.annotation.Nonnull; + import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; @@ -40,6 +42,8 @@ import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; @@ -152,13 +156,17 @@ public static final class DescriptorImpl extends StepDescriptor { return set; } + @Restricted(NoExternalUse.class) + @Nonnull public ListBoxModel doFillRemoteJenkinsNameItems() { Jenkins jenkins = Jenkins.getInstance(); - if (jenkins == null) throw new NullPointerException("Jenkins instance can not be null"); + if (jenkins == null) return new ListBoxModel(0); RemoteBuildConfiguration.DescriptorImpl descriptor = (RemoteBuildConfiguration.DescriptorImpl) jenkins.getDescriptor(RemoteBuildConfiguration.class); + if(descriptor == null) throw new RuntimeException("Could not get descriptor for RemoteBuildConfiguration"); return descriptor.doFillRemoteJenkinsNameItems(); } + @Restricted(NoExternalUse.class) public FormValidation doCheckJob( @QueryParameter("job") final String value, @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, @@ -168,6 +176,7 @@ public FormValidation doCheckJob( return FormValidation.ok(); } + @Restricted(NoExternalUse.class) public FormValidation doCheckRemoteJenkinsUrl( @QueryParameter("remoteJenkinsUrl") final String value, @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, @@ -177,6 +186,7 @@ public FormValidation doCheckRemoteJenkinsUrl( return FormValidation.ok(); } + @Restricted(NoExternalUse.class) public FormValidation doCheckRemoteJenkinsName( @QueryParameter("remoteJenkinsName") final String value, @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, From 85cf03e9745e688c5059f6731771b69e7817b985 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 21 Mar 2018 10:56:16 +0100 Subject: [PATCH 054/262] removed method only used in test --- .../ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 9b5c0dce..9ceb8cfc 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -77,7 +77,6 @@ public void testDefaults() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("job"); - assertEquals(1, config.getAuth().size()); assertEquals(false, config.getBlockBuildUntilComplete()); //False in Job assertEquals(false, config.getEnhancedLogging()); assertEquals("job", config.getJob()); From bd6813fa26302f2a4d7658a3361dad29210cb7d6 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 21 Mar 2018 11:17:52 +0100 Subject: [PATCH 055/262] switched to findByDescribableClassName --- .../pipeline/RemoteBuildPipelineStep.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 65ab90b0..7cb08944 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -49,8 +49,10 @@ import org.kohsuke.stapler.QueryParameter; import hudson.Extension; +import hudson.ExtensionList; import hudson.FilePath; import hudson.Launcher; +import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; import hudson.util.FormValidation; @@ -159,9 +161,8 @@ public static final class DescriptorImpl extends StepDescriptor { @Restricted(NoExternalUse.class) @Nonnull public ListBoxModel doFillRemoteJenkinsNameItems() { - Jenkins jenkins = Jenkins.getInstance(); - if (jenkins == null) return new ListBoxModel(0); - RemoteBuildConfiguration.DescriptorImpl descriptor = (RemoteBuildConfiguration.DescriptorImpl) jenkins.getDescriptor(RemoteBuildConfiguration.class); + RemoteBuildConfiguration.DescriptorImpl descriptor = Descriptor.findByDescribableClassName( + ExtensionList.lookup(RemoteBuildConfiguration.DescriptorImpl.class), RemoteBuildConfiguration.class.getName()); if(descriptor == null) throw new RuntimeException("Could not get descriptor for RemoteBuildConfiguration"); return descriptor.doFillRemoteJenkinsNameItems(); } From 9552b6012201786fd32aae60a37507369a6ccb76 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Wed, 21 Mar 2018 11:26:36 +0100 Subject: [PATCH 056/262] stripped down equals() implementations --- .../ParameterizedRemoteTrigger/auth2/NoneAuth.java | 9 +-------- .../ParameterizedRemoteTrigger/auth2/NullAuth.java | 9 +-------- .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 12 ++++++++++++ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index b3ff9470..0049f4dd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -63,15 +63,8 @@ public int hashCode() { } @Override - @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - return true; + return this.getClass().isInstance(obj); } } \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index fb1f1c08..ee589bdc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -62,15 +62,8 @@ public int hashCode() { } @Override - @SuppressFBWarnings("EQ_UNUSUAL") public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (!this.getClass().isInstance(obj)) - return false; - return true; + return this.getClass().isInstance(obj); } } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index 25318eb8..63237092 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -48,6 +48,12 @@ public void testNullAuthCloneBehaviour() throws CloneNotSupportedException { verifyEqualsHashCode(original, clone); } + @Test + public void testNullAuthEqualsWithNull() throws CloneNotSupportedException { + NullAuth original = new NullAuth(); + assertFalse(original.equals(null)); + } + @Test public void testNoneAuthCloneBehaviour() throws CloneNotSupportedException { NoneAuth original = new NoneAuth(); @@ -55,6 +61,12 @@ public void testNoneAuthCloneBehaviour() throws CloneNotSupportedException { verifyEqualsHashCode(original, clone); } + @Test + public void testNoneAuthEqualsWithNull() throws CloneNotSupportedException { + NoneAuth original = new NoneAuth(); + assertFalse(original.equals(null)); + } + private void verifyEqualsHashCode(Auth2 original, Auth2 clone) throws CloneNotSupportedException { verifyEqualsHashCode(original, clone, true); } From cc2a0a4a95665b46241faff0f0b739b768591480 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 29 Mar 2018 09:13:37 +0200 Subject: [PATCH 057/262] Adjusted pom as recommended in docu --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 90611b96..9d25cf7b 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ org.jenkins-ci.tools maven-hpi-plugin + true 3.0.0-SNAPSHOT From e9ed218f588ee13cfe7e1579403913de6a3f4615 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 3 Apr 2018 08:31:33 +0200 Subject: [PATCH 058/262] removed extension:true again after discussions --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9d25cf7b..90611b96 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,6 @@ org.jenkins-ci.tools maven-hpi-plugin - true 3.0.0-SNAPSHOT From 39c8314e83602e74a72f816fd218e83a48c04db0 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 29 Mar 2018 09:36:16 +0200 Subject: [PATCH 059/262] Fixed missing line endings in remote log --- .../RemoteBuildConfiguration.java | 6 ++++-- .../utils/StringTools.java | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index cfeaaa30..bbef17d6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -5,6 +5,7 @@ import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; import java.io.BufferedReader; import java.io.FileInputStream; @@ -46,6 +47,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -575,7 +577,7 @@ protected void failBuild(Exception e, PrintStream logger) throws IOException { e.getMessage(), this.getShouldNotFailBuild() ? " But the build will continue." : "")); if(enhancedLogging) { - msg.append("\n").append(ExceptionUtils.getFullStackTrace(e)); + msg.append(NL).append(ExceptionUtils.getFullStackTrace(e)); } if(logger != null) logger.println("ERROR: " + msg.toString()); if (!this.getShouldNotFailBuild()) { @@ -1159,7 +1161,7 @@ private String readInputStream(HttpURLConnection connection) throws IOException String line; StringBuilder response = new StringBuilder(); while ((line = rd.readLine()) != null) { - response.append(line); + response.append(line).append(NL); } return response.toString(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java new file mode 100644 index 00000000..edc0bd85 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java @@ -0,0 +1,17 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +public class StringTools +{ + + /** + * System specific new line character/string + */ + public static final String NL = getSystemLineSeparator(); + + private static String getSystemLineSeparator() { + String newLine = System.getProperty("line.separator"); + if(newLine == null || newLine.length() <= 0) newLine = "\n"; + return newLine; + } + +} From 793de4b750a803846a3b907a9f634807e9c8e0d3 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 29 Mar 2018 10:56:31 +0200 Subject: [PATCH 060/262] Fixed and extended test --- .../RemoteBuildConfiguration.java | 3 ++- .../utils/StringTools.java | 5 +++++ .../RemoteBuildConfigurationTest.java | 22 ++++++++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index bbef17d6..c1f3c949 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1161,7 +1161,8 @@ private String readInputStream(HttpURLConnection connection) throws IOException String line; StringBuilder response = new StringBuilder(); while ((line = rd.readLine()) != null) { - response.append(line).append(NL); + if(response.length() > 0) response.append(NL); + response.append(line); } return response.toString(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java index edc0bd85..3d2f4ffb 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java @@ -8,6 +8,11 @@ public class StringTools */ public static final String NL = getSystemLineSeparator(); + /** + * Unix/Linux specific new line character '\n' + */ + public static final String NL_UNIX = "\n"; + private static String getSystemLineSeparator() { String newLine = System.getProperty("line.separator"); if(newLine == null || newLine.length() <= 0) newLine = "\n"; diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 9ceb8cfc..44e608bc 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -1,5 +1,7 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -9,10 +11,13 @@ import java.io.IOException; import java.lang.reflect.Field; import java.net.MalformedURLException; +import java.util.List; +import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -20,6 +25,7 @@ import org.jvnet.hudson.test.WithoutJenkins; import hudson.AbortException; +import hudson.EnvVars; import hudson.model.FreeStyleProject; import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; @@ -53,22 +59,32 @@ private void _testRemoteBuild() throws Exception { FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); remoteProject.addProperty(new ParametersDefinitionProperty( - new StringParameterDefinition("parameterName1", "value1"), - new StringParameterDefinition("parameterName2", "value2"))); + new StringParameterDefinition("parameterName1", "default1"), + new StringParameterDefinition("parameterName2", "default2"))); FreeStyleProject project = jenkinsRule.createFreeStyleProject(); RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); configuration.setJob(remoteProject.getFullName()); configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); configuration.setPreventRemoteBuildQueue(false); + configuration.setBlockBuildUntilComplete(true); configuration.setPollInterval(1); configuration.setEnhancedLogging(true); - configuration.setParameters(""); + configuration.setParameters("parameterName1=value1" + NL_UNIX + "parameterName2=value2"); project.getBuildersList().add(configuration); + //Trigger build jenkinsRule.waitUntilNoActivity(); jenkinsRule.buildAndAssertSuccess(project); + + //Check results + List log = IOUtils.readLines(project.getLastBuild().getLogInputStream()); + assertTrue(log.toString(), log.toString().contains("Started by user anonymous, Building in workspace")); + + EnvVars remoteEnv = remoteProject.getLastBuild().getEnvironment(null); + assertEquals("value1", remoteEnv.get("parameterName1")); + assertEquals("value2", remoteEnv.get("parameterName2")); } @Test @WithoutJenkins From 75473a893a148351f10faa5875b11db919563907 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 3 Apr 2018 13:56:13 +0200 Subject: [PATCH 061/262] organized imports --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 -- .../RemoteBuildConfigurationTest.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index c1f3c949..99331641 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -47,7 +47,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -71,7 +70,6 @@ import hudson.util.CopyOnWriteList; import hudson.util.FormValidation; import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 44e608bc..7ed63cbc 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -1,6 +1,5 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -17,7 +16,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; From c72916b1ad2dce451fb4a35357dbe5ab5c2bb247 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 5 Apr 2018 15:09:39 +0200 Subject: [PATCH 062/262] Added authenticated tests --- .../BuildContext.java | 25 ++++--- .../ConnectionResponse.java | 3 +- .../RemoteBuildConfiguration.java | 31 ++++++-- .../pipeline/RemoteBuildPipelineStep.java | 1 - .../RemoteBuildConfigurationTest.java | 70 +++++++++++++++++-- .../RemoteJenkinsServerTest.java | 10 +-- 6 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java index bafd71ae..2ae63d21 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -4,6 +4,7 @@ import java.io.PrintStream; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; @@ -26,49 +27,53 @@ @ParametersAreNullableByDefault public class BuildContext { - @Nullable + @Nullable @CheckForNull public final Run run; - @Nullable + @Nullable @CheckForNull public final FilePath workspace; - @Nullable + @Nullable @CheckForNull public final TaskListener listener; @Nonnull public final PrintStream logger; - @Nullable + @Nullable @CheckForNull public RemoteJenkinsServer effectiveRemoteServer; - /** * The current Item (job, pipeline,...) where the plugin is used from. - * */ @Nonnull public final String currentItem; - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem) { + + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem, @Nullable RemoteJenkinsServer effectiveRemoteServer) { this.run = run; this.workspace = workspace; this.listener = listener; this.logger = logger; this.currentItem = getCurrentItem(run, currentItem); + this.effectiveRemoteServer = effectiveRemoteServer; } + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem) { + this(run, workspace, listener, logger, null, null); + } + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger) { - this(run, workspace, listener, logger, null); + this(run, workspace, listener, logger, null, null); } public BuildContext(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnull TaskListener listener) { - this(run, workspace, listener, listener.getLogger()); + this(run, workspace, listener, listener.getLogger(), null, null); } public BuildContext(@Nonnull PrintStream logger, String currentItem) { - this(null, null, null, logger, currentItem); + this(null, null, null, logger, currentItem, null); } private String getCurrentItem(Run run, String currentItem) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java index dfb8f81f..ab66694e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -17,7 +18,7 @@ public class ConnectionResponse @Nonnull private final Map> header; - @Nullable + @Nullable @CheckForNull private final JSONObject body; @Nonnull diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 99331641..31c0b862 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -28,7 +28,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; import org.apache.commons.lang.StringUtils; @@ -406,7 +408,7 @@ private String buildUrlQueryString(Collection parameters) { * Name of the configuration you are looking for * @return A deep-copy of the RemoteJenkinsServer object configured globally */ - private RemoteJenkinsServer findRemoteHost(String displayName) { + private @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { if(isEmpty(displayName)) return null; RemoteJenkinsServer server = null; for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { @@ -496,6 +498,10 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec String triggerUrlString; String query = ""; + if(context.effectiveRemoteServer == null) { + throw new AbortException("context.effectiveRemoteServer is null"); + } + if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server triggerUrlString = context.effectiveRemoteServer.getRemoteAddress(); @@ -687,6 +693,10 @@ public Handle performTriggerAndGetQueueId(BuildContext context) ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); + + if(context.effectiveRemoteServer == null) { + throw new AbortException("context.effectiveRemoteServer is null"); + } Handle handle = new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer); handle.setJobMetadata(remoteJobMetadata); return handle; @@ -813,6 +823,9 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { + if(context.effectiveRemoteServer == null) { + throw new AbortException("context.effectiveRemoteServer null"); + } String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getRemoteAddress(), queueId); ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); JSONObject queueResponse = response.getBody(); @@ -1184,6 +1197,9 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { + if(context.effectiveRemoteServer == null) { + throw new AbortException("context.effectiveRemoteServer null"); + } String address = context.effectiveRemoteServer.getRemoteAddress(); URL crumbProviderUrl; try { @@ -1216,7 +1232,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) { URLConnection connection = url.openConnection(); - Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = (context.effectiveRemoteServer == null) ? null : context.effectiveRemoteServer.getAuth2(); Auth2 overrideAuth = this.getAuth2(); if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { @@ -1232,12 +1248,14 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) private void logAuthInformation(BuildContext context) throws IOException { - Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = (context.effectiveRemoteServer == null) ? null : context.effectiveRemoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if(localAuth != null && !(localAuth instanceof NullAuth)) { - context.logger.println(String.format(" Using job-level defined " + localAuth.toString((Item)context.run.getParent()) )); + String authString = (context.run == null) ? localAuth.getDescriptor().getDisplayName() : localAuth.toString((Item)context.run.getParent()); + context.logger.println(String.format(" Using job-level defined " + authString )); } else if(serverAuth != null && !(serverAuth instanceof NullAuth)) { - context.logger.println(String.format(" Using globally defined " + serverAuth.toString((Item)context.run.getParent()) )); + String authString = (context.run == null) ? serverAuth.getDescriptor().getDisplayName() : serverAuth.toString((Item)context.run.getParent()); + context.logger.println(String.format(" Using globally defined " + authString)); } else { context.logger.println(" No credentials configured"); } @@ -1268,8 +1286,9 @@ private void logConfiguration(BuildContext context, List effectiveParams String.format(" - remoteJenkinsUrl: %s", _remoteJenkinsUrl)); } if(_auth != null && !(_auth instanceof NullAuth)) { + String authString = context.run == null ? _auth.getDescriptor().getDisplayName() : _auth.toString((Item)context.run.getParent()); context.logger.println( - String.format(" - auth: %s", _auth.toString((Item)context.run.getParent()))); + String.format(" - auth: %s", authString)); } context.logger.println( String.format(" - parameters: %s", _parameters)); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 7cb08944..9e632d51 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -57,7 +57,6 @@ import hudson.model.TaskListener; import hudson.util.FormValidation; import hudson.util.ListBoxModel; -import jenkins.model.Jenkins; public class RemoteBuildPipelineStep extends Step { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 7ed63cbc..70468beb 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -2,6 +2,7 @@ import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; @@ -15,37 +16,82 @@ import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.WithoutJenkins; import hudson.AbortException; import hudson.EnvVars; +import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; +import hudson.model.User; +import hudson.security.HudsonPrivateSecurityRealm; +import hudson.security.SecurityRealm; +import hudson.security.AuthorizationStrategy.Unsecured; import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.LogTaskListener; +import jenkins.model.Jenkins; public class RemoteBuildConfigurationTest { + @Rule public JenkinsRule jenkinsRule = new JenkinsRule(); + private User testUser; + private String testUserToken; + + private void disableAuth() { + jenkinsRule.jenkins.setAuthorizationStrategy(Unsecured.UNSECURED); + jenkinsRule.jenkins.setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); + jenkinsRule.jenkins.setCrumbIssuer(null); + } + + private void enableAuth() throws IOException { + MockAuthorizationStrategy mockAuth = new MockAuthorizationStrategy(); + jenkinsRule.jenkins.setAuthorizationStrategy(mockAuth); + + HudsonPrivateSecurityRealm hudsonPrivateSecurityRealm = new HudsonPrivateSecurityRealm(false, false, null); + jenkinsRule.jenkins.setSecurityRealm(hudsonPrivateSecurityRealm); //jenkinsRule.createDummySecurityRealm()); + testUser = hudsonPrivateSecurityRealm.createAccount("test", "test"); + testUserToken = testUser.getProperty(jenkins.security.ApiTokenProperty.class).getApiToken(); + + mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); + } + + @Test public void testRemoteBuild() throws Exception { - jenkinsRule.jenkins.setCrumbIssuer(null); - _testRemoteBuild(); + disableAuth(); + _testRemoteBuild(false); } + @Test + public void testRemoteBuildWithAuthentication() throws Exception { + enableAuth(); + _testRemoteBuild(true); + } + +// @Test +// public void testRemoteBuildWithMissingAuthentication() throws Exception { +// enableAuth(); +// _testRemoteBuild(false); +// } + @Test public void testRemoteBuildWithCrumb() throws Exception { + disableAuth(); jenkinsRule.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); - _testRemoteBuild(); + _testRemoteBuild(false); } - private void _testRemoteBuild() throws Exception { + private void _testRemoteBuild(boolean authenticate) throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); @@ -69,6 +115,12 @@ private void _testRemoteBuild() throws Exception { configuration.setPollInterval(1); configuration.setEnhancedLogging(true); configuration.setParameters("parameterName1=value1" + NL_UNIX + "parameterName2=value2"); + if(authenticate) { + TokenAuth tokenAuth = new TokenAuth(); + tokenAuth.setUserName(testUser.getId()); + tokenAuth.setApiToken(testUserToken); + configuration.setAuth2(tokenAuth); + } project.getBuildersList().add(configuration); @@ -77,10 +129,14 @@ private void _testRemoteBuild() throws Exception { jenkinsRule.buildAndAssertSuccess(project); //Check results - List log = IOUtils.readLines(project.getLastBuild().getLogInputStream()); - assertTrue(log.toString(), log.toString().contains("Started by user anonymous, Building in workspace")); + FreeStyleBuild lastBuild2 = project.getLastBuild(); + assertNotNull(lastBuild2); + List log = IOUtils.readLines(lastBuild2.getLogInputStream()); + assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "anonymous") + ", Building in workspace")); - EnvVars remoteEnv = remoteProject.getLastBuild().getEnvironment(null); + FreeStyleBuild lastBuild = remoteProject.getLastBuild(); + assertNotNull("lastBuild null", lastBuild); + EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); assertEquals("value1", remoteEnv.get("parameterName1")); assertEquals("value2", remoteEnv.get("parameterName2")); } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index 4b12bac4..2f69b585 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -1,15 +1,13 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.CredentialsAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - import org.junit.Test; @@ -58,9 +56,11 @@ public void testCloneBehaviour() throws Exception { //Test if clone is deep-copy or if server fields can be modified TokenAuth cloneAuth = (TokenAuth)clone.getAuth2(); + assertNotNull(cloneAuth); cloneAuth.setApiToken("changed"); cloneAuth.setUserName("changed"); TokenAuth serverAuth = (TokenAuth)server.getAuth2(); + assertNotNull(serverAuth); assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken()); assertEquals("auth.userName", USER, serverAuth.getUserName()); From 8d4e5a6d75d5a2bc7823afb10e359f73b1eebd9c Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 5 Apr 2018 15:53:18 +0200 Subject: [PATCH 063/262] Authenticated connection for readJsonFileFromBuildArchive When the remote job is secured and the authentication is configured via global remote jenkins config the job can be triggered but handle.readJsonFileFromBuildArchive() fails (unauthenticated). ERROR: ForbiddenException: Server returned 403 - Forbidden. User does not have enough permissions for this request: http://localhost:8081/jenkins/job/Job1/370/artifact/build-results.json --- .../RemoteBuildConfiguration.java | 2 +- .../pipeline/Handle.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 31c0b862..d8bfdac5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -408,7 +408,7 @@ private String buildUrlQueryString(Collection parameters) { * Name of the configuration you are looking for * @return A deep-copy of the RemoteJenkinsServer object configured globally */ - private @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { + public @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { if(isEmpty(displayName)) return null; RemoteJenkinsServer server = null; for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index d55276d6..f591cfd3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -16,6 +16,8 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; @@ -375,12 +377,24 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, PrintStreamWrapper log = new PrintStreamWrapper(); try { BuildContext context = new BuildContext(log.getPrintStream(), this.currentItem); + context.effectiveRemoteServer = new RemoteJenkinsServer(); + context.effectiveRemoteServer.setAuth2(getEffectiveAuthentication()); return remoteBuildConfiguration.sendHTTPCall(fileUrl.toString(), "GET", context); } finally { lastLog = log.getContent(); } } + private Auth2 getEffectiveAuthentication() { + Auth2 overrideAuth = remoteBuildConfiguration.getAuth2(); + if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { + return overrideAuth; + } else { + RemoteJenkinsServer globalAuth = remoteBuildConfiguration.findRemoteHost(remoteBuildConfiguration.getRemoteJenkinsName()); + return globalAuth == null ? null : globalAuth.getAuth2(); + } + } + public void setJobMetadata(JSONObject remoteJobMetadata) { this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); From 1cfeab90798f3c1af42241c39c2fe173f2e47016 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 13 Apr 2018 13:03:38 +0200 Subject: [PATCH 064/262] effectiveRemoteServer nonnull --- .../BasicBuildContext.java | 35 ++++++++++++++++ .../BuildContext.java | 40 +++++-------------- .../RemoteBuildConfiguration.java | 22 +++++----- .../pipeline/Handle.java | 28 ++++--------- .../pipeline/RemoteBuildPipelineStep.java | 5 ++- .../utils/TokenMacroUtils.java | 8 ++-- 6 files changed, 70 insertions(+), 68 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java new file mode 100644 index 00000000..58fff64d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java @@ -0,0 +1,35 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNullableByDefault; + +import hudson.FilePath; +import hudson.model.Run; +import hudson.model.TaskListener; + +/** + * This object wraps a {@link Run}, {@link FilePath}, and {@link TaskListener} - + * the typical objects passed from one method to the other in a Jenkins Builder/BuildStep implementation.
+ *
+ * The reason for wrapping is simplicity. + */ +@ParametersAreNullableByDefault +public class BasicBuildContext +{ + @Nullable @CheckForNull + public final Run run; + + @Nullable @CheckForNull + public final FilePath workspace; + + @Nullable @CheckForNull + public final TaskListener listener; + + + public BasicBuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener) { + this.run = run; + this.workspace = workspace; + this.listener = listener; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java index 2ae63d21..9be6270c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -4,7 +4,6 @@ import java.io.PrintStream; -import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; @@ -25,21 +24,12 @@ * must not be null. */ @ParametersAreNullableByDefault -public class BuildContext +public class BuildContext extends BasicBuildContext { - @Nullable @CheckForNull - public final Run run; - - @Nullable @CheckForNull - public final FilePath workspace; - - @Nullable @CheckForNull - public final TaskListener listener; - @Nonnull public final PrintStream logger; - @Nullable @CheckForNull + @Nonnull public RemoteJenkinsServer effectiveRemoteServer; /** @@ -49,33 +39,23 @@ public class BuildContext public final String currentItem; - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem, @Nullable RemoteJenkinsServer effectiveRemoteServer) { - this.run = run; - this.workspace = workspace; - this.listener = listener; + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) { + super(run, workspace, listener); this.logger = logger; - this.currentItem = getCurrentItem(run, currentItem); this.effectiveRemoteServer = effectiveRemoteServer; + this.currentItem = getCurrentItem(run, currentItem); } - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nullable String currentItem) { - this(run, workspace, listener, logger, null, null); - } - - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger) { - this(run, workspace, listener, logger, null, null); - } - - public BuildContext(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnull TaskListener listener) - { - this(run, workspace, listener, listener.getLogger(), null, null); + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer) { + this(run, workspace, listener, logger, effectiveRemoteServer, null); } - public BuildContext(@Nonnull PrintStream logger, String currentItem) + public BuildContext(@Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) { - this(null, null, null, logger, currentItem, null); + this(null, null, null, logger, effectiveRemoteServer, currentItem); } + @Nonnull private String getCurrentItem(Run run, String currentItem) { String runItem = null; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index d8bfdac5..b482fdb1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -103,7 +103,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep * We need to keep this for compatibility - old config deserialization! * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. */ - private transient List auth; + private transient List auth; private String remoteJenkinsName; private String remoteJenkinsUrl; @@ -119,6 +119,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean loadParamsFromFile; private String parameterFile; + @DataBoundConstructor public RemoteBuildConfiguration() { pollInterval = DEFAULT_POLLINTERVALL; @@ -357,7 +358,8 @@ private String buildUrlQueryString(Collection parameters) { * @throws MalformedURLException * if remoteJenkinsName no valid URL or job an URL but nor valid. */ - protected @Nonnull RemoteJenkinsServer findEffectiveRemoteHost(BuildContext context) throws IOException { + @Nonnull + public RemoteJenkinsServer findEffectiveRemoteHost(BasicBuildContext context) throws IOException { RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); RemoteJenkinsServer server = globallyConfiguredServer; String expandedJob = getJobExpanded(context); @@ -614,7 +616,8 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListene public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { - BuildContext context = new BuildContext(build, workspace, listener); + RemoteJenkinsServer effectiveRemoteServer = findEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); Handle handle = performTriggerAndGetQueueId(context); performWaitForBuild(context, handle); } @@ -645,8 +648,6 @@ public Handle performTriggerAndGetQueueId(BuildContext context) logConfiguration(context, cleanedParams); - context.effectiveRemoteServer = findEffectiveRemoteHost(context); - final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); @@ -693,10 +694,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); - - if(context.effectiveRemoteServer == null) { - throw new AbortException("context.effectiveRemoteServer is null"); - } + Handle handle = new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer); handle.setJobMetadata(remoteJobMetadata); return handle; @@ -1232,7 +1230,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) { URLConnection connection = url.openConnection(); - Auth2 serverAuth = (context.effectiveRemoteServer == null) ? null : context.effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 overrideAuth = this.getAuth2(); if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { @@ -1248,7 +1246,7 @@ private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) private void logAuthInformation(BuildContext context) throws IOException { - Auth2 serverAuth = (context.effectiveRemoteServer == null) ? null : context.effectiveRemoteServer.getAuth2(); + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if(localAuth != null && !(localAuth instanceof NullAuth)) { String authString = (context.run == null) ? localAuth.getDescriptor().getDisplayName() : localAuth.toString((Item)context.run.getParent()); @@ -1381,7 +1379,7 @@ public String getJob() { * @throws IOException * if there is an error replacing tokens. */ - private String getJobExpanded(BuildContext context) throws IOException { + private String getJobExpanded(BasicBuildContext context) throws IOException { return TokenMacroUtils.applyTokenMacroReplacements(getJob(), context); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index f591cfd3..529a90a9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -10,19 +10,17 @@ import java.lang.reflect.Modifier; import java.net.URL; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNullableByDefault; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; -import edu.umd.cs.findbugs.annotations.CheckForNull; import net.sf.json.JSONException; import net.sf.json.JSONObject; @@ -49,12 +47,12 @@ public class Handle implements Serializable { private String jobDisplayName; private String jobFullDisplayName; private String jobUrl; - private String remoteServerURL; /** * The current local Item (Job, Pipeline,...) where this plugin is currently used. */ private final String currentItem; + private final RemoteJenkinsServer effectiveRemoteServer; /* * The latest log entries from the last called method. @@ -74,7 +72,7 @@ public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, this.buildStatus = null; this.lastLog = ""; this.currentItem = currentItem; - this.remoteServerURL = effectiveRemoteServer.getRemoteAddress(); + this.effectiveRemoteServer = effectiveRemoteServer; if(trimToNull(currentItem) == null) throw new IllegalArgumentException("currentItem null"); } @@ -262,7 +260,7 @@ private BuildStatus getBuildStatus(boolean blockUntilFinished) throws IOExceptio //TODO: This currently blocks BuildData buildData = getBuildData(queueId, log.getPrintStream()); String jobLocation = buildData.getURL() + "api/json/"; - BuildContext context = new BuildContext(log.getPrintStream(), this.currentItem); + BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); buildStatus = remoteBuildConfiguration.getBuildStatus(jobLocation, context); finished = isFinishedBuildStatus(buildStatus); if(!blockUntilFinished) break; @@ -308,7 +306,7 @@ public void setBuildData(BuildData buildData) public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), remoteServerURL, queueId)); + sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getRemoteAddress(), queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); sb.append("]"); @@ -345,7 +343,7 @@ private BuildData getBuildData(String queueId, PrintStream logger) throws IOExce //Return if we already have the buildData if(buildData != null) return buildData; - BuildContext context = new BuildContext(logger, this.currentItem); + BuildContext context = new BuildContext(logger, effectiveRemoteServer, this.currentItem); BuildData build = remoteBuildConfiguration.getBuildData(queueId, context); this.buildData = build; return build; @@ -376,25 +374,13 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, PrintStreamWrapper log = new PrintStreamWrapper(); try { - BuildContext context = new BuildContext(log.getPrintStream(), this.currentItem); - context.effectiveRemoteServer = new RemoteJenkinsServer(); - context.effectiveRemoteServer.setAuth2(getEffectiveAuthentication()); + BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); return remoteBuildConfiguration.sendHTTPCall(fileUrl.toString(), "GET", context); } finally { lastLog = log.getContent(); } } - private Auth2 getEffectiveAuthentication() { - Auth2 overrideAuth = remoteBuildConfiguration.getAuth2(); - if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { - return overrideAuth; - } else { - RemoteJenkinsServer globalAuth = remoteBuildConfiguration.findRemoteHost(remoteBuildConfiguration.getRemoteJenkinsName()); - return globalAuth == null ? null : globalAuth.getAuth2(); - } - } - public void setJobMetadata(JSONObject remoteJobMetadata) { this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 9e632d51..d224d1cf 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -29,8 +29,10 @@ import javax.annotation.Nonnull; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; @@ -221,7 +223,8 @@ public static class Execution extends SynchronousNonBlockingStepExecution build = stepContext.get(Run.class); FilePath workspace = stepContext.get(FilePath.class); TaskListener listener = stepContext.get(TaskListener.class); - BuildContext context = new BuildContext(build, workspace, listener); + RemoteJenkinsServer effectiveRemoteServer = remoteBuildConfig.findEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); Handle handle = remoteBuildConfig.performTriggerAndGetQueueId(context); if(remoteBuildConfig.getBlockBuildUntilComplete()) { remoteBuildConfig.performWaitForBuild(context, handle); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java index 14f3906b..224e7f80 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java @@ -4,14 +4,14 @@ import java.util.ArrayList; import java.util.List; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.jenkinsci.plugins.tokenmacro.TokenMacro; public class TokenMacroUtils { - public static String applyTokenMacroReplacements(String input, BuildContext context) throws IOException + public static String applyTokenMacroReplacements(String input, BasicBuildContext context) throws IOException { try { if (isUseTokenMacro(context)) { @@ -27,7 +27,7 @@ public static String applyTokenMacroReplacements(String input, BuildContext cont return input; } - public static List applyTokenMacroReplacements(List inputs, BuildContext context) throws IOException + public static List applyTokenMacroReplacements(List inputs, BasicBuildContext context) throws IOException { List outputs = new ArrayList(); for (String input : inputs) { @@ -36,7 +36,7 @@ public static List applyTokenMacroReplacements(List inputs, Buil return outputs; } - public static boolean isUseTokenMacro(BuildContext context) + public static boolean isUseTokenMacro(BasicBuildContext context) { return context != null && context.run != null && context.workspace != null && context.listener != null; } From 9a372d48994dff756e04eda8671344fb1b29354b Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 13 Apr 2018 14:08:12 +0200 Subject: [PATCH 065/262] RemoteJenkinsServer serializable --- .../RemoteJenkinsServer.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 503363cd..240aa33a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -2,6 +2,7 @@ import static org.apache.commons.lang.StringUtils.trimToEmpty; +import java.io.Serializable; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -30,13 +31,15 @@ * @author Maurice W. * */ -public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable { +public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable, Serializable { + + private static final long serialVersionUID = -9211781849078964416L; /** * Default for this class is No Authentication */ - private final static Auth2 DEFAULT_AUTH = NoneAuth.INSTANCE; - + private static final Auth2 DEFAULT_AUTH = NoneAuth.INSTANCE; + /** * We need to keep this for compatibility - old config deserialization! * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. @@ -72,8 +75,8 @@ protected Object readResolve() { auth = null; return this; } - - + + @DataBoundSetter public void setDisplayName(String displayName) { From eaed0a09d9cfe8eb90c5dfe0656a227abb66dfeb Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 13 Apr 2018 14:08:30 +0200 Subject: [PATCH 066/262] organize imports --- .../plugins/ParameterizedRemoteTrigger/auth2/Auth2.java | 1 - .../plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java | 1 - .../plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index de074522..4ce62926 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -5,7 +5,6 @@ import java.net.URLConnection; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; import hudson.DescriptorExtensionList; import hudson.model.AbstractDescribableImpl; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java index bec15954..81c6671f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -7,7 +7,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.model.Item; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java index e3b715e3..f6525ffe 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -7,7 +7,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.model.Item; From 9c96b4322d6bf3de92adc8b894dee2d578ee94d6 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Mon, 16 Apr 2018 09:41:29 +0200 Subject: [PATCH 067/262] remove unnecessary checks --- .../RemoteBuildConfiguration.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b482fdb1..080954f5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -500,10 +500,6 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec String triggerUrlString; String query = ""; - if(context.effectiveRemoteServer == null) { - throw new AbortException("context.effectiveRemoteServer is null"); - } - if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server triggerUrlString = context.effectiveRemoteServer.getRemoteAddress(); @@ -821,9 +817,6 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { - if(context.effectiveRemoteServer == null) { - throw new AbortException("context.effectiveRemoteServer null"); - } String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getRemoteAddress(), queueId); ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); JSONObject queueResponse = response.getBody(); @@ -1195,9 +1188,6 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - if(context.effectiveRemoteServer == null) { - throw new AbortException("context.effectiveRemoteServer null"); - } String address = context.effectiveRemoteServer.getRemoteAddress(); URL crumbProviderUrl; try { From 195fe338e05fadf48dc5ec0250a4976f1cd4d101 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Mon, 16 Apr 2018 14:37:19 +0200 Subject: [PATCH 068/262] add null annotaions to Handle --- .../RemoteBuildConfiguration.java | 5 +- .../pipeline/Handle.java | 49 +++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 080954f5..17266f38 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -691,9 +691,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); - Handle handle = new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer); - handle.setJobMetadata(remoteJobMetadata); - return handle; + return new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); } /** @@ -883,6 +881,7 @@ public BuildData getBuildData(@Nonnull String queueId, @Nonnull BuildContext con return buildData; } + @Nonnull public BuildStatus getBuildStatus(String buildUrlString, BuildContext context) throws IOException { BuildStatus buildStatus = BuildStatus.UNKNOWN; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 529a90a9..21884b8f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -12,6 +12,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; @@ -35,23 +36,35 @@ public class Handle implements Serializable { private static final long serialVersionUID = 4418782245518194292L; + @Nonnull private final RemoteBuildConfiguration remoteBuildConfiguration; + @Nonnull private final String queueId; //Available once moved from queue to an executor + @Nullable private BuildData buildData; + @Nullable private BuildStatus buildStatus; + @Nullable private String jobName; + @Nullable private String jobFullName; + @Nullable private String jobDisplayName; + @Nullable private String jobFullDisplayName; + @Nullable private String jobUrl; /** * The current local Item (Job, Pipeline,...) where this plugin is currently used. */ + @Nonnull private final String currentItem; + + @Nonnull private final RemoteJenkinsServer effectiveRemoteServer; /* @@ -61,18 +74,25 @@ public class Handle implements Serializable { * already finished. * TODO: Once we found a way to log to the pipeline log directly we can switch */ + @Nonnull private String lastLog; - public Handle(RemoteBuildConfiguration remoteBuildConfiguration, String queueId, @Nonnull String currentItem, @Nonnull RemoteJenkinsServer effectiveRemoteServer) + public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnull String queueId, @Nonnull String currentItem, + @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nonnull JSONObject remoteJobMetadata) { this.remoteBuildConfiguration = remoteBuildConfiguration; this.queueId = queueId; this.buildData = null; this.buildStatus = null; - this.lastLog = ""; + this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); + this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); + this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); + this.jobFullDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "fullDisplayName"); + this.jobUrl = getParameterFromJobMetadata(remoteJobMetadata, "url"); this.currentItem = currentItem; this.effectiveRemoteServer = effectiveRemoteServer; + this.lastLog = ""; if(trimToNull(currentItem) == null) throw new IllegalArgumentException("currentItem null"); } @@ -125,26 +145,31 @@ public String getConfiguredJobNameOrUrl() { return remoteBuildConfiguration.getJob(); } + @CheckForNull public String getJobName() { return jobName; } + @CheckForNull public String getJobFullName() { return jobFullName; } + @CheckForNull public String getJobDisplayName() { return jobDisplayName; } + @CheckForNull public String getJobFullDisplayName() { return jobFullDisplayName; } + @CheckForNull public String getJobUrl() { return jobUrl; @@ -153,6 +178,7 @@ public String getJobUrl() /** * @return the name of the remote job. */ + @Nonnull public String getQueueId() { return queueId; } @@ -169,7 +195,7 @@ public String getQueueId() { * @throws InterruptedException * if any thread has interrupted the current thread. */ - @CheckForNull + @Nonnull @Whitelisted public URL getBuildUrl() throws IOException, InterruptedException { //Return if we already have the buildData @@ -225,7 +251,7 @@ public int getBuildNumber() throws IOException, InterruptedException { * @throws InterruptedException * if any thread has interrupted the current thread. */ - @CheckForNull + @Nonnull @Whitelisted public BuildStatus getBuildStatus() throws IOException, InterruptedException { return getBuildStatus(false); @@ -243,11 +269,13 @@ public BuildStatus getBuildStatus() throws IOException, InterruptedException { * @throws InterruptedException * if any thread has interrupted the current thread. */ + @Nonnull @Whitelisted public BuildStatus getBuildStatusBlocking() throws IOException, InterruptedException { return getBuildStatus(true); } + @Nonnull private BuildStatus getBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { //Return if buildStatus exists and is final (does not change anymore) if(buildStatus != null && isFinishedBuildStatus(buildStatus)) return buildStatus; @@ -289,6 +317,7 @@ private boolean isFinishedBuildStatus(BuildStatus buildStatus) * * @return The latest log entries from the last called method. */ + @Nonnull @Whitelisted public String lastLog() { String log = lastLog.trim(); @@ -338,6 +367,7 @@ public static String help() { return sb.toString(); } + @Nonnull private BuildData getBuildData(String queueId, PrintStream logger) throws IOException, InterruptedException { //Return if we already have the buildData @@ -369,7 +399,6 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, if(isEmpty(filename)) return null; URL remoteBuildUrl = getBuildUrl(); - if(remoteBuildUrl == null) return null; URL fileUrl = new URL(remoteBuildUrl, "artifact/" + filename); PrintStreamWrapper log = new PrintStreamWrapper(); @@ -381,15 +410,7 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, } } - public void setJobMetadata(JSONObject remoteJobMetadata) - { - this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); - this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); - this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); - this.jobFullDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "fullDisplayName"); - this.jobUrl = getParameterFromJobMetadata(remoteJobMetadata, "url"); - } - + @CheckForNull private String getParameterFromJobMetadata(JSONObject remoteJobMetadata, String string) { try { From 719cab83c42516f761ed7ebc9d85312f05c0d026 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Tue, 17 Apr 2018 12:17:44 +0200 Subject: [PATCH 069/262] Renamed findEffectiveRemoteHost to evaluateEffectiveRemoteHost --- .../RemoteBuildConfiguration.java | 4 +- .../pipeline/RemoteBuildPipelineStep.java | 2 +- .../RemoteBuildConfigurationTest.java | 46 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 17266f38..43b719dd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -359,7 +359,7 @@ private String buildUrlQueryString(Collection parameters) { * if remoteJenkinsName no valid URL or job an URL but nor valid. */ @Nonnull - public RemoteJenkinsServer findEffectiveRemoteHost(BasicBuildContext context) throws IOException { + public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context) throws IOException { RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); RemoteJenkinsServer server = globallyConfiguredServer; String expandedJob = getJobExpanded(context); @@ -612,7 +612,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListene public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { - RemoteJenkinsServer effectiveRemoteServer = findEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + RemoteJenkinsServer effectiveRemoteServer = evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); Handle handle = performTriggerAndGetQueueId(context); performWaitForBuild(context, handle); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index d224d1cf..c358ea26 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -223,7 +223,7 @@ public static class Execution extends SynchronousNonBlockingStepExecution build = stepContext.get(Run.class); FilePath workspace = stepContext.get(FilePath.class); TaskListener listener = stepContext.get(TaskListener.class); - RemoteJenkinsServer effectiveRemoteServer = remoteBuildConfig.findEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + RemoteJenkinsServer effectiveRemoteServer = remoteBuildConfig.evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); Handle handle = remoteBuildConfig.performTriggerAndGetQueueId(context); if(remoteBuildConfig.getBlockBuildUntilComplete()) { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 70468beb..f8f1c1e9 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -186,7 +186,7 @@ public void testJobUrlHandling_withoutServer() throws IOException { config.setJob("MyJob"); assertEquals("MyJob", config.getJob()); try { - config.findEffectiveRemoteHost(null); + config.evaluateEffectiveRemoteHost(null); fail("findRemoteHost() should throw an AbortException since server not specified"); } catch(AbortException e) { assertEquals("Configuration of the remote Jenkins host is missing.", e.getMessage()); @@ -199,7 +199,7 @@ public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://test:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -210,7 +210,7 @@ public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -221,7 +221,7 @@ public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOEx config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("A/B/C/D/MyJob", config.getJob()); - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -229,7 +229,7 @@ public void testJobUrlHandling_withJobUrl() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("http://test:8080/job/folder/job/MyJob"); assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" - assertEquals("http://test:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -239,7 +239,7 @@ public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { config.setJob("http://testA:8080/job/folder/job/MyJobA"); config.setRemoteJenkinsUrl("http://testB:8080"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins @@ -251,18 +251,18 @@ public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_withoutJob() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + public void testEvaluateEffectiveRemoteHost_withoutJob() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { try { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("xxx"); Field field = config.getClass().getDeclaredField("job"); field.setAccessible(true); field.set(config, ""); - config.findEffectiveRemoteHost(null); + config.evaluateEffectiveRemoteHost(null); fail("findRemoteHost() should throw an AbortException since job not specified"); } catch(AbortException e) { assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); @@ -276,19 +276,19 @@ public void testRemoteUrlOverridesRemoteName() throws IOException { config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("http://globallyConfigured:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://globallyConfigured:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); //Now override remote host URL config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_jobNameMissing() throws IOException { + public void testEvaluateEffectiveRemoteHost_jobNameMissing() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); try { - config.findEffectiveRemoteHost(null); + config.evaluateEffectiveRemoteHost(null); } catch (AbortException e) { assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); @@ -296,14 +296,14 @@ public void testFindEffectiveRemoteHost_jobNameMissing() throws IOException { } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_globalConfigMissing() throws IOException { + public void testEvaluateEffectiveRemoteHost_globalConfigMissing() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("notConfiguredRemoteHost"); try { - config.findEffectiveRemoteHost(null); + config.evaluateEffectiveRemoteHost(null); } catch (AbortException e) { assertEquals("Could get remote host with ID 'notConfiguredRemoteHost' configured in Jenkins global configuration. Please check your global configuration.", e.getMessage()); @@ -311,41 +311,41 @@ public void testFindEffectiveRemoteHost_globalConfigMissing() throws IOException } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL() throws IOException { + public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("notConfiguredRemoteHost"); config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); - assertEquals("http://locallyOverridden:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL() throws IOException { + public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("http://localJobUrl:8080/job/MyJob"); config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("notConfiguredRemoteHost"); - assertEquals("http://localJobUrl:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://localJobUrl:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_localOverrideHostURL() throws IOException { + public void testEvaluateEffectiveRemoteHost_localOverrideHostURL() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://hostname:8080"); - assertEquals("http://hostname:8080", config.findEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://hostname:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); } @Test @WithoutJenkins - public void testFindEffectiveRemoteHost_localOverrideHostURLWrong() throws IOException { + public void testEvaluateEffectiveRemoteHost_localOverrideHostURLWrong() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config.setRemoteJenkinsUrl("hostname:8080"); try { - config.findEffectiveRemoteHost(null); + config.evaluateEffectiveRemoteHost(null); fail("Expected AbortException"); } catch (AbortException e) { From e9b9fee7bdcb29478eec649988d56a8b0ef3b304 Mon Sep 17 00:00:00 2001 From: Alexander Link Date: Thu, 19 Apr 2018 11:07:08 +0200 Subject: [PATCH 070/262] Removed commented test --- .../RemoteBuildConfigurationTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index f8f1c1e9..aced9d9b 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -78,12 +78,6 @@ public void testRemoteBuildWithAuthentication() throws Exception { _testRemoteBuild(true); } -// @Test -// public void testRemoteBuildWithMissingAuthentication() throws Exception { -// enableAuth(); -// _testRemoteBuild(false); -// } - @Test public void testRemoteBuildWithCrumb() throws Exception { disableAuth(); From f240686e53a36c70ad8827e03574978aa5aea25d Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 25 Apr 2018 11:28:34 +0200 Subject: [PATCH 071/262] cleanup dependencies --- pom.xml | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index 90611b96..ce37adfc 100644 --- a/pom.xml +++ b/pom.xml @@ -78,15 +78,9 @@ 2.0 - org.jenkins-ci.plugins.workflow - workflow-job - 2.7 - true - - - org.jenkins-ci.plugins.workflow - workflow-cps - 2.18 + org.jenkins-ci.plugins + script-security + 1.34 true @@ -95,19 +89,6 @@ 2.9 true - - - org.jenkins-ci.plugins.workflow - workflow-support - 2.6 - true - - - org.jenkins-ci.plugins - structs - 1.5 - true - org.mockito mockito-core @@ -120,12 +101,6 @@ - - org.mortbay.jetty - jetty-util - 6.1.26 - test - From 4787f05f4666e3004c09509ba7f981dd7a10751e Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 25 Apr 2018 14:08:24 +0200 Subject: [PATCH 072/262] update dependencies to latest version --- pom.xml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index ce37adfc..5e8b3ab7 100644 --- a/pom.xml +++ b/pom.xml @@ -70,12 +70,12 @@ org.jenkins-ci.plugins credentials - 2.1.4 + 2.1.16 org.jenkins-ci.plugins token-macro - 2.0 + 2.3 org.jenkins-ci.plugins @@ -86,20 +86,14 @@ org.jenkins-ci.plugins.workflow workflow-step-api - 2.9 + 2.13 true org.mockito mockito-core - 1.10.19 + 2.18.3 test - - - hamcrest-core - org.hamcrest - - From 0bce588b12bdef52a1764cde0f1ab789b525b4b1 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Fri, 4 May 2018 17:56:54 +0200 Subject: [PATCH 073/262] review remote address --- .../RemoteBuildConfiguration.java | 24 ++++++++++++---- .../RemoteJenkinsServer.java | 18 ------------ .../pipeline/Handle.java | 2 +- .../RemoteBuildConfigurationTest.java | 28 +++++++++---------- .../RemoteJenkinsServerTest.java | 2 -- 5 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 43b719dd..3bc930a1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -92,7 +92,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep */ private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; - + private static final int DEFAULT_POLLINTERVALL = 10; private static final String paramerizedBuildUrl = "/buildWithParameters"; private static final String normalBuildUrl = "/build"; @@ -502,7 +502,10 @@ private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collec if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { // start building the proper URL based on known capabiltiies of the remote server - triggerUrlString = context.effectiveRemoteServer.getRemoteAddress(); + if (context.effectiveRemoteServer.getAddress() == null) { + throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); + } + triggerUrlString = context.effectiveRemoteServer.getAddress(); triggerUrlString += buildTokenRootUrl; triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? @@ -815,7 +818,10 @@ public void performWaitForBuild(BuildContext context, Handle handle) private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) throws IOException { - String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getRemoteAddress(), queueId); + if (context.effectiveRemoteServer.getAddress() == null) { + throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); + } + String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getAddress(), queueId); ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); JSONObject queueResponse = response.getBody(); @@ -1187,7 +1193,10 @@ private String readInputStream(HttpURLConnection connection) throws IOException @Nonnull private JenkinsCrumb getCrumb(BuildContext context) throws IOException { - String address = context.effectiveRemoteServer.getRemoteAddress(); + String address = context.effectiveRemoteServer.getAddress(); + if (address == null) { + throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); + } URL crumbProviderUrl; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); @@ -1461,7 +1470,7 @@ private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IO return isParameterized; } - protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) + protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) throws AbortException { if(isEmpty(jobNameOrUrl)) throw new IllegalArgumentException("Invalid job name/url: " + jobNameOrUrl); String remoteJobUrl; @@ -1469,7 +1478,10 @@ protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String if(FormValidationUtils.isURL(_jobNameOrUrl)) { remoteJobUrl = _jobNameOrUrl; } else { - remoteJobUrl = remoteServer.getRemoteAddress(); + remoteJobUrl = remoteServer.getAddress(); + if (remoteJobUrl == null) { + throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); + } while(remoteJobUrl.endsWith("/")) remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length()-1); String[] split = _jobNameOrUrl.trim().split("/"); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 240aa33a..a0e89d7b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -135,24 +135,6 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } - /** - * @return the remote server address - * @throws RuntimeException - * if the address of the remote server was not set - */ - @Nonnull - public String getRemoteAddress() { - if (address == null) { - throw new RuntimeException("The remote address can not be empty."); - } else { - try { - new URL(address); - } catch (MalformedURLException e) { - throw new RuntimeException("Malformed address (" + address + "). Remember to indicate the protocol, i.e. http, https, etc."); - } - } - return address; - } @Extension public static class DescriptorImpl extends Descriptor { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 21884b8f..0706444b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -335,7 +335,7 @@ public void setBuildData(BuildData buildData) public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getRemoteAddress(), queueId)); + sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getAddress(), queueId)); if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); sb.append("]"); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index aced9d9b..61be7dcf 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -193,7 +193,7 @@ public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://test:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -204,7 +204,7 @@ public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -215,7 +215,7 @@ public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOEx config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("A/B/C/D/MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -223,7 +223,7 @@ public void testJobUrlHandling_withJobUrl() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("http://test:8080/job/folder/job/MyJob"); assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -233,7 +233,7 @@ public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { config.setJob("http://testA:8080/job/folder/job/MyJobA"); config.setRemoteJenkinsUrl("http://testB:8080"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -245,7 +245,7 @@ public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { config.setRemoteJenkinsName("remoteJenkinsName"); assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -270,12 +270,12 @@ public void testRemoteUrlOverridesRemoteName() throws IOException { config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("http://globallyConfigured:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://globallyConfigured:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); //Now override remote host URL config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); assertEquals("MyJob", config.getJob()); - assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -312,7 +312,7 @@ public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideHos config.setRemoteJenkinsName("notConfiguredRemoteHost"); config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); - assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -322,7 +322,7 @@ public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideJob config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); config.setRemoteJenkinsName("notConfiguredRemoteHost"); - assertEquals("http://localJobUrl:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://localJobUrl:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -330,7 +330,7 @@ public void testEvaluateEffectiveRemoteHost_localOverrideHostURL() throws IOExce RemoteBuildConfiguration config = new RemoteBuildConfiguration(); config.setJob("MyJob"); config.setRemoteJenkinsUrl("http://hostname:8080"); - assertEquals("http://hostname:8080", config.evaluateEffectiveRemoteHost(null).getRemoteAddress()); + assertEquals("http://hostname:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } @Test @WithoutJenkins @@ -384,7 +384,7 @@ public void testRemoveHashParameters() { } @Test @WithoutJenkins - public void testGenerateJobUrl() throws MalformedURLException { + public void testGenerateJobUrl() throws MalformedURLException, AbortException { RemoteJenkinsServer remoteServer = new RemoteJenkinsServer(); remoteServer.setAddress("https://server:8080/jenkins"); @@ -411,8 +411,8 @@ public void testGenerateJobUrl() throws MalformedURLException { try { RemoteJenkinsServer missingUrl = new RemoteJenkinsServer(); RemoteBuildConfiguration.generateJobUrl(missingUrl, "JobName"); - Assert.fail("Expected RuntimeException"); - } catch(RuntimeException e) {} + Assert.fail("Expected AbortException"); + } catch(AbortException e) {} } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index 2f69b585..9a741577 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -37,8 +37,6 @@ public void testCloneBehaviour() throws Exception { verifyEqualsHashCode(server, clone); assertEquals("address", ADDRESS, clone.getAddress()); assertEquals("address", server.getAddress(), clone.getAddress()); - assertEquals("remoteAddress", ADDRESS, clone.getRemoteAddress()); - assertEquals("remoteAddress", server.getRemoteAddress(), clone.getRemoteAddress()); assertEquals("auth2", server.getAuth2(), clone.getAuth2()); assertEquals("displayName", DISPLAY_NAME, clone.getDisplayName()); assertEquals("displayName", server.getDisplayName(), clone.getDisplayName()); From 6f9944bc6917c334b0a063c76de7f9fc822d2b15 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 13 May 2018 14:54:32 +0800 Subject: [PATCH 074/262] add the test of triggering remote jobs in a folder --- .../RemoteBuildConfigurationTest.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 61be7dcf..b9fa9e8a 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; +import org.jvnet.hudson.test.MockFolder; import org.jvnet.hudson.test.WithoutJenkins; import hudson.AbortException; @@ -85,7 +86,7 @@ public void testRemoteBuildWithCrumb() throws Exception { _testRemoteBuild(false); } - private void _testRemoteBuild(boolean authenticate) throws Exception { + private void _testRemoteBuild(boolean authenticate, FreeStyleProject remoteProject) throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); @@ -95,11 +96,6 @@ private void _testRemoteBuild(boolean authenticate) throws Exception { jenkinsRule.jenkins.getDescriptorByType(RemoteBuildConfiguration.DescriptorImpl.class); descriptor.setRemoteSites(remoteJenkinsServer); - FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); - remoteProject.addProperty(new ParametersDefinitionProperty( - new StringParameterDefinition("parameterName1", "default1"), - new StringParameterDefinition("parameterName2", "default2"))); - FreeStyleProject project = jenkinsRule.createFreeStyleProject(); RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); configuration.setJob(remoteProject.getFullName()); @@ -135,6 +131,14 @@ private void _testRemoteBuild(boolean authenticate) throws Exception { assertEquals("value2", remoteEnv.get("parameterName2")); } + private void _testRemoteBuild(boolean authenticate) throws Exception { + FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); + remoteProject.addProperty( + new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), + new StringParameterDefinition("parameterName2", "default2"))); + _testRemoteBuild(authenticate, remoteProject); + } + @Test @WithoutJenkins public void testDefaults() throws IOException { @@ -416,5 +420,17 @@ public void testGenerateJobUrl() throws MalformedURLException, AbortException { } + @Test + public void testRemoteFolderedBuild() throws Exception { + disableAuth(); + + MockFolder remoteJobFolder = jenkinsRule.createFolder("someJobFolder"); + FreeStyleProject remoteProject = remoteJobFolder.createProject(FreeStyleProject.class, "someJobName"); + remoteProject.addProperty( + new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), + new StringParameterDefinition("parameterName2", "default2"))); + + this._testRemoteBuild(false, remoteProject); + } } From 1909ba74b246af957db69824648c9a396781c880 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 14 May 2018 00:09:33 +0800 Subject: [PATCH 075/262] add the test of triggering remote jobs without parameters in a folder --- .../RemoteBuildConfigurationTest.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index b9fa9e8a..2cdb7dd4 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -86,7 +87,7 @@ public void testRemoteBuildWithCrumb() throws Exception { _testRemoteBuild(false); } - private void _testRemoteBuild(boolean authenticate, FreeStyleProject remoteProject) throws Exception { + private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject) throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); @@ -104,7 +105,9 @@ private void _testRemoteBuild(boolean authenticate, FreeStyleProject remoteProje configuration.setBlockBuildUntilComplete(true); configuration.setPollInterval(1); configuration.setEnhancedLogging(true); - configuration.setParameters("parameterName1=value1" + NL_UNIX + "parameterName2=value2"); + if (withParam){ + configuration.setParameters("parameterName1=value1" + NL_UNIX + "parameterName2=value2"); + } if(authenticate) { TokenAuth tokenAuth = new TokenAuth(); tokenAuth.setUserName(testUser.getId()); @@ -126,9 +129,13 @@ private void _testRemoteBuild(boolean authenticate, FreeStyleProject remoteProje FreeStyleBuild lastBuild = remoteProject.getLastBuild(); assertNotNull("lastBuild null", lastBuild); - EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); - assertEquals("value1", remoteEnv.get("parameterName1")); - assertEquals("value2", remoteEnv.get("parameterName2")); + if (withParam){ + EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); + assertEquals("value1", remoteEnv.get("parameterName1")); + assertEquals("value2", remoteEnv.get("parameterName2")); + } else { + assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); + } } private void _testRemoteBuild(boolean authenticate) throws Exception { @@ -136,7 +143,7 @@ private void _testRemoteBuild(boolean authenticate) throws Exception { remoteProject.addProperty( new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), new StringParameterDefinition("parameterName2", "default2"))); - _testRemoteBuild(authenticate, remoteProject); + _testRemoteBuild(authenticate, true, remoteProject); } @Test @WithoutJenkins @@ -430,7 +437,16 @@ public void testRemoteFolderedBuild() throws Exception { new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), new StringParameterDefinition("parameterName2", "default2"))); - this._testRemoteBuild(false, remoteProject); + this._testRemoteBuild(false, true, remoteProject); + } + + @Test + public void testRemoteFolderedBuildWithoutParameters() throws Exception { + disableAuth(); + + MockFolder remoteJobFolder = jenkinsRule.createFolder("someJobFolder1"); + FreeStyleProject remoteProject = remoteJobFolder.createProject(FreeStyleProject.class, "someJobName1"); + this._testRemoteBuild(false, false, remoteProject); } } From 4f482c903c55124fdb6a90b61434b75e2a3d7620 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 14 May 2018 15:29:46 +0800 Subject: [PATCH 076/262] add the remote parameter file support --- .../RemoteBuildConfiguration.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 5d23a70f..c796a5d8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -244,19 +244,18 @@ private List loadExternalParameterFile(BuildContext context) { List parameterList = new ArrayList(); try { - String filePath = String.format("%s/%s", context.workspace, getParameterFile()); + FilePath filePath = context.workspace.child(getParameterFile()); String sCurrentLine; + context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); - context.logger.println(String.format("Loading parameters from file %s", filePath)); - - br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8")); + br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); while ((sCurrentLine = br.readLine()) != null) { parameterList.add(sCurrentLine); } - } catch (IOException e) { + } catch (InterruptedException | IOException e) { context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); - } finally { + } finally { try { if (br != null) { br.close(); From c0d0a8ceae3a7ff615820dfafe965fcde045a2ee Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 15 May 2018 00:00:03 +0800 Subject: [PATCH 077/262] fix findbug issue: check if workspace is null --- .../RemoteBuildConfiguration.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index c796a5d8..5d991433 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -243,16 +243,19 @@ private List loadExternalParameterFile(BuildContext context) { BufferedReader br = null; List parameterList = new ArrayList(); try { + if (context.workspace != null){ + FilePath filePath = context.workspace.child(getParameterFile()); + String sCurrentLine; + context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); - FilePath filePath = context.workspace.child(getParameterFile()); - String sCurrentLine; - context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); + br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); - br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); - - while ((sCurrentLine = br.readLine()) != null) { - parameterList.add(sCurrentLine); - } + while ((sCurrentLine = br.readLine()) != null) { + parameterList.add(sCurrentLine); + } + } else { + context.logger.println("[WARNING] workspace is null"); + } } catch (InterruptedException | IOException e) { context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); } finally { From 1bdc4d57f9728c8fac8d15d689fb0f0d2bed4349 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 15 May 2018 00:26:12 +0800 Subject: [PATCH 078/262] avoid url cache only in loop inquiry --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 5d991433..99c1ad69 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -737,7 +737,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) if (this.getBlockBuildUntilComplete()) { context.logger.println("Blocking local job until remote job completes."); // Form the URL for the triggered job - String jobLocation = jobURL + "api/json/"; + // Only avoid url cache while loop inquiry + String jobLocation = String.format("%sapi/json/?seed=%d", jobURL, System.currentTimeMillis()); buildStatus = getBuildStatus(jobLocation, context); handle.setBuildStatus(buildStatus); From 710b8082e8e539a41f128b2b27fdb279d4fe9887 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 16 May 2018 16:42:37 +0200 Subject: [PATCH 079/262] review build status (#37) * review build status * rename BuildStatus and BuildInfo Renames BuildStatus to RemoteBuildStatus and BuildInfo to RemoteBuildInfo. --- .../RemoteBuildConfiguration.java | 74 +++++++-------- .../pipeline/Handle.java | 66 +++++++------ .../remoteJob/BuildInfoExporterAction.java | 14 +-- .../remoteJob/BuildStatus.java | 92 ------------------- .../remoteJob/RemoteBuildInfo.java | 70 ++++++++++++++ .../remoteJob/RemoteBuildStatus.java | 37 ++++++++ .../pipeline/HandleTest.java | 4 +- .../BuildInfoExporterActionTest.java | 7 +- .../remoteJob/BuildInfoTest.java | 59 ++++++++++++ 9 files changed, 254 insertions(+), 169 deletions(-) delete mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 99c1ad69..3c4c4327 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -42,10 +42,11 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildInfoExporterAction; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; @@ -65,6 +66,7 @@ import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Item; +import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; @@ -727,11 +729,11 @@ public void performWaitForBuild(BuildContext context, Handle handle) int jobNumber = buildData.getBuildNumber(); URL jobURL = buildData.getURL(); - if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, BuildStatus.NOT_BUILT); - // Stores the status of the remote build - BuildStatus buildStatus = BuildStatus.UNKNOWN; - handle.setBuildStatus(buildStatus); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + handle.setBuildInfo(buildInfo); + + if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); // If we are told to block until remoteBuildComplete: if (this.getBlockBuildUntilComplete()) { @@ -740,13 +742,13 @@ public void performWaitForBuild(BuildContext context, Handle handle) // Only avoid url cache while loop inquiry String jobLocation = String.format("%sapi/json/?seed=%d", jobURL, System.currentTimeMillis()); - buildStatus = getBuildStatus(jobLocation, context); - handle.setBuildStatus(buildStatus); + buildInfo = getBuildInfo(jobLocation, context); + handle.setBuildInfo(buildInfo); - if (buildStatus.equals(BuildStatus.NOT_STARTED)) + if (buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED) context.logger.println("Waiting for remote build to start ..."); - while (buildStatus.equals(BuildStatus.NOT_STARTED)) { + while (buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED) { context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) @@ -756,16 +758,16 @@ public void performWaitForBuild(BuildContext context, Handle handle) } catch (InterruptedException e) { this.failBuild(e, context.logger); } - buildStatus = getBuildStatus(jobLocation, context); - handle.setBuildStatus(buildStatus); + buildInfo = getBuildInfo(jobLocation, context); + handle.setBuildInfo(buildInfo); } - context.logger.println("Remote build started!"); - - if (buildStatus.equals(BuildStatus.RUNNING)) + if (buildInfo.getStatus() == RemoteBuildStatus.RUNNING) { + context.logger.println("Remote build started!"); context.logger.println("Waiting for remote build to finish ..."); + } - while (buildStatus.equals(BuildStatus.RUNNING)) { + while (buildInfo.getStatus() == RemoteBuildStatus.RUNNING) { context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) @@ -775,11 +777,11 @@ public void performWaitForBuild(BuildContext context, Handle handle) } catch (InterruptedException e) { this.failBuild(e, context.logger); } - buildStatus = getBuildStatus(jobLocation, context); - handle.setBuildStatus(buildStatus); + buildInfo = getBuildInfo(jobLocation, context); + handle.setBuildInfo(buildInfo); } - context.logger.println("Remote build finished with status " + buildStatus + "."); - if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildStatus); + context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); + if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); if (this.getEnhancedLogging()) { String consoleOutput = getConsoleOutput(jobURL, context); @@ -792,7 +794,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) } // If build did not finish with 'success' then fail build step. - if (!buildStatus.equals(BuildStatus.SUCCESS)) { + if (buildInfo.getResult() != Result.SUCCESS) { // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so will decide how to // handle the failure. this.failBuild(new Exception("The remote job did not succeed."), context.logger); @@ -891,33 +893,23 @@ public BuildData getBuildData(@Nonnull String queueId, @Nonnull BuildContext con } @Nonnull - public BuildStatus getBuildStatus(String buildUrlString, BuildContext context) throws IOException { - BuildStatus buildStatus = BuildStatus.UNKNOWN; + public RemoteBuildInfo getBuildInfo(@Nonnull String buildUrlString, @Nonnull BuildContext context) throws IOException { - //logAuthInformation(context); JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); - // get the next build from the location - try { - if (responseObject == null || responseObject.getString("result") == null && responseObject.getBoolean("building") == false) { - // build not started - buildStatus = BuildStatus.NOT_STARTED; - } else if (responseObject.getBoolean("building")) { - // build running - buildStatus = BuildStatus.RUNNING; - } else if (responseObject.getString("result") != null) { - // build finished - buildStatus = BuildStatus.valueOf(responseObject.getString("result")); - } else { - // Add additional else to check for unhandled conditions - context.logger.println("WARNING: Unhandled condition!"); - } + if (responseObject == null || responseObject.getString("result") == null && !responseObject.getBoolean("building")) { + return new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); + } else if (responseObject.getBoolean("building")) { + return new RemoteBuildInfo(RemoteBuildStatus.RUNNING); + } else if (responseObject.getString("result") != null) { + return new RemoteBuildInfo(responseObject.getString("result")); + } else { + context.logger.println("WARNING: Unhandled condition!"); + } } catch (Exception ex) { - return buildStatus; } - - return buildStatus; + return new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); } private String getConsoleOutput(URL url, BuildContext context) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 0706444b..eaa09a78 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -19,9 +19,11 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildStatus; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import hudson.model.Result; import net.sf.json.JSONException; import net.sf.json.JSONObject; @@ -44,8 +46,8 @@ public class Handle implements Serializable { //Available once moved from queue to an executor @Nullable private BuildData buildData; - @Nullable - private BuildStatus buildStatus; + @Nonnull + private RemoteBuildInfo buildInfo; @Nullable private String jobName; @@ -84,7 +86,7 @@ public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnu this.remoteBuildConfiguration = remoteBuildConfiguration; this.queueId = queueId; this.buildData = null; - this.buildStatus = null; + this.buildInfo = new RemoteBuildInfo(); this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); @@ -134,8 +136,7 @@ public boolean isQueued() throws IOException, InterruptedException { */ @Whitelisted public boolean isFinished() throws IOException, InterruptedException { - BuildStatus buildStatus = getBuildStatus(); - return isFinishedBuildStatus(buildStatus); + return buildInfo.getStatus() == RemoteBuildStatus.FINISHED; } /** @@ -238,11 +239,22 @@ public int getBuildNumber() throws IOException, InterruptedException { } } + /** + * Gets the current build info of the remote job, containing build status and build result. + * + * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo} the build info + */ + @Nonnull + @Whitelisted + public RemoteBuildInfo getBuildInfo() { + return buildInfo; + } + /** * Gets the current build status of the remote job. * - * @return the {@link BuildStatus} - either reflecting a {@link hudson.model.Result} if finished, - * or if not finished yet a custom status like QUEUED, RUNNING,... + * @return {@link hudson.model.Result} the build result + * * @throws IOException * if there is an error retrieving the remote build number, or, * if there is an error retrieving the remote build status, or, @@ -253,14 +265,14 @@ public int getBuildNumber() throws IOException, InterruptedException { */ @Nonnull @Whitelisted - public BuildStatus getBuildStatus() throws IOException, InterruptedException { + public RemoteBuildStatus getBuildStatus() throws IOException, InterruptedException { return getBuildStatus(false); } /** * Gets the build status of the remote build and blocks until it finished. * - * @return the {@link BuildStatus} reflecting a {@link hudson.model.Result}. + * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status * @throws IOException * if there is an error retrieving the remote build number, or, * if there is an error retrieving the remote build status, or, @@ -271,43 +283,45 @@ public BuildStatus getBuildStatus() throws IOException, InterruptedException { */ @Nonnull @Whitelisted - public BuildStatus getBuildStatusBlocking() throws IOException, InterruptedException { + public RemoteBuildStatus getBuildStatusBlocking() throws IOException, InterruptedException { return getBuildStatus(true); } @Nonnull - private BuildStatus getBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { + private RemoteBuildStatus getBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { //Return if buildStatus exists and is final (does not change anymore) - if(buildStatus != null && isFinishedBuildStatus(buildStatus)) return buildStatus; + if(buildInfo.getStatus() == RemoteBuildStatus.FINISHED) return buildInfo.getStatus(); PrintStreamWrapper log = new PrintStreamWrapper(); try { - buildStatus = null; - boolean finished = false; - while(!finished) { + while(buildInfo.getStatus() != RemoteBuildStatus.FINISHED) { //TODO: This currently blocks BuildData buildData = getBuildData(queueId, log.getPrintStream()); String jobLocation = buildData.getURL() + "api/json/"; BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); - buildStatus = remoteBuildConfiguration.getBuildStatus(jobLocation, context); - finished = isFinishedBuildStatus(buildStatus); + buildInfo = remoteBuildConfiguration.getBuildInfo(jobLocation, context); if(!blockUntilFinished) break; } - return buildStatus; + return buildInfo.getStatus(); } finally { lastLog = log.getContent(); } } - public void setBuildStatus(BuildStatus buildStatus) + public void setBuildInfo(RemoteBuildInfo buildInfo) { - this.buildStatus = buildStatus; + this.buildInfo = buildInfo; } - private boolean isFinishedBuildStatus(BuildStatus buildStatus) - { - if(buildStatus == null) return false; - return buildStatus.isJenkinsResult(); + /** + * Gets the current build result of the remote job. + * + * @return {@link hudson.model.Result} the build result + */ + @Nonnull + @Whitelisted + public Result getBuildResult() { + return buildInfo.getResult(); } /** @@ -336,7 +350,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getAddress(), queueId)); - if(buildStatus != null) sb.append(String.format(", buildStatus=%s", buildStatus)); + if(buildInfo != null) sb.append(String.format(", %s", buildInfo.toString())); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); sb.append("]"); return sb.toString(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java index d2c7ad5d..7b0646f1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java @@ -33,8 +33,8 @@ public BuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { addBuildReferenceSafe(buildRef); } - public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, BuildStatus buildResult) { - BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildResult); + public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { + BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildInfo); BuildInfoExporterAction action; synchronized(parentBuild) { @@ -86,13 +86,13 @@ public void addBuildReference(BuildReference buildRef) { public static class BuildReference { public final String projectName; public final int buildNumber; - public final BuildStatus buildResult; + public final RemoteBuildInfo buildInfo; public final URL jobURL; - public BuildReference(String projectName, int buildNumber, URL jobURL, BuildStatus buildResult) { + public BuildReference(String projectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { this.projectName = projectName; this.buildNumber = buildNumber; - this.buildResult = buildResult; + this.buildInfo = buildInfo; this.jobURL = jobURL; } } @@ -122,7 +122,7 @@ public void buildEnvVars(AbstractBuild build, EnvVars env) { for (BuildReference br : refs) { if (br.buildNumber != 0) { String tiggeredBuildRunResultKey = BUILD_RESULT_VARIABLE_PREFIX + sanatizedProjectName + RUN + Integer.toString(br.buildNumber); - env.put(tiggeredBuildRunResultKey, br.buildResult.toString()); + env.put(tiggeredBuildRunResultKey, br.buildInfo.getResult().toString()); } } BuildReference lastBuild = null; @@ -136,7 +136,7 @@ public void buildEnvVars(AbstractBuild build, EnvVars env) { env.put(JOB_NAME_VARIABLE, lastBuild.projectName); env.put(BUILD_NUMBER_VARIABLE_PREFIX + sanatizedProjectName, Integer.toString(lastBuild.buildNumber)); env.put(BUILD_URL_VARIABLE_PREFIX + sanatizedProjectName, lastBuild.jobURL.toString()); - env.put(BUILD_RESULT_VARIABLE_PREFIX + sanatizedProjectName, lastBuild.buildResult.toString()); + env.put(BUILD_RESULT_VARIABLE_PREFIX + sanatizedProjectName, lastBuild.buildInfo.getResult().toString()); } } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java deleted file mode 100644 index b80b4edc..00000000 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildStatus.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; - -import hudson.model.Result; - -/** - * The build status of a remote build either reflecting a {@link hudson.model.Result} if finished, - * or if not finished yet a custom status like QUEUED, RUNNING,...
- * Using {@link #isJenkinsResult()} it can be checked if the status if reflecting a {@link Result} or a custom status.
- * The Jenkins {@link Result} can be obtained using {@link BuildStatus#getJenkinsResult()}. - */ -public enum BuildStatus -{ - - /** - * custom status indicating an UNKNOWN state - */ - UNKNOWN("UNKNOWN"), - - /** - * custom status indicating nothing started yet, neither QUEUED nor RUNNING - */ - NOT_STARTED("NOT_STARTED"), - - /** - * custom status indicating the remote build is in the QUEUE but not running yet - */ - QUEUED("QUEUED"), - - /** - * custom status indicating the build is RUNNING currently. - */ - RUNNING("RUNNING"), - - /** - * Status corresponding to the Jenkins Result.ABORTED - */ - ABORTED(Result.ABORTED), - - /** - * Status corresponding to the Jenkins Result.FAILURE - */ - FAILURE(Result.FAILURE), - - /** - * Status corresponding to the Jenkins Result.NOT_BUILT - */ - NOT_BUILT(Result.NOT_BUILT), - - /** - * Status corresponding to the Jenkins Result.SUCCESS - */ - SUCCESS(Result.SUCCESS), - - /** - * Status corresponding to the Jenkins Result.UNSTABLE - */ - UNSTABLE(Result.UNSTABLE); - - - private final String id; - private final Result jenkinsResult; - - private BuildStatus(String id) { - this.id = id; - this.jenkinsResult = null; - } - - private BuildStatus(Result jenkinsResult) { - this.id = jenkinsResult.toString(); - this.jenkinsResult = jenkinsResult; - } - - /** - * @return The corresponding Jenkins {@link Result} or null if it is a custom status - */ - public Result getJenkinsResult() { - return jenkinsResult; - } - - /** - * @return true if it reflects a Jenkins {@link Result} or false if it is a custom status - */ - public boolean isJenkinsResult() { - return jenkinsResult != null; - } - - @Override - public String toString() { - return id; - } - -} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java new file mode 100644 index 00000000..3b5c692e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -0,0 +1,70 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.io.Serializable; + +import javax.annotation.Nonnull; + +import hudson.model.Result; + +/** + * Remote build info, containing build status and build result. + * + */ +public class RemoteBuildInfo implements Serializable +{ + private static final long serialVersionUID = -5177308623227407314L; + + @Nonnull + private RemoteBuildStatus status; + + @Nonnull + private Result result; + + + public RemoteBuildInfo() + { + status = RemoteBuildStatus.NOT_STARTED; + result = Result.NOT_BUILT; + } + + public RemoteBuildInfo(RemoteBuildStatus status) + { + this.status = status; + if (status == RemoteBuildStatus.FINISHED) { + throw new IllegalArgumentException("It is not possible to set the status to finished without setting the build result. " + + "Please use BuildInfo(Result result) or BuildInfo(String result) in order to set the status to finished."); + } else { + this.result = Result.NOT_BUILT; + } + } + + public RemoteBuildInfo(Result result) + { + this.status = RemoteBuildStatus.FINISHED; + this.result = result; + } + + public RemoteBuildInfo(String result) + { + this.status = RemoteBuildStatus.FINISHED; + this.result = Result.fromString(result); + } + + public RemoteBuildStatus getStatus() + { + return status; + } + + public Result getResult() + { + return result; + } + + @Override + public String toString() + { + if (status == RemoteBuildStatus.FINISHED) return String.format("status=%s, result=%s", status.toString(), result.toString()); + else return String.format("status=%s", status.toString()); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java new file mode 100644 index 00000000..3eb58668 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java @@ -0,0 +1,37 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +/** + * The build status of a remote build. + */ +public enum RemoteBuildStatus +{ + + /** + * Nothing started yet, neither QUEUED nor RUNNING + */ + NOT_STARTED("NOT_STARTED"), + + /** + * The build is RUNNING currently. + */ + RUNNING("RUNNING"), + + /** + * The build is RUNNING currently. + */ + FINISHED("FINISHED"); + + + private final String id; + + + private RemoteBuildStatus(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java index 7a2b9aca..54fab1e4 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java @@ -13,7 +13,9 @@ public void testHelp() { String help = Handle.help(); //Check only a few to see if it works in general assertContains(help, true, "- String toString()"); - assertContains(help, true, "- BuildStatus getBuildStatus()"); + assertContains(help, true, "- RemoteBuildInfo getBuildInfo()"); + assertContains(help, true, "- RemoteBuildStatus getBuildStatus()"); + assertContains(help, true, "- Result getBuildResult()"); assertContains(help, true, "- URL getBuildUrl()"); assertContains(help, true, "- int getBuildNumber()"); assertContains(help, true, "- boolean isFinished()"); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 11d58f9e..fceee255 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -20,6 +20,7 @@ import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.ItemGroup; +import hudson.model.Result; import hudson.model.Run; import hudson.model.TopLevelItem; import jenkins.model.Jenkins; @@ -39,8 +40,9 @@ public class BuildInfoExporterActionTest { @Test public void testAddBuildInfoExporterAction_sequential() throws IOException { Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); for (int i = 1; i <= PARALLEL_JOBS; i++) { - BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); } BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); EnvVars env = new EnvVars(); @@ -135,8 +137,9 @@ public AddActionCallable(Run parentBuild, int i) { public Boolean call() throws MalformedURLException { String jobName = "Job" + i; + RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, - new URL("http://jenkins/jobs/Job" + i), BuildStatus.SUCCESS); + new URL("http://jenkins/jobs/Job" + i), buildInfo); System.out.println("AddActionCallable finished for Job" + i); BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java new file mode 100644 index 00000000..f0153e8f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java @@ -0,0 +1,59 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import hudson.model.Result; + + +public class BuildInfoTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + + @Test + public void buildStatusTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); + assert(buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED); + assert(buildInfo.getResult() == Result.NOT_BUILT); + } + + @Test + public void illegalBuildStatusTest() { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("It is not possible to set the status to finished without setting the build result. " + + "Please use BuildInfo(Result result) or BuildInfo(String result) in order to set the status to finished."); + + new RemoteBuildInfo(RemoteBuildStatus.FINISHED); + } + + @Test + public void buildResultTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); + assert(buildInfo.getStatus() == RemoteBuildStatus.FINISHED); + assert(buildInfo.getResult() == Result.SUCCESS); + } + + @Test + public void stringBuildResultTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo("SUCCESS"); + assert(buildInfo.getStatus() == RemoteBuildStatus.FINISHED); + assert(buildInfo.getResult() == Result.SUCCESS); + } + + @Test + public void buildInfoTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); + assert(buildInfo.toString().equals("status=NOT_STARTED")); + + buildInfo = new RemoteBuildInfo(Result.SUCCESS); + assert(buildInfo.toString().equals("status=FINISHED, result=SUCCESS")); + } +} From 905fc20b2d70e293aa26b446c92fa4556bac01cc Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 16 May 2018 22:58:02 +0800 Subject: [PATCH 080/262] update maintainer for release --- pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 5e8b3ab7..72a30096 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,10 @@ morficus Maurice Williams + + cashlalala + KaiHsiang Chang + From a633377fbb5e6074ce2272ac1a5a60d1d4d6d179 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 17 May 2018 00:09:07 +0800 Subject: [PATCH 081/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 72a30096..897f9fb1 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.0-SNAPSHOT + 3.0.0 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.0 From 58b0da08c35ee46d6358914860b61134d85181e3 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 17 May 2018 00:11:25 +0800 Subject: [PATCH 082/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 897f9fb1..cf76f031 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.0 + 3.0.1-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.0 + HEAD From 8238d6c98b63081106c98630d3307728b281d84e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 17 May 2018 00:48:05 +0800 Subject: [PATCH 083/262] update the change log --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d3ee50..9fb876b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,35 @@ -#2.2.2 (Aug 16th, 2015) +# 3.0.0 (May 17th, 2018) +### New feature +* Pipeline support + +### Improvement +- [JENKINS-24240](https://issues.jenkins-ci.org/browse/JENKINS-24240) +- [JENKINS-29219](https://issues.jenkins-ci.org/browse/JENKINS-29219) +- [JENKINS-29220](https://issues.jenkins-ci.org/browse/JENKINS-29220) +- [JENKINS-29222](https://issues.jenkins-ci.org/browse/JENKINS-29222) + +### Bug fixes +- [JENKINS-29381](https://issues.jenkins-ci.org/browse/JENKINS-29381) +- [JENKINS-30962](https://issues.jenkins-ci.org/browse/JENKINS-30962) +- [JENKINS-32462](https://issues.jenkins-ci.org/browse/JENKINS-32462) +- [JENKINS-32671](https://issues.jenkins-ci.org/browse/JENKINS-32671) +- [JENKINS-33269](https://issues.jenkins-ci.org/browse/JENKINS-33269) +- [JENKINS-47919 ](https://issues.jenkins-ci.org/browse/JENKINS-47919) + + +# 2.2.2 (Aug 16th, 2015) ### Misc: - require Jenkins 1.580+ -###Bug fixes: +### Bug fixes: - 2.2.0 didn't make it to the update center -#2.2.0 (May 12th, 2015) -###New Feature/Enhancement: +# 2.2.0 (May 12th, 2015) +### New Feature/Enhancement: - Ability to debug connection errors with (optional) enhanced console output ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/9)) -###Bug fixes: +### Bug fixes: - fixing [JENKINS-23748](https://issues.jenkins-ci.org/browse/JENKINS-23748) - Better error handleing for console output and logs to display info about the failure ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/10)) - Don't fail build on 404 ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/8)) - Fixed unhandled NPE ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/7)) @@ -20,25 +39,25 @@ * fixing [JENKINS-25366](https://issues.jenkins-ci.org/browse/JENKINS-25366) -#2.1.3 (July 6th, 2014) -###Bug fixes: +# 2.1.3 (July 6th, 2014) +### Bug fixes: - merging [pull request #4](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/4) -#2.1.2 (April 26th, 2014) -###Bug fixes: +# 2.1.2 (April 26th, 2014) +### Bug fixes: - fixing [JENKINS-22325](https://issues.jenkins-ci.org/browse/JENKINS-22325) - local job fails when not sending any parameters to remote job - fixing [JENKINS-21470](https://issues.jenkins-ci.org/browse/JENKINS-21470) - UI does not display that a build is using a file to get the parameter list - fixing [JENKINS-22493](https://issues.jenkins-ci.org/browse/JENKINS-22493) - 400 when remote job has default parameters and parameters are not explicitly list them - fixing [JENKINS-22427](https://issues.jenkins-ci.org/browse/JENKINS-22427) - fails when remote job waits for available executor -#2.1 (Feb 17th, 2014) -###New Feature/Enhancement: +# 2.1 (Feb 17th, 2014) +### New Feature/Enhancement: - ability to specify the list of remote parameters from a file ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-21470)) - optionally block the local build until remote build is complete ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-20828)) -###Misc: +### Misc: - the console output has also been cleansed of displaying any URLs, since this could pose a security risk for public CI environemnts. - special thanks to [@tombrown5](https://github.com/timbrown5) for his contributions to the last item mentioned above @@ -47,34 +66,34 @@ Lots of refactoring and addition of some major new features. -###New Feature/Enhancement: +### New Feature/Enhancement: - integration with the 'Credentials' plugin ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-20826)) - able to override global credentials at a job-level ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-20829)) - support for 'Token Macro' plugin ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-20827)) - support for traditional Jenkins environment variables ([request ticket](https://issues.jenkins-ci.org/browse/JENKINS-21125)) -###Misc: +### Misc: Special thanks to [@elksson](https://github.com/elksson) for his contributions to the last 2 items mentioned above and to [@imod](https://github.com/imod) for his awesome feedback and feature suggestions. -#1.1 ( Nov. 30th, 2013) +# 1.1 ( Nov. 30th, 2013) -###Bug fixes: +### Bug fixes: - closing potential security gap for public-read environments -###New Feature/Enhancement: +### New Feature/Enhancement: - ability to not mark the build as failed if the remote build fails -###Misc: +### Misc: - General code clean-up -#1.0 +# 1.0 Initial release -###Available features: +### Available features: - Trigger parameterized build on a remote Jenkins server - Trigger non-parameterized build on a remote Jenkins server - Authentication via username + API token From d4214d444b284a9b881ce024ddaec347fea448c6 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 23 May 2018 09:19:29 +0200 Subject: [PATCH 084/262] review TODO: this currently blocks (#38) * review TODO: this currently blocks Adds the remote build queue status. * add methods for remote build status * update java doc --- .../RemoteBuildConfiguration.java | 132 +++++++++--------- .../pipeline/Handle.java | 132 ++++++------------ .../remoteJob/BuildData.java | 3 + .../remoteJob/QueueItem.java | 2 + .../remoteJob/QueueItemData.java | 11 +- .../remoteJob/RemoteBuildInfo.java | 93 ++++++++++-- .../remoteJob/RemoteBuildQueueStatus.java | 36 +++++ .../remoteJob/RemoteBuildStatus.java | 6 +- .../BuildInfoExporterActionTest.java | 6 +- .../remoteJob/BuildInfoTest.java | 31 ++-- 10 files changed, 263 insertions(+), 189 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 3c4c4327..cdc12440 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -46,6 +46,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildQueueStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; @@ -693,12 +694,16 @@ public Handle performTriggerAndGetQueueId(BuildContext context) } + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); // QueueStatus.NOT_QUEUED + context.logger.println("Triggering remote job now."); ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); + buildInfo.setQueueId(queueItem.getId()); // QueueStatus.QUEUED + buildInfo = updateBuildInfo(buildInfo, context); - return new Handle(this, queueItem.getId(), context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); + return new Handle(this, buildInfo, context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); } /** @@ -720,36 +725,51 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws InterruptedException, IOException { String jobName = handle.getJobName(); - BuildData buildData = getBuildData(handle.getQueueId(), context); - handle.setBuildData(buildData); - context.logger.println(" Remote build URL: " + buildData.getURL()); - context.logger.println(" Remote build number: " + buildData.getBuildNumber()); + RemoteBuildInfo buildInfo = handle.getBuildInfo(); + String queueId = buildInfo.getQueueId(); + if (queueId == null) { + throw new AbortException(String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); + } + context.logger.println(" Remote job queue number: " + buildInfo.getQueueId()); + + if (buildInfo.isQueued()) { + context.logger.println("Waiting for remote build to be executed..."); + } + + while (buildInfo.isQueued()) + { + context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); + Thread.sleep(this.pollInterval * 1000); + buildInfo = updateBuildInfo(buildInfo, context); + handle.setBuildInfo(buildInfo); + } + + BuildData buildData = buildInfo.getBuildData(); + if (buildData == null) { + throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); + } int jobNumber = buildData.getBuildNumber(); URL jobURL = buildData.getURL(); - // Stores the status of the remote build - RemoteBuildInfo buildInfo = new RemoteBuildInfo(); - handle.setBuildInfo(buildInfo); + context.logger.println(" Remote build URL: " + jobURL); + context.logger.println(" Remote build number: " + jobNumber); if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); // If we are told to block until remoteBuildComplete: if (this.getBlockBuildUntilComplete()) { context.logger.println("Blocking local job until remote job completes."); - // Form the URL for the triggered job - // Only avoid url cache while loop inquiry - String jobLocation = String.format("%sapi/json/?seed=%d", jobURL, System.currentTimeMillis()); - buildInfo = getBuildInfo(jobLocation, context); + buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); - if (buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED) + if (buildInfo.isNotStarted()) context.logger.println("Waiting for remote build to start ..."); - while (buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED) { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + while (buildInfo.isNotStarted()) { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { @@ -758,17 +778,17 @@ public void performWaitForBuild(BuildContext context, Handle handle) } catch (InterruptedException e) { this.failBuild(e, context.logger); } - buildInfo = getBuildInfo(jobLocation, context); + buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } - if (buildInfo.getStatus() == RemoteBuildStatus.RUNNING) { - context.logger.println("Remote build started!"); - context.logger.println("Waiting for remote build to finish ..."); + if (buildInfo.isRunning()) { + context.logger.println("Remote build started!"); + context.logger.println("Waiting for remote build to finish ..."); } - while (buildInfo.getStatus() == RemoteBuildStatus.RUNNING) { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + while (buildInfo.isRunning()) { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { @@ -777,7 +797,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) } catch (InterruptedException e) { this.failBuild(e, context.logger); } - buildInfo = getBuildInfo(jobLocation, context); + buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); @@ -834,7 +854,7 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", response.getResponseCode(), queueQuery)); } - QueueItemData queueItem = new QueueItemData(queueResponse); + QueueItemData queueItem = new QueueItemData(context, queueResponse); if (queueItem.isBlocked()) context.logger.println("The remote job is blocked. Reason: " + queueItem.getWhy() + "."); @@ -851,65 +871,45 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo return queueItem; } - /** - * Requests the queue item data till the job is executable and the build data can be retrieved. - * - * @param queueId - * the id of the remote job on the queue. - * @param context - * the context of this Builder/BuildStep. - * @return {@link BuildData} - * the build data containing the build number and build URL. - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if there is an error identifying the remote host, or - * if there is an error setting the authorization header, or - * if the request fails due to an unknown host, unauthorized credentials, or another reason. - * @throws AbortException - * if the queue item response is unexpected. - * @throws MalformedURLException - * if there is an error creating the build URL. - */ @Nonnull - public BuildData getBuildData(@Nonnull String queueId, @Nonnull BuildContext context) - throws IOException, InterruptedException, AbortException, MalformedURLException - { - context.logger.println(" Remote job queue number: " + queueId); - - QueueItemData queueItem = getQueueItemData(queueId, context); - BuildData buildData = queueItem.getBuildData(context); + public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonnull BuildContext context) throws IOException { - context.logger.println("Waiting for remote build to be executed..."); + if (buildInfo.isNotQueued()) return buildInfo; - while (buildData == null) - { - context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); - Thread.sleep(this.pollInterval * 1000); - queueItem = getQueueItemData(queueId, context); - buildData = queueItem.getBuildData(context); + if (buildInfo.isQueued()) { + String queueId = buildInfo.getQueueId(); + if (queueId == null) { + throw new AbortException(String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); + } + QueueItemData queueItem = getQueueItemData(queueId, context); + BuildData buildData = queueItem.getBuildData(context); + if (queueItem.isExecutable() && buildData!=null) { + buildInfo.setBuildData(buildData); // QueueStatus.EXECUTED + } + return buildInfo; } - return buildData; - } - - @Nonnull - public RemoteBuildInfo getBuildInfo(@Nonnull String buildUrlString, @Nonnull BuildContext context) throws IOException { + // QueueStatus.EXECUTED + BuildData buildData = buildInfo.getBuildData(); + if (buildData == null) { + throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); + } + String buildUrlString = buildData.getURL() + "api/json/"; JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); try { if (responseObject == null || responseObject.getString("result") == null && !responseObject.getBoolean("building")) { - return new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); + return buildInfo; } else if (responseObject.getBoolean("building")) { - return new RemoteBuildInfo(RemoteBuildStatus.RUNNING); + buildInfo.setBuildStatus(RemoteBuildStatus.RUNNING); } else if (responseObject.getString("result") != null) { - return new RemoteBuildInfo(responseObject.getString("result")); + buildInfo.setBuildResult(responseObject.getString("result")); } else { context.logger.println("WARNING: Unhandled condition!"); } } catch (Exception ex) { } - return new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); + return buildInfo; } private String getConsoleOutput(URL url, BuildContext context) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index eaa09a78..9df348b8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -40,12 +40,7 @@ public class Handle implements Serializable { @Nonnull private final RemoteBuildConfiguration remoteBuildConfiguration; - @Nonnull - private final String queueId; - //Available once moved from queue to an executor - @Nullable - private BuildData buildData; @Nonnull private RemoteBuildInfo buildInfo; @@ -80,13 +75,11 @@ public class Handle implements Serializable { private String lastLog; - public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnull String queueId, @Nonnull String currentItem, + public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnull RemoteBuildInfo buildInfo, @Nonnull String currentItem, @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nonnull JSONObject remoteJobMetadata) { this.remoteBuildConfiguration = remoteBuildConfiguration; - this.queueId = queueId; - this.buildData = null; - this.buildInfo = new RemoteBuildInfo(); + this.buildInfo = buildInfo; this.jobName = getParameterFromJobMetadata(remoteJobMetadata, "name"); this.jobFullName = getParameterFromJobMetadata(remoteJobMetadata, "fullName"); this.jobDisplayName = getParameterFromJobMetadata(remoteJobMetadata, "displayName"); @@ -109,17 +102,7 @@ public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnu */ @Whitelisted public boolean isQueued() throws IOException, InterruptedException { - //Return if we already have the buildData - if(buildData != null) return false; - - PrintStreamWrapper log = new PrintStreamWrapper(); - try { - //TODO: This currently blocks - getBuildData(queueId, log.getPrintStream()); - return false; - } finally { - lastLog = log.getContent(); - } + return buildInfo.isQueued(); } /** @@ -136,7 +119,7 @@ public boolean isQueued() throws IOException, InterruptedException { */ @Whitelisted public boolean isFinished() throws IOException, InterruptedException { - return buildInfo.getStatus() == RemoteBuildStatus.FINISHED; + return buildInfo.isFinished(); } /** @@ -177,66 +160,35 @@ public String getJobUrl() } /** - * @return the name of the remote job. + * @return the id of the remote job on the queue. */ - @Nonnull + @CheckForNull public String getQueueId() { - return queueId; + return buildInfo.getQueueId(); } /** * Get the build URL of the remote build. * * @return the URL, or null if it could not be identified (yet). - * @throws IOException - * if there is an error retrieving the remote build number, or, - * if there is an error retrieving the remote build status, or, - * if there is an error retrieving the console output of the remote build, or, - * if the remote build does not succeed. - * @throws InterruptedException - * if any thread has interrupted the current thread. */ - @Nonnull + @CheckForNull @Whitelisted - public URL getBuildUrl() throws IOException, InterruptedException { - //Return if we already have the buildData - if(buildData != null) return buildData.getURL(); - - PrintStreamWrapper log = new PrintStreamWrapper(); - try { - //TODO: This currently blocks - BuildData buildData = getBuildData(queueId, log.getPrintStream()); - return buildData.getURL(); - } finally { - lastLog = log.getContent(); - } + public URL getBuildUrl() { + BuildData buildData = buildInfo.getBuildData(); + return buildData == null ? null : buildData.getURL(); } /** * Get the build number of the remote build. * * @return the number, or -1 if it could not be identified (yet). - * @throws IOException - * if there is an error retrieving the remote build number, or, - * if there is an error retrieving the remote build status, or, - * if there is an error retrieving the console output of the remote build, or, - * if the remote build does not succeed. - * @throws InterruptedException - * if any thread has interrupted the current thread. */ + @Nonnull @Whitelisted - public int getBuildNumber() throws IOException, InterruptedException { - //Return if we already have the buildData - if(buildData != null) return buildData.getBuildNumber(); - - PrintStreamWrapper log = new PrintStreamWrapper(); - try { - //TODO: This currently blocks - BuildData buildData = getBuildData(queueId, log.getPrintStream()); - return buildData.getBuildNumber(); - } finally { - lastLog = log.getContent(); - } + public int getBuildNumber() { + BuildData buildData = buildInfo.getBuildData(); + return buildData == null ? -1 : buildData.getBuildNumber(); } /** @@ -254,6 +206,17 @@ public RemoteBuildInfo getBuildInfo() { * Gets the current build status of the remote job. * * @return {@link hudson.model.Result} the build result + */ + @Nonnull + @Whitelisted + public RemoteBuildStatus getBuildStatus() { + return buildInfo.getStatus(); + } + + /** + * Updates the current build status of the remote job. + * + * @return {@link hudson.model.Result} the build result * * @throws IOException * if there is an error retrieving the remote build number, or, @@ -265,12 +228,12 @@ public RemoteBuildInfo getBuildInfo() { */ @Nonnull @Whitelisted - public RemoteBuildStatus getBuildStatus() throws IOException, InterruptedException { - return getBuildStatus(false); + public RemoteBuildStatus updateBuildStatus() throws IOException, InterruptedException { + return updateBuildStatus(false); } /** - * Gets the build status of the remote build and blocks until it finished. + * Updates the build status of the remote build until it is finished. * * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status * @throws IOException @@ -283,23 +246,20 @@ public RemoteBuildStatus getBuildStatus() throws IOException, InterruptedExcepti */ @Nonnull @Whitelisted - public RemoteBuildStatus getBuildStatusBlocking() throws IOException, InterruptedException { - return getBuildStatus(true); + public RemoteBuildStatus updateBuildStatusBlocking() throws IOException, InterruptedException { + return updateBuildStatus(true); } @Nonnull - private RemoteBuildStatus getBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { + private RemoteBuildStatus updateBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { //Return if buildStatus exists and is final (does not change anymore) - if(buildInfo.getStatus() == RemoteBuildStatus.FINISHED) return buildInfo.getStatus(); + if(buildInfo.isFinished()) return buildInfo.getStatus(); PrintStreamWrapper log = new PrintStreamWrapper(); try { - while(buildInfo.getStatus() != RemoteBuildStatus.FINISHED) { - //TODO: This currently blocks - BuildData buildData = getBuildData(queueId, log.getPrintStream()); - String jobLocation = buildData.getURL() + "api/json/"; + while(!buildInfo.isFinished()) { BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); - buildInfo = remoteBuildConfiguration.getBuildInfo(jobLocation, context); + buildInfo = remoteBuildConfiguration.updateBuildInfo(buildInfo, context); if(!blockUntilFinished) break; } return buildInfo.getStatus(); @@ -339,18 +299,14 @@ public String lastLog() { return log; } - public void setBuildData(BuildData buildData) - { - this.buildData = buildData; - } - @Whitelisted @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getAddress(), queueId)); - if(buildInfo != null) sb.append(String.format(", %s", buildInfo.toString())); + sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getAddress(), buildInfo.getQueueId())); + sb.append(String.format(", %s", buildInfo.toString())); + BuildData buildData = buildInfo.getBuildData(); if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); sb.append("]"); return sb.toString(); @@ -381,16 +337,10 @@ public static String help() { return sb.toString(); } - @Nonnull - private BuildData getBuildData(String queueId, PrintStream logger) throws IOException, InterruptedException + @CheckForNull + private BuildData getBuildData(String queueId, PrintStream logger) { - //Return if we already have the buildData - if(buildData != null) return buildData; - - BuildContext context = new BuildContext(logger, effectiveRemoteServer, this.currentItem); - BuildData build = remoteBuildConfiguration.getBuildData(queueId, context); - this.buildData = build; - return build; + return buildInfo.getBuildData(); } /** diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java index 339da8ed..a8229b71 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java @@ -25,16 +25,19 @@ public BuildData(@Nonnull int buildNumber, @Nonnull URL buildURL) this.buildURL = buildURL; } + @Nonnull public int getBuildNumber() { return buildNumber; } + @Nonnull public URL getURL() { return buildURL; } + @Nonnull @Override public String toString() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java index 522e69e7..3026de2c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java @@ -36,10 +36,12 @@ public QueueItem(@Nonnull Map> header) throws AbortException } } + @Nonnull public String getLocation() { return location; } + @Nonnull public String getId() { return id; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java index d8eddb83..5e654933 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -20,10 +20,15 @@ public class QueueItemData @Nonnull private final JSONObject queueResponse; + @Nonnull + private RemoteBuildQueueStatus status; + - public QueueItemData(@Nonnull JSONObject queueResponse) + public QueueItemData(@Nonnull BuildContext context, @Nonnull JSONObject queueResponse) throws MalformedURLException { this.queueResponse = queueResponse; + if (isExecutable() && getBuildData(context)!=null) status = RemoteBuildQueueStatus.EXECUTED; + else status = RemoteBuildQueueStatus.QUEUED; } public boolean isBlocked() @@ -56,6 +61,9 @@ public boolean isExecutable() return (!isBlocked() && !isBuildable() && !isPending() && !isCancelled()); } + public RemoteBuildQueueStatus getQueueStatus() { + return status; + } /** * When a queue item is executable, the build number and the build URL * of the remote job are available in the queue item data. @@ -75,6 +83,7 @@ public BuildData getBuildData(@Nonnull BuildContext context) throws MalformedURL JSONObject remoteJobInfo; try { remoteJobInfo = queueResponse.getJSONObject("executable"); + if (remoteJobInfo == null) return null; } catch (JSONException e) { context.logger.println("The attribute \"executable\" was not found. Unexpected response: " + queueResponse.toString()); return null; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index 3b5c692e..4ba782ad 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -2,18 +2,30 @@ import java.io.Serializable; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import hudson.model.Result; /** - * Remote build info, containing build status and build result. + * The remote build info contains the queue id and the queue status of the remote build, + * while it enters the queue, and the remote job build number, build url, build status and build result, + * when it leaves the queue. * */ public class RemoteBuildInfo implements Serializable { private static final long serialVersionUID = -5177308623227407314L; + @CheckForNull + private String queueId; + + @CheckForNull + private BuildData buildData; + + @Nonnull + private RemoteBuildQueueStatus queueStatus; + @Nonnull private RemoteBuildStatus status; @@ -23,48 +35,99 @@ public class RemoteBuildInfo implements Serializable public RemoteBuildInfo() { + queueId = null; + buildData = null; + queueStatus = RemoteBuildQueueStatus.NOT_QUEUED; status = RemoteBuildStatus.NOT_STARTED; result = Result.NOT_BUILT; } - public RemoteBuildInfo(RemoteBuildStatus status) + @CheckForNull + public String getQueueId() { + return queueId; + } + + @CheckForNull + public BuildData getBuildData() { + return buildData; + } + + @Nonnull + public RemoteBuildQueueStatus getQueueStatus() + { + return queueStatus; + } + + @Nonnull + public RemoteBuildStatus getStatus() + { + return status; + } + + @Nonnull + public Result getResult() + { + return result; + } + + public void setQueueId(String queueId) { + this.queueId = queueId; + this.queueStatus = RemoteBuildQueueStatus.QUEUED; + } + + public void setBuildData(BuildData buildData) { + this.buildData = buildData; + this.queueStatus = RemoteBuildQueueStatus.EXECUTED; + } + + public void setBuildStatus(RemoteBuildStatus status) { - this.status = status; if (status == RemoteBuildStatus.FINISHED) { throw new IllegalArgumentException("It is not possible to set the status to finished without setting the build result. " + "Please use BuildInfo(Result result) or BuildInfo(String result) in order to set the status to finished."); } else { + this.status = status; this.result = Result.NOT_BUILT; } } - public RemoteBuildInfo(Result result) + public void setBuildResult(Result result) { this.status = RemoteBuildStatus.FINISHED; this.result = result; } - public RemoteBuildInfo(String result) + public void setBuildResult(String result) { this.status = RemoteBuildStatus.FINISHED; this.result = Result.fromString(result); } - public RemoteBuildStatus getStatus() + @Nonnull + @Override + public String toString() { - return status; + if (status == RemoteBuildStatus.FINISHED) return String.format("status=%s, result=%s", status.toString(), result.toString()); + else return String.format("queueStatus=%s, status=%s", queueStatus.toString(), status.toString()); } - public Result getResult() - { - return result; + public boolean isNotQueued() { + return queueStatus == RemoteBuildQueueStatus.NOT_QUEUED; } - @Override - public String toString() - { - if (status == RemoteBuildStatus.FINISHED) return String.format("status=%s, result=%s", status.toString(), result.toString()); - else return String.format("status=%s", status.toString()); + public boolean isQueued() { + return queueStatus == RemoteBuildQueueStatus.QUEUED; } + public boolean isNotStarted() { + return status == RemoteBuildStatus.NOT_STARTED; + } + + public boolean isRunning() { + return status == RemoteBuildStatus.RUNNING; + } + + public boolean isFinished() { + return status == RemoteBuildStatus.FINISHED; + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java new file mode 100644 index 00000000..30c39f1c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java @@ -0,0 +1,36 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +/** + * The status of the remote job on the queue. + */ +public enum RemoteBuildQueueStatus +{ + /** + * The remote job was not triggered and it is not on the queue. + */ + NOT_QUEUED("NOT_QUEUED"), + + /** + * The remote job was triggered and it is on the queue waiting to be executed. + */ + QUEUED("QUEUED"), + + /** + * The remote job was executed. + */ + EXECUTED("EXECUTED"); + + + private final String id; + + + private RemoteBuildQueueStatus(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java index 3eb58668..26a5ae2f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java @@ -7,17 +7,17 @@ public enum RemoteBuildStatus { /** - * Nothing started yet, neither QUEUED nor RUNNING + * The remote build did not start. */ NOT_STARTED("NOT_STARTED"), /** - * The build is RUNNING currently. + * The remote build is running currently. */ RUNNING("RUNNING"), /** - * The build is RUNNING currently. + * The remote build is finished. */ FINISHED("FINISHED"); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index fceee255..71576313 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -40,7 +40,8 @@ public class BuildInfoExporterActionTest { @Test public void testAddBuildInfoExporterAction_sequential() throws IOException { Run parentBuild = new FreeStyleBuild(new FreeStyleProject((ItemGroup) Jenkins.getInstance(), "ParentJob")); - RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); for (int i = 1; i <= PARALLEL_JOBS; i++) { BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); } @@ -137,7 +138,8 @@ public AddActionCallable(Run parentBuild, int i) { public Boolean call() throws MalformedURLException { String jobName = "Job" + i; - RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); System.out.println("AddActionCallable finished for Job" + i); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java index f0153e8f..105b335b 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java @@ -16,8 +16,9 @@ public class BuildInfoTest { @Test public void buildStatusTest() { - RemoteBuildInfo buildInfo = new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); - assert(buildInfo.getStatus() == RemoteBuildStatus.NOT_STARTED); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + + assert(buildInfo.isNotStarted()); assert(buildInfo.getResult() == Result.NOT_BUILT); } @@ -28,32 +29,40 @@ public void illegalBuildStatusTest() { thrown.expectMessage("It is not possible to set the status to finished without setting the build result. " + "Please use BuildInfo(Result result) or BuildInfo(String result) in order to set the status to finished."); - new RemoteBuildInfo(RemoteBuildStatus.FINISHED); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildStatus(RemoteBuildStatus.FINISHED); } @Test public void buildResultTest() { - RemoteBuildInfo buildInfo = new RemoteBuildInfo(Result.SUCCESS); - assert(buildInfo.getStatus() == RemoteBuildStatus.FINISHED); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + + assert(buildInfo.isFinished()); assert(buildInfo.getResult() == Result.SUCCESS); } @Test public void stringBuildResultTest() { - RemoteBuildInfo buildInfo = new RemoteBuildInfo("SUCCESS"); - assert(buildInfo.getStatus() == RemoteBuildStatus.FINISHED); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + + assert(buildInfo.isFinished()); assert(buildInfo.getResult() == Result.SUCCESS); } @Test - public void buildInfoTest() { + public void buildInfoToStringTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + + assert(buildInfo.toString().equals("queueStatus=NOT_QUEUED, status=NOT_STARTED")); - RemoteBuildInfo buildInfo = new RemoteBuildInfo(RemoteBuildStatus.NOT_STARTED); - assert(buildInfo.toString().equals("status=NOT_STARTED")); + buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); - buildInfo = new RemoteBuildInfo(Result.SUCCESS); assert(buildInfo.toString().equals("status=FINISHED, result=SUCCESS")); } } From ce20b35df2affa2d4ece0355885c6979bf1015f8 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Tue, 22 May 2018 14:13:34 +0200 Subject: [PATCH 085/262] refactor build data Removes BuildData and refactors the corresponding code. --- .../RemoteBuildConfiguration.java | 24 ++---- .../pipeline/Handle.java | 17 +---- .../remoteJob/BuildData.java | 47 ------------ .../remoteJob/QueueItemData.java | 76 +++++++++++-------- .../remoteJob/RemoteBuildInfo.java | 41 +++++++--- 5 files changed, 87 insertions(+), 118 deletions(-) delete mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index cdc12440..d705ad60 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -8,7 +8,6 @@ import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; import java.io.BufferedReader; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -41,11 +40,10 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildQueueStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; @@ -745,14 +743,13 @@ public void performWaitForBuild(BuildContext context, Handle handle) handle.setBuildInfo(buildInfo); } - BuildData buildData = buildInfo.getBuildData(); - if (buildData == null) { + URL jobURL = buildInfo.getBuildURL(); + int jobNumber = buildInfo.getBuildNumber(); + + if (jobURL == null || jobNumber == -1) { throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); } - int jobNumber = buildData.getBuildNumber(); - URL jobURL = buildData.getURL(); - context.logger.println(" Remote build URL: " + jobURL); context.logger.println(" Remote build number: " + jobNumber); @@ -882,19 +879,14 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn throw new AbortException(String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); } QueueItemData queueItem = getQueueItemData(queueId, context); - BuildData buildData = queueItem.getBuildData(context); - if (queueItem.isExecutable() && buildData!=null) { - buildInfo.setBuildData(buildData); // QueueStatus.EXECUTED + if (queueItem.isExecuted()) { + buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); // QueueStatus.EXECUTED } return buildInfo; } // QueueStatus.EXECUTED - BuildData buildData = buildInfo.getBuildData(); - if (buildData == null) { - throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); - } - String buildUrlString = buildData.getURL() + "api/json/"; + String buildUrlString = buildInfo.getBuildURL() + "api/json/"; JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); try { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 9df348b8..c5230769 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -4,7 +4,6 @@ import static org.apache.commons.lang.StringUtils.trimToNull; import java.io.IOException; -import java.io.PrintStream; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -18,7 +17,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; @@ -175,8 +173,7 @@ public String getQueueId() { @CheckForNull @Whitelisted public URL getBuildUrl() { - BuildData buildData = buildInfo.getBuildData(); - return buildData == null ? null : buildData.getURL(); + return buildInfo.getBuildURL() == null ? null : buildInfo.getBuildURL(); } /** @@ -187,8 +184,7 @@ public URL getBuildUrl() { @Nonnull @Whitelisted public int getBuildNumber() { - BuildData buildData = buildInfo.getBuildData(); - return buildData == null ? -1 : buildData.getBuildNumber(); + return buildInfo.getBuildNumber(); } /** @@ -306,8 +302,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Handle [job=%s, remoteServerURL=%s, queueId=%s", remoteBuildConfiguration.getJob(), effectiveRemoteServer.getAddress(), buildInfo.getQueueId())); sb.append(String.format(", %s", buildInfo.toString())); - BuildData buildData = buildInfo.getBuildData(); - if(buildData != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildData.getBuildNumber(), buildData.getURL())); + if(buildInfo != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildInfo.getBuildNumber(), buildInfo.getBuildURL())); sb.append("]"); return sb.toString(); } @@ -337,12 +332,6 @@ public static String help() { return sb.toString(); } - @CheckForNull - private BuildData getBuildData(String queueId, PrintStream logger) - { - return buildInfo.getBuildData(); - } - /** * This method reads and parses a JSON file which has been archived by the last remote build. * From Groovy/Pipeline code elements can be accessed directly via object.nodeC.nodeB.leafC. diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java deleted file mode 100644 index a8229b71..00000000 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildData.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; - -import java.io.Serializable; -import java.net.URL; - -import javax.annotation.Nonnull; - -/** - * Contains information about the location of the job while is being built. - * - */ -public class BuildData implements Serializable -{ - private static final long serialVersionUID = 3553303097206059203L; - - @Nonnull - private final int buildNumber; - - @Nonnull - private final URL buildURL; - - public BuildData(@Nonnull int buildNumber, @Nonnull URL buildURL) - { - this.buildNumber = buildNumber; - this.buildURL = buildURL; - } - - @Nonnull - public int getBuildNumber() - { - return buildNumber; - } - - @Nonnull - public URL getURL() - { - return buildURL; - } - - @Nonnull - @Override - public String toString() - { - return "RemoteBuild [buildNumber=" + buildNumber + ", buildURL=" + buildURL + "]"; - } - -} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java index 5e654933..77feba2b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -23,12 +23,18 @@ public class QueueItemData @Nonnull private RemoteBuildQueueStatus status; + @Nonnull + private int buildNumber; + + @CheckForNull + private URL buildURL; + public QueueItemData(@Nonnull BuildContext context, @Nonnull JSONObject queueResponse) throws MalformedURLException { this.queueResponse = queueResponse; - if (isExecutable() && getBuildData(context)!=null) status = RemoteBuildQueueStatus.EXECUTED; - else status = RemoteBuildQueueStatus.QUEUED; + this.status = RemoteBuildQueueStatus.QUEUED; + setQueueItemData(context); } public boolean isBlocked() @@ -61,48 +67,58 @@ public boolean isExecutable() return (!isBlocked() && !isBuildable() && !isPending() && !isCancelled()); } + public boolean isExecuted() + { + return status == RemoteBuildQueueStatus.EXECUTED; + } + public RemoteBuildQueueStatus getQueueStatus() { return status; } + + @Nonnull + public int getBuildNumber() + { + return buildNumber; + } + + @CheckForNull + public URL getBuildURL() + { + return buildURL; + } + /** * When a queue item is executable, the build number and the build URL * of the remote job are available in the queue item data. * * @param context * the context of this Builder/BuildStep. - * @return {@link BuildData} - * the remote build or null if the queue item is not executable. * @throws MalformedURLException * if there is an error creating the build URL. */ - @CheckForNull - public BuildData getBuildData(@Nonnull BuildContext context) throws MalformedURLException + private void setQueueItemData(@Nonnull BuildContext context) throws MalformedURLException { - if (!isExecutable()) return null; - - JSONObject remoteJobInfo; - try { - remoteJobInfo = queueResponse.getJSONObject("executable"); - if (remoteJobInfo == null) return null; - } catch (JSONException e) { - context.logger.println("The attribute \"executable\" was not found. Unexpected response: " + queueResponse.toString()); - return null; - } - int buildNumber; - try { - buildNumber = remoteJobInfo.getInt("number"); - } catch (JSONException e) { - context.logger.println("The attribute \"number\" was not found. Unexpected response: " + queueResponse.toString()); - return null; - } - String buildUrl; - try { - buildUrl = remoteJobInfo.getString("url"); - } catch (JSONException e) { - context.logger.println("The attribute \"url\" was not found. Unexpected response: " + queueResponse.toString()); - return null; + if (isExecutable()) { + try { + JSONObject remoteJobInfo = queueResponse.getJSONObject("executable"); + if (remoteJobInfo != null) { + try { + buildNumber = remoteJobInfo.getInt("number"); + } catch (JSONException e) { + context.logger.println("The attribute \"number\" was not found. Unexpected response: " + queueResponse.toString()); + } + try { + buildURL = new URL(remoteJobInfo.getString("url")); + } catch (JSONException e) { + context.logger.println("The attribute \"url\" was not found. Unexpected response: " + queueResponse.toString()); + } + } + } catch (JSONException e) { + context.logger.println("The attribute \"executable\" was not found. Unexpected response: " + queueResponse.toString()); + } + if (buildNumber != -1 && buildURL != null) status = RemoteBuildQueueStatus.EXECUTED; } - return new BuildData(buildNumber, new URL(buildUrl)); } private boolean getOptionalBoolean(String attribute) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index 4ba782ad..dbec3d4c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -1,10 +1,13 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; import java.io.Serializable; +import java.net.URL; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import hudson.AbortException; import hudson.model.Result; /** @@ -20,12 +23,15 @@ public class RemoteBuildInfo implements Serializable @CheckForNull private String queueId; - @CheckForNull - private BuildData buildData; - @Nonnull private RemoteBuildQueueStatus queueStatus; + @Nonnull + private int buildNumber; + + @CheckForNull + private URL buildURL; + @Nonnull private RemoteBuildStatus status; @@ -36,8 +42,9 @@ public class RemoteBuildInfo implements Serializable public RemoteBuildInfo() { queueId = null; - buildData = null; queueStatus = RemoteBuildQueueStatus.NOT_QUEUED; + buildNumber = -1; + buildURL = null; status = RemoteBuildStatus.NOT_STARTED; result = Result.NOT_BUILT; } @@ -47,17 +54,24 @@ public String getQueueId() { return queueId; } - @CheckForNull - public BuildData getBuildData() { - return buildData; - } - @Nonnull public RemoteBuildQueueStatus getQueueStatus() { return queueStatus; } + @Nonnull + public int getBuildNumber() + { + return buildNumber; + } + + @CheckForNull + public URL getBuildURL() + { + return buildURL; + } + @Nonnull public RemoteBuildStatus getStatus() { @@ -75,8 +89,13 @@ public void setQueueId(String queueId) { this.queueStatus = RemoteBuildQueueStatus.QUEUED; } - public void setBuildData(BuildData buildData) { - this.buildData = buildData; + public void setBuildData(@Nonnull int buildNumber, @Nullable URL buildURL) throws AbortException + { + if (buildURL == null) { + throw new AbortException(String.format("Unexpected remote build status: %s", toString())); + } + this.buildNumber = buildNumber; + this.buildURL = buildURL; this.queueStatus = RemoteBuildQueueStatus.EXECUTED; } From dc3a8fa9fba8ba987dbab1451bd9e890a014ae73 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Tue, 22 May 2018 19:46:01 +0200 Subject: [PATCH 086/262] add queue item status --- .../RemoteBuildConfiguration.java | 11 +-- .../pipeline/Handle.java | 2 +- .../remoteJob/QueueItemData.java | 77 +++++++++++-------- .../remoteJob/QueueItemStatus.java | 57 ++++++++++++++ .../remoteJob/RemoteBuildInfo.java | 2 - 5 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index d705ad60..34d36150 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -746,7 +746,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) URL jobURL = buildInfo.getBuildURL(); int jobNumber = buildInfo.getBuildNumber(); - if (jobURL == null || jobNumber == -1) { + if (jobURL == null || jobNumber == 0) { throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); } @@ -851,16 +851,17 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", response.getResponseCode(), queueQuery)); } - QueueItemData queueItem = new QueueItemData(context, queueResponse); + QueueItemData queueItem = new QueueItemData(); + queueItem.update(context, queueResponse); if (queueItem.isBlocked()) - context.logger.println("The remote job is blocked. Reason: " + queueItem.getWhy() + "."); + context.logger.println(String.format("The remote job is blocked. %s.", queueItem.getWhy())); if (queueItem.isPending()) - context.logger.println("The remote job is pending. Reason: " + queueItem.getWhy() + "."); + context.logger.println(String.format("The remote job is pending. %s.", queueItem.getWhy())); if (queueItem.isBuildable()) - context.logger.println("The remote job is buildable. Reason: " + queueItem.getWhy() + "."); + context.logger.println(String.format("The remote job is buildable. %s.", queueItem.getWhy())); if (queueItem.isCancelled()) throw new AbortException("The remote job was canceled"); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index c5230769..7ff86816 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -179,7 +179,7 @@ public URL getBuildUrl() { /** * Get the build number of the remote build. * - * @return the number, or -1 if it could not be identified (yet). + * @return the build number, or 0 if it could not be identified (yet). */ @Nonnull @Whitelisted diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java index 77feba2b..044c527a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -5,6 +5,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; @@ -12,68 +13,72 @@ import net.sf.json.JSONObject; /** - * Contains information about the job while is waiting on the queue. + * Contains information about the remote job while is waiting on the queue. * */ public class QueueItemData { @Nonnull - private final JSONObject queueResponse; + private QueueItemStatus status; - @Nonnull - private RemoteBuildQueueStatus status; + @Nullable + private String why; @Nonnull private int buildNumber; - @CheckForNull + @Nullable private URL buildURL; - public QueueItemData(@Nonnull BuildContext context, @Nonnull JSONObject queueResponse) throws MalformedURLException + public QueueItemData() throws MalformedURLException { - this.queueResponse = queueResponse; - this.status = RemoteBuildQueueStatus.QUEUED; - setQueueItemData(context); + this.status = QueueItemStatus.WAITING; + } + + public boolean isWaiting() + { + return status == QueueItemStatus.WAITING; } public boolean isBlocked() { - return queueResponse.getBoolean("blocked"); + return status == QueueItemStatus.BLOCKED; } public boolean isBuildable() { - return queueResponse.getBoolean("buildable"); + return status == QueueItemStatus.BUILDABLE; } public boolean isPending() { - return getOptionalBoolean("pending"); + return status == QueueItemStatus.PENDING; } - public boolean isCancelled() + public boolean isLeft() { - return getOptionalBoolean("cancelled"); + return status == QueueItemStatus.LEFT; } - public String getWhy() + public boolean isExecuted() { - return queueResponse.getString("why"); + return status == QueueItemStatus.EXECUTED; } - public boolean isExecutable() + public boolean isCancelled() { - return (!isBlocked() && !isBuildable() && !isPending() && !isCancelled()); + return status == QueueItemStatus.CANCELLED; } - public boolean isExecuted() - { - return status == RemoteBuildQueueStatus.EXECUTED; + @Nonnull + public QueueItemStatus getStatus() { + return status; } - public RemoteBuildQueueStatus getQueueStatus() { - return status; + @CheckForNull + public String getWhy() { + return why; } @Nonnull @@ -89,39 +94,47 @@ public URL getBuildURL() } /** - * When a queue item is executable, the build number and the build URL - * of the remote job are available in the queue item data. + * Updates the queue item data with a queue response. * * @param context * the context of this Builder/BuildStep. + * @param queueResponse + * the queue response * @throws MalformedURLException * if there is an error creating the build URL. */ - private void setQueueItemData(@Nonnull BuildContext context) throws MalformedURLException + public void update(@Nonnull BuildContext context, @Nonnull JSONObject queueResponse) throws MalformedURLException { - if (isExecutable()) { + if (queueResponse.getBoolean("blocked")) status = QueueItemStatus.BLOCKED; + if (queueResponse.getBoolean("buildable")) status = QueueItemStatus.BUILDABLE; + if (getOptionalBoolean(queueResponse, "pending")) status = QueueItemStatus.PENDING; + if (getOptionalBoolean(queueResponse, "cancelled")) status = QueueItemStatus.CANCELLED; + if (isBlocked() || isBuildable() || isPending()) why = queueResponse.getString("why"); + else if (!isCancelled()) status = QueueItemStatus.LEFT; + + if (isLeft()) { try { JSONObject remoteJobInfo = queueResponse.getJSONObject("executable"); if (remoteJobInfo != null) { try { buildNumber = remoteJobInfo.getInt("number"); } catch (JSONException e) { - context.logger.println("The attribute \"number\" was not found. Unexpected response: " + queueResponse.toString()); + context.logger.println(String.format("[WARNING] The attribute \"number\" was not found. Unexpected response: %s", queueResponse.toString())); } try { buildURL = new URL(remoteJobInfo.getString("url")); } catch (JSONException e) { - context.logger.println("The attribute \"url\" was not found. Unexpected response: " + queueResponse.toString()); + context.logger.println(String.format("[WARNING] The attribute \"url\" was not found. Unexpected response: %s", queueResponse.toString())); } } } catch (JSONException e) { - context.logger.println("The attribute \"executable\" was not found. Unexpected response: " + queueResponse.toString()); + context.logger.println(String.format("[WARNING] The attribute \"executable\" was not found. Unexpected response: %s", queueResponse.toString())); } - if (buildNumber != -1 && buildURL != null) status = RemoteBuildQueueStatus.EXECUTED; + if (buildNumber != 0 && buildURL != null) status = QueueItemStatus.EXECUTED; } } - private boolean getOptionalBoolean(String attribute) + private boolean getOptionalBoolean(@Nonnull JSONObject queueResponse, @Nonnull String attribute) { if (queueResponse.containsKey(attribute)) return queueResponse.getBoolean(attribute); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java new file mode 100644 index 00000000..70909a5c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java @@ -0,0 +1,57 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +/** + * The status of one queue item while it is on the queue. + * See {@link hudson.model.Queue}. + */ +public enum QueueItemStatus +{ + /** + * If a queue item enters the queue (waiting list). + */ + WAITING("WAITING"), + + /** + * If another build is already in progress. + */ + BLOCKED("BLOCKED"), + + /** + * If there is not another build in progress. + */ + BUILDABLE("BUILDABLE"), + + /** + * If the queue item is waiting for an available executor. + */ + PENDING("PENDING"), + + /** + * If there is an available executor and no build is already in progress. + */ + LEFT("LEFT"), + + /** + * The queue item left the queue and the build information (build number and build URL) is available. + */ + EXECUTED("EXECUTED"), + + /** + * If the queue item was cancelled and therefore it will not be executed. + */ + CANCELLED("CANCELLED"); + + + private final String id; + + + private QueueItemStatus(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index dbec3d4c..16bbad3f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -43,8 +43,6 @@ public RemoteBuildInfo() { queueId = null; queueStatus = RemoteBuildQueueStatus.NOT_QUEUED; - buildNumber = -1; - buildURL = null; status = RemoteBuildStatus.NOT_STARTED; result = Result.NOT_BUILT; } From 097f9bac41edd8a8ece1ad264f6943d187b9ae0b Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Tue, 22 May 2018 20:18:06 +0200 Subject: [PATCH 087/262] unify remote build status Merges RemoteBuildQueueStatus and RemoteBuildStatus. --- .../RemoteBuildConfiguration.java | 15 ++++---- .../remoteJob/RemoteBuildInfo.java | 28 ++++----------- .../remoteJob/RemoteBuildQueueStatus.java | 36 ------------------- .../remoteJob/RemoteBuildStatus.java | 10 ++++-- .../remoteJob/BuildInfoTest.java | 4 +-- 5 files changed, 23 insertions(+), 70 deletions(-) delete mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 34d36150..27999f9d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -44,7 +44,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildQueueStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; @@ -692,13 +691,13 @@ public Handle performTriggerAndGetQueueId(BuildContext context) } - RemoteBuildInfo buildInfo = new RemoteBuildInfo(); // QueueStatus.NOT_QUEUED + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); context.logger.println("Triggering remote job now."); ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); - buildInfo.setQueueId(queueItem.getId()); // QueueStatus.QUEUED + buildInfo.setQueueId(queueItem.getId()); buildInfo = updateBuildInfo(buildInfo, context); return new Handle(this, buildInfo, context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); @@ -762,10 +761,10 @@ public void performWaitForBuild(BuildContext context, Handle handle) buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); - if (buildInfo.isNotStarted()) + if (buildInfo.isQueued()) context.logger.println("Waiting for remote build to start ..."); - while (buildInfo.isNotStarted()) { + while (buildInfo.isQueued()) { context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) @@ -797,6 +796,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } + context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); @@ -872,7 +872,7 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo @Nonnull public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonnull BuildContext context) throws IOException { - if (buildInfo.isNotQueued()) return buildInfo; + if (buildInfo.isNotTriggered()) return buildInfo; if (buildInfo.isQueued()) { String queueId = buildInfo.getQueueId(); @@ -881,12 +881,11 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } QueueItemData queueItem = getQueueItemData(queueId, context); if (queueItem.isExecuted()) { - buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); // QueueStatus.EXECUTED + buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); } return buildInfo; } - // QueueStatus.EXECUTED String buildUrlString = buildInfo.getBuildURL() + "api/json/"; JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index 16bbad3f..a16636c8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -23,9 +23,6 @@ public class RemoteBuildInfo implements Serializable @CheckForNull private String queueId; - @Nonnull - private RemoteBuildQueueStatus queueStatus; - @Nonnull private int buildNumber; @@ -42,8 +39,7 @@ public class RemoteBuildInfo implements Serializable public RemoteBuildInfo() { queueId = null; - queueStatus = RemoteBuildQueueStatus.NOT_QUEUED; - status = RemoteBuildStatus.NOT_STARTED; + status = RemoteBuildStatus.NOT_TRIGGERED; result = Result.NOT_BUILT; } @@ -52,12 +48,6 @@ public String getQueueId() { return queueId; } - @Nonnull - public RemoteBuildQueueStatus getQueueStatus() - { - return queueStatus; - } - @Nonnull public int getBuildNumber() { @@ -84,7 +74,7 @@ public Result getResult() public void setQueueId(String queueId) { this.queueId = queueId; - this.queueStatus = RemoteBuildQueueStatus.QUEUED; + this.status = RemoteBuildStatus.QUEUED; } public void setBuildData(@Nonnull int buildNumber, @Nullable URL buildURL) throws AbortException @@ -94,7 +84,7 @@ public void setBuildData(@Nonnull int buildNumber, @Nullable URL buildURL) throw } this.buildNumber = buildNumber; this.buildURL = buildURL; - this.queueStatus = RemoteBuildQueueStatus.EXECUTED; + this.status = RemoteBuildStatus.RUNNING; } public void setBuildStatus(RemoteBuildStatus status) @@ -125,19 +115,15 @@ public void setBuildResult(String result) public String toString() { if (status == RemoteBuildStatus.FINISHED) return String.format("status=%s, result=%s", status.toString(), result.toString()); - else return String.format("queueStatus=%s, status=%s", queueStatus.toString(), status.toString()); + else return String.format("status=%s", status.toString()); } - public boolean isNotQueued() { - return queueStatus == RemoteBuildQueueStatus.NOT_QUEUED; + public boolean isNotTriggered() { + return status == RemoteBuildStatus.NOT_TRIGGERED; } public boolean isQueued() { - return queueStatus == RemoteBuildQueueStatus.QUEUED; - } - - public boolean isNotStarted() { - return status == RemoteBuildStatus.NOT_STARTED; + return status == RemoteBuildStatus.QUEUED; } public boolean isRunning() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java deleted file mode 100644 index 30c39f1c..00000000 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildQueueStatus.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; - -/** - * The status of the remote job on the queue. - */ -public enum RemoteBuildQueueStatus -{ - /** - * The remote job was not triggered and it is not on the queue. - */ - NOT_QUEUED("NOT_QUEUED"), - - /** - * The remote job was triggered and it is on the queue waiting to be executed. - */ - QUEUED("QUEUED"), - - /** - * The remote job was executed. - */ - EXECUTED("EXECUTED"); - - - private final String id; - - - private RemoteBuildQueueStatus(String id) { - this.id = id; - } - - @Override - public String toString() { - return id; - } - -} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java index 26a5ae2f..80458873 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java @@ -5,14 +5,18 @@ */ public enum RemoteBuildStatus { + /** + * The remote job was not triggered and it did not enter the queue. + */ + NOT_TRIGGERED("NOT_TRIGGERED"), /** - * The remote build did not start. + * The remote job was triggered and it did enter the queue. */ - NOT_STARTED("NOT_STARTED"), + QUEUED("QUEUED"), /** - * The remote build is running currently. + * The remote job left the queue and it is running currently. */ RUNNING("RUNNING"), diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java index 105b335b..6d753b8f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java @@ -18,7 +18,7 @@ public void buildStatusTest() { RemoteBuildInfo buildInfo = new RemoteBuildInfo(); - assert(buildInfo.isNotStarted()); + assert(buildInfo.isNotTriggered()); assert(buildInfo.getResult() == Result.NOT_BUILT); } @@ -58,7 +58,7 @@ public void buildInfoToStringTest() { RemoteBuildInfo buildInfo = new RemoteBuildInfo(); - assert(buildInfo.toString().equals("queueStatus=NOT_QUEUED, status=NOT_STARTED")); + assert(buildInfo.toString().equals("status=NOT_TRIGGERED")); buildInfo = new RemoteBuildInfo(); buildInfo.setBuildResult(Result.SUCCESS); From 0e73d8f4f325a4ecb6b9e88870d371e2f26f4fc2 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 23 May 2018 11:12:30 +0200 Subject: [PATCH 088/262] remove unnecessary code --- .../RemoteBuildConfiguration.java | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 27999f9d..59b4b5dd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -749,50 +749,25 @@ public void performWaitForBuild(BuildContext context, Handle handle) throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); } + context.logger.println("Remote build started!"); context.logger.println(" Remote build URL: " + jobURL); context.logger.println(" Remote build number: " + jobNumber); if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); - // If we are told to block until remoteBuildComplete: if (this.getBlockBuildUntilComplete()) { context.logger.println("Blocking local job until remote job completes."); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); - if (buildInfo.isQueued()) - context.logger.println("Waiting for remote build to start ..."); - - while (buildInfo.isQueued()) { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, context.logger); - } - buildInfo = updateBuildInfo(buildInfo, context); - handle.setBuildInfo(buildInfo); - } - if (buildInfo.isRunning()) { - context.logger.println("Remote build started!"); context.logger.println("Waiting for remote build to finish ..."); } while (buildInfo.isRunning()) { context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, context.logger); - } + Thread.sleep(this.pollInterval * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } From 75f258c9edd8220e33d878746d0add0fe7b1dc45 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 23 May 2018 14:48:16 +0200 Subject: [PATCH 089/262] improve documentation --- .../remoteJob/QueueItemStatus.java | 42 ++++++++++++++++++- .../remoteJob/RemoteBuildInfo.java | 23 ++++++++-- .../remoteJob/RemoteBuildStatus.java | 22 +++++++++- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java index 70909a5c..6f43a27a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java @@ -1,8 +1,48 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; /** - * The status of one queue item while it is on the queue. + * This class implements the status of an item while it is on the queue. + *

* See {@link hudson.model.Queue}. + * + *

{@code
+ * (enter Queue) --> WAITING --+--> BLOCKED
+                          |          ^
+                          |          |
+                          |          v
+                          +-------> BUILDABLE ---> PENDING ---> LEFT --> EXECUTED
+                                       ^              |
+                                       |              |
+                                       +---(rarely)---+
+ *}
+ * + *

+ * When the remote build is triggered, the remote job enters the queue (waiting list) + * and the queue item status is WAITING. + *

+ * After that, if there is another build already in progress, the queue item status changes to BLOCKED. + *

+ * On the contrary, if there is not another build in progress, the queue item status changes to BUILDABLE. + *

+ * Once the queue item is buildable, it needs to wait for an available executor, and the status changes + * to PENDING. + *

+ * If the node disappears before the execution starts, the status moves back to BUILDABLE, + * but this is not the normal case. + *

+ * When there is an available executor and the execution starts, the queue item leaves the queue, + * and the status changes to LEFT. + *

+ * When the remote job leaves the queue, the build number and the build URL are available. + * The build URL can be used to request information about the remote job while it is being executed. + *

+ * Sometimes, we did face some issues, because the item left the queue but this properties where not available, + * therefore the status EXECUTED was added. + *

+ * When this properties are available, the queue item status changes to EXECUTED. This is the final status. + *

+ * In addition, at any status, an item can be removed from the queue, in this case an AbortException is thrown. + * */ public enum QueueItemStatus { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index a16636c8..e0d7def3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -11,9 +11,25 @@ import hudson.model.Result; /** - * The remote build info contains the queue id and the queue status of the remote build, - * while it enters the queue, and the remote job build number, build url, build status and build result, - * when it leaves the queue. + * This class contains information about the remote build. + * + *

{@code
+ * NOT_TRIGGERED ---+--->    QUEUED    --+-->    RUNNING    -----+----->         FINISHED
+                             queueId           buildNumber                        result
+                                                & buildURL              (ABORTED | UNSTABLE | FAILURE | SUCCESS)
+ *}
+ * + *

+ * By default, the remote build status is NOT_TRIGGERED and the remote build result is NOT_BUILT. + *

+ * When the remote build is triggered, the remote job enters the queue (waiting list) + * and the status of the remote build changes to QUEUED. In this moment the queueId is available. + * The queueId can be used to request information about the remote job while it is waiting to be executed. + *

+ * When the remote job leaves the queue, the status changes to RUNNING. Then, the build number and the build URL + * are available. The build URL can be used to request information about the remote job while it is being executed. + *

+ * When the remote job is finished, the status changes to FINISHED. Then, the remote build result is available. * */ public class RemoteBuildInfo implements Serializable @@ -38,7 +54,6 @@ public class RemoteBuildInfo implements Serializable public RemoteBuildInfo() { - queueId = null; status = RemoteBuildStatus.NOT_TRIGGERED; result = Result.NOT_BUILT; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java index 80458873..3b6a9c05 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java @@ -1,7 +1,27 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; /** - * The build status of a remote build. + * This class implements the build status of a remote build. + * + *

{@code
+ * NOT_TRIGGERED --+--> QUEUED --+--> RUNNING --+--> FINISHED
+                          |                              |
+                          |                              |
+                          +-------> Cancelled <----------+
+ *}
+ * + *

+ * By default, the remote build status is NOT_TRIGGERED. + *

+ * When the remote build is triggered, the remote job enters the queue (waiting list) + * and the status of the remote build changes to QUEUED. + *

+ * When the remote job leaves the queue, the status changes to RUNNING. + *

+ * When the remote job is finished, the status changes to FINISHED. This is the final status. + * + * In addition, at the status QUEUED and FINISHED, a remote build can be cancelled, + * in this case an AbortException is thrown. */ public enum RemoteBuildStatus { From cf587b06ec5bf8308606ee594d14bb957ca6a1c5 Mon Sep 17 00:00:00 2001 From: Alejandra Ferreiro Vidal Date: Wed, 23 May 2018 15:27:19 +0200 Subject: [PATCH 090/262] rename BuildInfoExporterAction Renames BuildInfoExporterAction to RemoteBuildExporterAction. --- .../RemoteBuildConfiguration.java | 6 +++--- ...ava => RemoteBuildInfoExporterAction.java} | 12 +++++------ .../BuildInfoExporterActionTest.java | 20 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) rename src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/{BuildInfoExporterAction.java => RemoteBuildInfoExporterAction.java} (92%) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 59b4b5dd..547ee901 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -40,7 +40,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.BuildInfoExporterAction; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; @@ -753,7 +753,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) context.logger.println(" Remote build URL: " + jobURL); context.logger.println(" Remote build number: " + jobNumber); - if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); + if(context.run != null) RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); if (this.getBlockBuildUntilComplete()) { context.logger.println("Blocking local job until remote job completes."); @@ -773,7 +773,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) } context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); - if(context.run != null) BuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); + if(context.run != null) RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); if (this.getEnhancedLogging()) { String consoleOutput = getConsoleOutput(jobURL, context); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java similarity index 92% rename from src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java rename to src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java index 7b0646f1..75431d3b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java @@ -13,7 +13,7 @@ import hudson.model.EnvironmentContributingAction; import hudson.model.Run; -public class BuildInfoExporterAction implements EnvironmentContributingAction { +public class RemoteBuildInfoExporterAction implements EnvironmentContributingAction { public static final String JOB_NAME_VARIABLE = "LAST_TRIGGERED_JOB_NAME"; public static final String ALL_JOBS_NAME_VARIABLE = "TRIGGERED_JOB_NAMES"; @@ -26,21 +26,21 @@ public class BuildInfoExporterAction implements EnvironmentContributingAction { private List builds; - public BuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { + public RemoteBuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { super(); this.builds = new ArrayList(); addBuildReferenceSafe(buildRef); } - public static BuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { + public static RemoteBuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildInfo); - BuildInfoExporterAction action; + RemoteBuildInfoExporterAction action; synchronized(parentBuild) { - action = parentBuild.getAction(BuildInfoExporterAction.class); + action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); if (action == null) { - action = new BuildInfoExporterAction(parentBuild, reference); + action = new RemoteBuildInfoExporterAction(parentBuild, reference); parentBuild.addAction(action); } else { action.addBuildReference(reference); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java index 71576313..ed09f06d 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -43,17 +43,17 @@ public void testAddBuildInfoExporterAction_sequential() throws IOException { RemoteBuildInfo buildInfo = new RemoteBuildInfo(); buildInfo.setBuildResult(Result.SUCCESS); for (int i = 1; i <= PARALLEL_JOBS; i++) { - BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); } - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + RemoteBuildInfoExporterAction action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); EnvVars env = new EnvVars(); action.buildEnvVars(null, env); checkEnv(env); } /** - * We had ConcurrentModificationExceptions in the past. This test executes {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} - * and {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} in parallel to provoke a ConcurrentModificationException (which should not occur anymore). + * We had ConcurrentModificationExceptions in the past. This test executes {@link RemoteBuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} + * and {@link RemoteBuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} in parallel to provoke a ConcurrentModificationException (which should not occur anymore). */ @Test public void testAddBuildInfoExporterAction_parallel() throws IOException, InterruptedException, ExecutionException { @@ -124,7 +124,7 @@ private void checkEnv(EnvVars env) { } /** - * Calls {@link BuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. + * Calls {@link RemoteBuildInfoExporterAction#addBuildInfoExporterAction(Run, String, int, URL, BuildStatus)} a single time. * This Callable is typically executed multiple tiles in parallel to provoke a ConcurrentModificationException (which should not occur anymore). */ private static class AddActionCallable implements Callable { @@ -140,11 +140,11 @@ public Boolean call() throws MalformedURLException { String jobName = "Job" + i; RemoteBuildInfo buildInfo = new RemoteBuildInfo(); buildInfo.setBuildResult(Result.SUCCESS); - BuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); System.out.println("AddActionCallable finished for Job" + i); - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + RemoteBuildInfoExporterAction action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); Set projectsWithBuilds = action.getProjectsWithBuilds(); boolean success = projectsWithBuilds.contains(jobName); String message = String.format("AddActionCallable %s for %s (projects in list: %s)", @@ -156,7 +156,7 @@ public Boolean call() throws MalformedURLException { } /** - * Calls {@link BuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} repeatedly until all AddActionCallables finished. + * Calls {@link RemoteBuildInfoExporterAction#buildEnvVars(hudson.model.AbstractBuild, EnvVars)} repeatedly until all AddActionCallables finished. * This way we try to provoke a ConcurrentModificationException (which should not occur anymore). */ private static class BuildEnvVarsCallable implements Callable { @@ -167,14 +167,14 @@ public BuildEnvVarsCallable(Run parentBuild) { } public EnvVars call() throws MalformedURLException, InterruptedException, TimeoutException { - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); + RemoteBuildInfoExporterAction action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); EnvVars env = new EnvVars(); long startTime = System.currentTimeMillis(); while (action == null || action.getProjectsWithBuilds().size() < PARALLEL_JOBS) { try { Thread.sleep(100); } catch (InterruptedException e) {} - action = parentBuild.getAction(BuildInfoExporterAction.class); + action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); if (action != null) { //Provoke ConcurrentModificationException action.buildEnvVars(null, env); From 7b875a62c6d4892324a9d45e53ebbbe89a181a27 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 19 Jun 2018 23:41:58 +0800 Subject: [PATCH 091/262] avoid url cache only in loop inquiry, cherry pick missing commit. --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 547ee901..429698ec 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -861,7 +861,8 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn return buildInfo; } - String buildUrlString = buildInfo.getBuildURL() + "api/json/"; + // Only avoid url cache while loop inquiry + String buildUrlString = String.format("%sapi/json/?seed=%d", buildInfo.getBuildURL(), System.currentTimeMillis()); JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); try { From 8fd7f15efaf788baae37bd294f4ed3fdb261b770 Mon Sep 17 00:00:00 2001 From: Raphael Pionke Date: Fri, 22 Jun 2018 10:08:04 +0200 Subject: [PATCH 092/262] use Jenkins ProxyConfiguration to open URL --- .gitignore | 3 +++ .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 963dfac1..04ea336a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ .project *.prefs +# Idea-specific stuff +.idea/ +*.iml # jenkins data directory, build dir and release settings work diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 429698ec..e83c5480 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -34,6 +34,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; +import hudson.ProxyConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; @@ -1190,7 +1191,7 @@ private JenkinsCrumb getCrumb(BuildContext context) throws IOException private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) throws IOException { - URLConnection connection = url.openConnection(); + URLConnection connection = ProxyConfiguration.open(url); Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 overrideAuth = this.getAuth2(); From 6f5222195bc0f4387e5aa45ea1475b42decff9ff Mon Sep 17 00:00:00 2001 From: Raphael Pionke Date: Fri, 6 Jul 2018 16:13:48 +0200 Subject: [PATCH 093/262] add option for proxy usage --- .../RemoteBuildConfiguration.java | 2 +- .../RemoteJenkinsServer.java | 10 ++++++++++ .../RemoteJenkinsServer/config.jelly | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index e83c5480..2c9f83ca 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1191,7 +1191,7 @@ private JenkinsCrumb getCrumb(BuildContext context) throws IOException private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) throws IOException { - URLConnection connection = ProxyConfiguration.open(url); + URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 overrideAuth = this.getAuth2(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index a0e89d7b..f02d1564 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -54,6 +54,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl + + + +

From 5cf8e7cf414d0e16c213b2a64a3fc447ae99f21a Mon Sep 17 00:00:00 2001 From: Raphael Pionke Date: Sun, 8 Jul 2018 17:23:11 +0200 Subject: [PATCH 094/262] don't fail the build in case of an unstable remote build result (#42) * don't fail the build in case of an unstable remote build result * fix a typo --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 2c9f83ca..cbf98e91 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -786,8 +786,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) context.logger.println("--------------------------------------------------------------------------------"); } - // If build did not finish with 'success' then fail build step. - if (buildInfo.getResult() != Result.SUCCESS) { + // If build did not finish with 'success' or 'unstable' then fail build step. + if (buildInfo.getResult() != Result.SUCCESS && buildInfo.getResult() != Result.UNSTABLE) { // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so will decide how to // handle the failure. this.failBuild(new Exception("The remote job did not succeed."), context.logger); From eac7a60ff3141c23965823a8656bbe7d5f5b13f3 Mon Sep 17 00:00:00 2001 From: Raphael Pionke Date: Sun, 8 Jul 2018 17:25:32 +0200 Subject: [PATCH 095/262] add missing jelly entry for the shouldNotFailBuild option in pipeline syntax generator (#43) --- .../pipeline/RemoteBuildPipelineStep/config.jelly | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 2bfcb3c1..5416fa32 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -15,6 +15,9 @@ + + + From 11f24e0b807680a6b8e3e5b3db138d401850261c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 8 Jul 2018 23:39:42 +0800 Subject: [PATCH 096/262] update the compatibleSinceVersion for "do not fail build" --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf76f031..59646e9c 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ maven-hpi-plugin - 3.0.0-SNAPSHOT + 3.0.1-SNAPSHOT From 39f5d12d34609745dbc90e5f14504273c1fd9d82 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 9 Jul 2018 01:19:25 +0800 Subject: [PATCH 097/262] fix #JENKINS-47919 --- .../RemoteBuildConfiguration.java | 37 ++++--------------- .../exceptions/UrlNotFoundException.java | 22 +++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UrlNotFoundException.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index cbf98e91..652c336a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -40,6 +40,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UrlNotFoundException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; @@ -662,36 +663,6 @@ public Handle performTriggerAndGetQueueId(BuildContext context) logAuthInformation(context); - // get the ID of the Next Job to run. - if (this.getPreventRemoteBuildQueue()) { - context.logger.println(" Checking if the remote job " + jobNameOrUrl + " is currently running."); - String preCheckUrlString = jobUrlString; - preCheckUrlString += "/lastBuild"; - preCheckUrlString += "/api/json/"; - JSONObject preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", context); - - if ( preCheckResponse != null ) { - // check the latest build on the remote server to see if it's running - if so wait until it has stopped. - // if building is true then the build is running - // if result is null the build hasn't finished - but might not have started running. - while (preCheckResponse.getBoolean("building") == true || preCheckResponse.getString("result") == null) { - context.logger.println(String.format( - " Remote build is currently running - waiting for it to finish. Next try in %s seconds.", - this.pollInterval)); - try { - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, context.logger); - } - preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", context); - } - context.logger.println(" Remote job " + jobNameOrUrl + " is currently not building."); - } else { - this.failBuild(new Exception("Got a blank response from Remote Jenkins Server, cannot continue."), context.logger); - } - - } - RemoteBuildInfo buildInfo = new RemoteBuildInfo(); context.logger.println("Triggering remote job now."); @@ -934,6 +905,8 @@ private String getConsoleOutput(URL url, BuildContext context, int numberOfAttem throw new UnauthorizedException(buildUrl); } else if(responseCode == 403) { throw new ForbiddenException(buildUrl); + } else if(responseCode == 404) { + throw new UrlNotFoundException(buildUrl); } else { consoleOutput = readInputStream(connection); } @@ -1030,6 +1003,8 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu throw new UnauthorizedException(url); } else if(responseCode == 403) { throw new ForbiddenException(url); + } else if(responseCode == 404) { + throw new UrlNotFoundException(url); } else { String response = trimToNull(readInputStream(connection)); @@ -1054,6 +1029,8 @@ private ConnectionResponse sendHTTPCall(String urlString, String requestType, Bu this.failBuild(e, context.logger); } catch (ForbiddenException e) { this.failBuild(e, context.logger); + } catch (UrlNotFoundException e) { + this.failBuild(e, context.logger); } catch (IOException e) { //E.g. "HTTP/1.1 403 No valid crumb was included in the request" diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UrlNotFoundException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UrlNotFoundException.java new file mode 100644 index 00000000..005fc5a1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/UrlNotFoundException.java @@ -0,0 +1,22 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions; + +import java.io.IOException; +import java.net.URL; + +public class UrlNotFoundException extends IOException { + + private static final long serialVersionUID = -8787613112499246042L; + private URL url; + + public UrlNotFoundException(URL url) + { + this.url = url; + } + + @Override + public String getMessage() + { + return "Server returned 404 - URL not found for this request: " + url; + } + +} From 0f6e242ddadfd68274b74946d7a274cc6343da80 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 9 Jul 2018 21:21:13 +0800 Subject: [PATCH 098/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 59646e9c..5d26722f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.1-SNAPSHOT + 3.0.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.1 From 583a41f29f46f8bef6f9a60ed86d699cfd8a6510 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 9 Jul 2018 21:21:25 +0800 Subject: [PATCH 099/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5d26722f..572a6b19 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.1 + 3.0.2-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.1 + HEAD From ffab7282197147fd3cd5f9fc32410fdc6f0a4878 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 17 Jul 2018 15:11:40 +0800 Subject: [PATCH 100/262] fix the parameter's too long (http 414) and post with form-data & reorganize http utility --- .../RemoteBuildConfiguration.java | 2556 +++++++---------- .../exceptions/ExceedRetryLimitException.java | 17 + .../pipeline/Handle.java | 3 +- .../utils/HttpHelper.java | 532 ++++ .../RemoteBuildConfigurationTest.java | 35 +- .../ParameterizedRemoteTrigger/TestConst.java | 81 + 6 files changed, 1722 insertions(+), 1502 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/TestConst.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 652c336a..ca8141dd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,55 +1,42 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import static org.apache.commons.io.IOUtils.closeQuietly; -import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; import java.io.BufferedReader; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Serializable; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Map; +import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; -import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; -import hudson.ProxyConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UrlNotFoundException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.HttpHelper; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -76,8 +63,6 @@ import hudson.util.ListBoxModel; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; -import net.sf.json.JSONSerializer; -import net.sf.json.util.JSONUtils; /** * @@ -87,1485 +72,1062 @@ @ParametersAreNullableByDefault public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep, Serializable { - private static final long serialVersionUID = -4059001060991775146L; - - /** - * Default for this class is "no auth configured" since we do not want to override potential global config - */ - private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; - - - private static final int DEFAULT_POLLINTERVALL = 10; - private static final String paramerizedBuildUrl = "/buildWithParameters"; - private static final String normalBuildUrl = "/build"; - private static final String buildTokenRootUrl = "/buildByToken"; - private static final int connectionRetryLimit = 5; - - /** - * We need to keep this for compatibility - old config deserialization! - * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. - */ - private transient List auth; - - private String remoteJenkinsName; - private String remoteJenkinsUrl; - private Auth2 auth2; - private boolean shouldNotFailBuild; - private boolean preventRemoteBuildQueue; - private int pollInterval; - private boolean blockBuildUntilComplete; - private String job; - private String token; - private String parameters; - private boolean enhancedLogging; - private boolean loadParamsFromFile; - private String parameterFile; - - - @DataBoundConstructor - public RemoteBuildConfiguration() { - pollInterval = DEFAULT_POLLINTERVALL; - } - - /* - * see https://wiki.jenkins.io/display/JENKINS/Hint+on+retaining+backward+compatibility - */ - @SuppressWarnings("deprecation") - protected Object readResolve() { - //migrate Auth To Auth2 - if(auth2 == null) { - if(auth == null || auth.size() <= 0) { - auth2 = DEFAULT_AUTH; - } else { - auth2 = Auth.authToAuth2(auth); - } - } - auth = null; - return this; - } - - - @DataBoundSetter - public void setRemoteJenkinsName(String remoteJenkinsName) - { - this.remoteJenkinsName = trimToNull(remoteJenkinsName); - } - - @DataBoundSetter - public void setRemoteJenkinsUrl(String remoteJenkinsUrl) - { - this.remoteJenkinsUrl = trimToNull(remoteJenkinsUrl); - } - - @DataBoundSetter - public void setAuth2(Auth2 auth) { - this.auth2 = auth; - // disable old auth - this.auth = null; - } - - @DataBoundSetter - public void setShouldNotFailBuild(boolean shouldNotFailBuild) { - this.shouldNotFailBuild = shouldNotFailBuild; - } - - @DataBoundSetter - public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { - this.preventRemoteBuildQueue = preventRemoteBuildQueue; - } - - @DataBoundSetter - public void setPollInterval(int pollInterval) { - if(pollInterval <= 0) this.pollInterval = DEFAULT_POLLINTERVALL; - else this.pollInterval = pollInterval; - } - - @DataBoundSetter - public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { - this.blockBuildUntilComplete = blockBuildUntilComplete; - } - - @DataBoundSetter - public void setJob(String job) { - this.job = trimToNull(job); - } - - @DataBoundSetter - public void setToken(String token) { - if (token == null) this.token = ""; - else this.token = token.trim(); - } - - @DataBoundSetter - public void setParameters(String parameters) { - if (parameters == null) this.parameters = ""; - else this.parameters = parameters; - } - - @DataBoundSetter - public void setEnhancedLogging(boolean enhancedLogging) { - this.enhancedLogging = enhancedLogging; - } - - @DataBoundSetter - public void setLoadParamsFromFile(boolean loadParamsFromFile) { - this.loadParamsFromFile = loadParamsFromFile; - } - - @DataBoundSetter - public void setParameterFile(String parameterFile) { - if (loadParamsFromFile && (parameterFile == null || parameterFile.isEmpty())) - throw new IllegalArgumentException("Parameter file path is empty"); - - if (parameterFile == null) this.parameterFile = ""; - else this.parameterFile = parameterFile; - } - - public List getParameterList(BuildContext context) { - String params = getParameters(); - if (!params.isEmpty()) { - String[] parameterArray = params.split("\n"); - return new ArrayList(Arrays.asList(parameterArray)); - } else if (loadParamsFromFile) { - return loadExternalParameterFile(context); - } else { - return new ArrayList(); - } - } - - /** - * Reads a file from the jobs workspace, and loads the list of parameters from with in it. It will also call - * ```getCleanedParameters``` before returning. - * - * @param build - * @return List of build parameters - */ - private List loadExternalParameterFile(BuildContext context) { - - BufferedReader br = null; - List parameterList = new ArrayList(); - try { - if (context.workspace != null){ - FilePath filePath = context.workspace.child(getParameterFile()); - String sCurrentLine; - context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); - - br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); - - while ((sCurrentLine = br.readLine()) != null) { - parameterList.add(sCurrentLine); - } - } else { - context.logger.println("[WARNING] workspace is null"); - } - } catch (InterruptedException | IOException e) { - context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); + private static final long serialVersionUID = -4059001060991775146L; + + /** + * Default for this class is "no auth configured" since we do not want to + * override potential global config + */ + private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; + + private static final int DEFAULT_POLLINTERVALL = 10; + private static final int connectionRetryLimit = 5; + + /** + * We need to keep this for compatibility - old config deserialization! + * + * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. + */ + private transient List auth; + + private String remoteJenkinsName; + private String remoteJenkinsUrl; + private Auth2 auth2; + private boolean shouldNotFailBuild; + private boolean preventRemoteBuildQueue; + private int pollInterval; + private boolean blockBuildUntilComplete; + private String job; + private String token; + private String parameters; + private boolean enhancedLogging; + private boolean loadParamsFromFile; + private String parameterFile; + + @SuppressWarnings("unused") + private static Logger logger = Logger.getLogger(RemoteBuildConfiguration.class.getName()); + + @DataBoundConstructor + public RemoteBuildConfiguration() { + pollInterval = DEFAULT_POLLINTERVALL; + } + + /* + * see https://wiki.jenkins.io/display/JENKINS/Hint+on+retaining+backward+ + * compatibility + */ + @SuppressWarnings("deprecation") + protected Object readResolve() { + // migrate Auth To Auth2 + if (auth2 == null) { + if (auth == null || auth.size() <= 0) { + auth2 = DEFAULT_AUTH; + } else { + auth2 = Auth.authToAuth2(auth); + } + } + auth = null; + return this; + } + + @DataBoundSetter + public void setRemoteJenkinsName(String remoteJenkinsName) { + this.remoteJenkinsName = trimToNull(remoteJenkinsName); + } + + @DataBoundSetter + public void setRemoteJenkinsUrl(String remoteJenkinsUrl) { + this.remoteJenkinsUrl = trimToNull(remoteJenkinsUrl); + } + + @DataBoundSetter + public void setAuth2(Auth2 auth) { + this.auth2 = auth; + // disable old auth + this.auth = null; + } + + @DataBoundSetter + public void setShouldNotFailBuild(boolean shouldNotFailBuild) { + this.shouldNotFailBuild = shouldNotFailBuild; + } + + @DataBoundSetter + public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { + this.preventRemoteBuildQueue = preventRemoteBuildQueue; + } + + @DataBoundSetter + public void setPollInterval(int pollInterval) { + if (pollInterval <= 0) + this.pollInterval = DEFAULT_POLLINTERVALL; + else + this.pollInterval = pollInterval; + } + + @DataBoundSetter + public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { + this.blockBuildUntilComplete = blockBuildUntilComplete; + } + + @DataBoundSetter + public void setJob(String job) { + this.job = trimToNull(job); + } + + @DataBoundSetter + public void setToken(String token) { + if (token == null) + this.token = ""; + else + this.token = token.trim(); + } + + @DataBoundSetter + public void setParameters(String parameters) { + if (parameters == null) + this.parameters = ""; + else + this.parameters = parameters; + } + + @DataBoundSetter + public void setEnhancedLogging(boolean enhancedLogging) { + this.enhancedLogging = enhancedLogging; + } + + @DataBoundSetter + public void setLoadParamsFromFile(boolean loadParamsFromFile) { + this.loadParamsFromFile = loadParamsFromFile; + } + + @DataBoundSetter + public void setParameterFile(String parameterFile) { + if (loadParamsFromFile && (parameterFile == null || parameterFile.isEmpty())) + throw new IllegalArgumentException("Parameter file path is empty"); + + if (parameterFile == null) + this.parameterFile = ""; + else + this.parameterFile = parameterFile; + } + + public List getParameterList(BuildContext context) { + String params = getParameters(); + if (!params.isEmpty()) { + String[] parameterArray = params.split("\n"); + return new ArrayList(Arrays.asList(parameterArray)); + } else if (loadParamsFromFile) { + return loadExternalParameterFile(context); + } else { + return new ArrayList(); + } + } + + /** + * Reads a file from the jobs workspace, and loads the list of parameters from + * with in it. It will also call ```getCleanedParameters``` before returning. + * + * @param build + * @return List of build parameters + */ + private List loadExternalParameterFile(BuildContext context) { + + BufferedReader br = null; + List parameterList = new ArrayList(); + try { + if (context.workspace != null) { + FilePath filePath = context.workspace.child(getParameterFile()); + String sCurrentLine; + context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); + + br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); + + while ((sCurrentLine = br.readLine()) != null) { + parameterList.add(sCurrentLine); + } + } else { + context.logger.println("[WARNING] workspace is null"); + } + } catch (InterruptedException | IOException e) { + context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); } finally { - try { - if (br != null) { - br.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - return getCleanedParameters(parameterList); - } - - /** - * Strip out any empty strings from the parameterList - */ - private void removeEmptyElements(Collection collection) { - collection.removeAll(Arrays.asList(null, "")); - collection.removeAll(Arrays.asList(null, " ")); - } - - /** - * Same as "getParameterList", but removes comments and empty strings Notice that no type of character encoding is - * happening at this step. All encoding happens in the "buildUrlQueryString" method. - * - * @param List - * parameters - * @return List of build parameters - */ - private List getCleanedParameters(List parameters) { - List params = new ArrayList(parameters); - removeEmptyElements(params); - removeCommentsFromParameters(params); - return params; - } - - /** - * Strip out any comments (lines that start with a #) from the collection that is passed in. - */ - private void removeCommentsFromParameters(Collection collection) { - List itemsToRemove = new ArrayList(); - - for (String parameter : collection) { - if (parameter.indexOf("#") == 0) { - itemsToRemove.add(parameter); - } - } - collection.removeAll(itemsToRemove); - } - - /** - * Return the Collection in an encoded query-string. - * - * @param parameters - * the parameters needed to trigger the remote job. - * @return query-parameter-formated URL-encoded string. - */ - private String buildUrlQueryString(Collection parameters) { - - // List to hold the encoded parameters - List encodedParameters = new ArrayList(); - - for (String parameter : parameters) { - - // Step #1 - break apart the parameter-pairs (because we don't want to encode the "=" character) - String[] splitParameters = parameter.split("="); - - // List to hold each individually encoded parameter item - List encodedItems = new ArrayList(); - for (String item : splitParameters) { - try { - // Step #2 - encode each individual parameter item add the encoded item to its corresponding list - - encodedItems.add(encodeValue(item)); - - } catch (Exception e) { - // do nothing - // because we are "hard-coding" the encoding type, there is a 0% chance that this will fail. - } - - } - - // Step #3 - reunite the previously separated parameter items and add them to the corresponding list - encodedParameters.add(StringUtils.join(encodedItems, "=")); - } - - return StringUtils.join(encodedParameters, "&"); - } - - /** - * Tries to identify the effective Remote Host configuration based on the different parameters - * like remoteJenkinsName and the globally configured remote host, remoteJenkinsURL which overrides - * the address locally or job which can be a full job URL. - * - * @param context - * the context of this Builder/BuildStep. - * @return {@link RemoteJenkinsServer} - * a RemoteJenkinsServer object, never null. - * @throws AbortException - * if no server found and remoteJenkinsUrl empty. - * @throws MalformedURLException - * if remoteJenkinsName no valid URL or job an URL but nor valid. - */ - @Nonnull - public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context) throws IOException { - RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); - RemoteJenkinsServer server = globallyConfiguredServer; - String expandedJob = getJobExpanded(context); - boolean isJobEmpty = isEmpty(trimToNull(expandedJob)); - boolean isJobUrl = FormValidationUtils.isURL(expandedJob); - boolean isRemoteUrlEmpty = isEmpty(trimToNull(this.remoteJenkinsUrl)); - boolean isRemoteUrlSet = FormValidationUtils.isURL(this.remoteJenkinsUrl); - boolean isRemoteNameEmpty = isEmpty(trimToNull(this.remoteJenkinsName)); - - if(isJobEmpty) throw new AbortException("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified."); - if(!isRemoteUrlEmpty && !isRemoteUrlSet) throw new AbortException(String.format( - "The 'Override remote host URL' parameter value (remoteJenkinsUrl: '%s') is no valid URL", this.remoteJenkinsUrl)); - - if(isJobUrl) { - // Full job URL configured - get remote Jenkins root URL from there - if(server == null) server = new RemoteJenkinsServer(); - server.setAddress(getRootUrlFromJobUrl(expandedJob)); - - } else if(isRemoteUrlSet) { - // Remote Jenkins root URL overridden locally in Job/Pipeline - if(server == null) server = new RemoteJenkinsServer(); - server.setAddress(this.remoteJenkinsUrl); - - } - - if (server == null) { - if(!isJobUrl) { - if(!isRemoteUrlSet && isRemoteNameEmpty) - throw new AbortException("Configuration of the remote Jenkins host is missing."); - if(!isRemoteUrlSet && !isRemoteNameEmpty && globallyConfiguredServer == null) - throw new AbortException(String.format( - "Could get remote host with ID '%s' configured in Jenkins global configuration. Please check your global configuration.", - this.remoteJenkinsName)); - } - //Generic error message - throw new AbortException(String.format( - "Could not identify remote host - neither via 'Remote Job Name or URL' (job:'%s'), globally configured" - + " remote host (remoteJenkinsName:'%s') nor 'Override remote host URL' (remoteJenkinsUrl:'%s').", - expandedJob, this.remoteJenkinsName, this.remoteJenkinsUrl)); - } - return server; - } - - /** - * Lookup up the globally configured Remote Jenkins Server based on display name - * - * @param displayName - * Name of the configuration you are looking for - * @return A deep-copy of the RemoteJenkinsServer object configured globally - */ - public @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { - if(isEmpty(displayName)) return null; - RemoteJenkinsServer server = null; - for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { - // if we find a match, then stop looping - if (displayName.equals(host.getDisplayName())) { - try { - server = host.clone(); - break; - } catch(CloneNotSupportedException e) { - // Clone is supported by RemoteJenkinsServer - throw new RuntimeException(e); - } - } - } - return server; - } - - protected static String removeTrailingSlashes(String string) - { - if(isEmpty(string)) return string; - string = string.trim(); - while(string.endsWith("/")) string = string.substring(0, string.length() - 1); - return string; - } - - protected static String removeQueryParameters(String string) - { - if(isEmpty(string)) return string; - string = string.trim(); - int idx = string.indexOf("?"); - if(idx > 0) string = string.substring(0, idx); - return string; - } - - protected static String removeHashParameters(String string) - { - if(isEmpty(string)) return string; - string = string.trim(); - int idx = string.indexOf("#"); - if(idx > 0) string = string.substring(0, idx); - return string; - } - - private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException - { - if(isEmpty(jobUrl)) return null; - if(FormValidationUtils.isURL(jobUrl)) { - int index = jobUrl.indexOf("/job/"); - if(index < 0) throw new MalformedURLException("Expected '/job/' element in job URL but was: " + jobUrl); - return jobUrl.substring(0, index); - } else { - return null; - } - } - - /** - * Helper function to allow values to be added to the query string from any method. - * - * @param item - */ - private String addToQueryString(String queryString, String item) { - if (isBlank(queryString)) { - return item; - } else { - return queryString + "&" + item; - } - } - - /** - * Build the proper URL to trigger the remote build - * - * All passed in string have already had their tokens replaced with real values. All 'params' also have the proper - * character encoding - * - * @param jobNameOrUrl - * Name of the remote job - * @param securityToken - * Security token used to trigger remote job - * @param params - * Parameters for the remote job - * @return fully formed, fully qualified remote trigger URL - * @throws MalformedURLException - */ - private String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, boolean isRemoteJobParameterized, - BuildContext context) throws IOException { - - String triggerUrlString; - String query = ""; - - if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { - // start building the proper URL based on known capabiltiies of the remote server - if (context.effectiveRemoteServer.getAddress() == null) { - throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); - } - triggerUrlString = context.effectiveRemoteServer.getAddress(); - triggerUrlString += buildTokenRootUrl; - triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); - query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); //TODO: does it work with full URL? - - } else { - triggerUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); - } - - // don't try to include a security token in the URL if none is provided - if (!securityToken.equals("")) { - query = addToQueryString(query, "token=" + encodeValue(securityToken)); - } - - // turn our Collection into a query string - String buildParams = buildUrlQueryString(params); - - if (!buildParams.isEmpty()) { - query = addToQueryString(query, buildParams); - } - - // by adding "delay=0", this will (theoretically) force this job to the top of the remote queue - query = addToQueryString(query, "delay=0"); - - triggerUrlString += "?" + query; - - return triggerUrlString; - } - - /** - * Build the proper URL for GET calls. - * - * All passed in string have already had their tokens replaced with real values. - * - * @param jobNameOrUrl - * name or URL of the remote job. - * @param securityToken - * security token used to trigger remote job. - * @param context - * the context of this Builder/BuildStep. - * @return String - * fully formed, fully qualified remote trigger URL. - * @throws IOException - * if there is an error identifying the remote host. - */ - private String buildGetUrl(String jobNameOrUrl, String securityToken, BuildContext context) throws IOException { - - String urlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - // don't try to include a security token in the URL if none is provided - if (!isEmpty(securityToken)) { - urlString += "?token=" + encodeValue(securityToken); - } - return urlString; - } - - /** - * Convenience function to mark the build as failed. It's intended to only be called from this.perform(). - * - * @param e - * exception that caused the build to fail. - * @param logger - * build listener. - * @throws IOException - * if the build fails and shouldNotFailBuild is not set. - */ - protected void failBuild(Exception e, PrintStream logger) throws IOException { - StringBuilder msg = new StringBuilder(); - if(e instanceof InterruptedException) { - Thread current = Thread.currentThread(); - msg.append(String.format("[Thread: %s/%s]: ", current.getId(), current.getName())); - } - msg.append(String.format("Remote build failed with '%s' for the following reason: '%s'.%s", - e.getClass().getSimpleName(), - e.getMessage(), - this.getShouldNotFailBuild() ? " But the build will continue." : "")); - if(enhancedLogging) { - msg.append(NL).append(ExceptionUtils.getFullStackTrace(e)); - } - if(logger != null) logger.println("ERROR: " + msg.toString()); - if (!this.getShouldNotFailBuild()) { - throw new AbortException(e.getClass().getSimpleName() + ": " + e.getMessage()); - } - } - - @Override - public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, - IOException, IllegalArgumentException - { - FilePath workspace = build.getWorkspace(); - if (workspace == null) throw new IllegalArgumentException("The workspace can not be null"); - perform(build, workspace, launcher, listener); - return true; - } - - /** - * Triggers the remote job and, waits until completion if blockBuildUntilComplete is set. - * - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if there is an error retrieving the remote build data, or, - * if there is an error retrieving the remote build status, or, - * if there is an error retrieving the console output of the remote build, or, - * if the remote build does not succeed. - */ - @Override - public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) - throws InterruptedException, IOException - { - RemoteJenkinsServer effectiveRemoteServer = evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); - BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); - Handle handle = performTriggerAndGetQueueId(context); - performWaitForBuild(context, handle); - } - - /** - * Triggers the remote job, identifies the queue ID and, returns a Handle to this remote execution. - * - * @param context - * the context of this Builder/BuildStep. - * @return Handle - * to further tracking of the remote build status. - * @throws IOException - * if there is an error triggering the remote job. - */ - public Handle performTriggerAndGetQueueId(BuildContext context) - throws IOException - { - List cleanedParams = getCleanedParameters(getParameterList(context)); - String jobNameOrUrl = this.getJob(); - String securityToken = this.getToken(); - try { - cleanedParams = TokenMacroUtils.applyTokenMacroReplacements(cleanedParams, context); - jobNameOrUrl = TokenMacroUtils.applyTokenMacroReplacements(jobNameOrUrl, context); - securityToken = TokenMacroUtils.applyTokenMacroReplacements(securityToken, context); - } catch(IOException e) { - this.failBuild(e, context.logger); - } - - logConfiguration(context, cleanedParams); - - final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); - boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); - - final String triggerUrlString = this.buildTriggerUrl(jobNameOrUrl, securityToken, cleanedParams, isRemoteParameterized, context); - final String jobUrlString = this.buildGetUrl(jobNameOrUrl, securityToken, context); - - // Trigger remote job - context.logger.println(String.format("Triggering %s remote job '%s'", - (isRemoteParameterized ? "parameterized" : "non-parameterized"), jobUrlString )); - - logAuthInformation(context); - - RemoteBuildInfo buildInfo = new RemoteBuildInfo(); - - context.logger.println("Triggering remote job now."); - - ConnectionResponse responseRemoteJob = sendHTTPCall(triggerUrlString, "POST", context, 1); - QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); - buildInfo.setQueueId(queueItem.getId()); - buildInfo = updateBuildInfo(buildInfo, context); - - return new Handle(this, buildInfo, context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); - } - - /** - * Checks the remote build status and, waits for completion if blockBuildUntilComplete is set. - * - * @param context - * the context of this Builder/BuildStep. - * @param handle - * the handle to the remote execution. - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if there is an error retrieving the remote build data, or, - * if there is an error retrieving the remote build status, or, - * if there is an error retrieving the console output of the remote build, or, - * if the remote build does not succeed. - */ - public void performWaitForBuild(BuildContext context, Handle handle) - throws InterruptedException, IOException - { - String jobName = handle.getJobName(); - - RemoteBuildInfo buildInfo = handle.getBuildInfo(); - String queueId = buildInfo.getQueueId(); - if (queueId == null) { - throw new AbortException(String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); - } - context.logger.println(" Remote job queue number: " + buildInfo.getQueueId()); - - if (buildInfo.isQueued()) { - context.logger.println("Waiting for remote build to be executed..."); - } - - while (buildInfo.isQueued()) - { - context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); - Thread.sleep(this.pollInterval * 1000); - buildInfo = updateBuildInfo(buildInfo, context); - handle.setBuildInfo(buildInfo); - } - - URL jobURL = buildInfo.getBuildURL(); - int jobNumber = buildInfo.getBuildNumber(); - - if (jobURL == null || jobNumber == 0) { - throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); - } - - context.logger.println("Remote build started!"); - context.logger.println(" Remote build URL: " + jobURL); - context.logger.println(" Remote build number: " + jobNumber); - - if(context.run != null) RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); - - if (this.getBlockBuildUntilComplete()) { - context.logger.println("Blocking local job until remote job completes."); - - buildInfo = updateBuildInfo(buildInfo, context); - handle.setBuildInfo(buildInfo); - - if (buildInfo.isRunning()) { - context.logger.println("Waiting for remote build to finish ..."); - } - - while (buildInfo.isRunning()) { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); - Thread.sleep(this.pollInterval * 1000); - buildInfo = updateBuildInfo(buildInfo, context); - handle.setBuildInfo(buildInfo); - } - - context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); - if(context.run != null) RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); - - if (this.getEnhancedLogging()) { - String consoleOutput = getConsoleOutput(jobURL, context); - - context.logger.println(); - context.logger.println("Console output of remote job:"); - context.logger.println("--------------------------------------------------------------------------------"); - context.logger.println(consoleOutput); - context.logger.println("--------------------------------------------------------------------------------"); - } - - // If build did not finish with 'success' or 'unstable' then fail build step. - if (buildInfo.getResult() != Result.SUCCESS && buildInfo.getResult() != Result.UNSTABLE) { - // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so will decide how to - // handle the failure. - this.failBuild(new Exception("The remote job did not succeed."), context.logger); - } - } else { - context.logger.println("Not blocking local job until remote job completes - fire and forget."); - } - } - - /** - * Sends a HTTP request to the API of the remote server requesting a queue item. - * - * @param queueId - * the id of the remote job on the queue. - * @param context - * the context of this Builder/BuildStep. - * @return {@link QueueItemData} - * the queue item data. - * @throws IOException - * if there is an error identifying the remote host, or - * if there is an error setting the authorization header, or - * if the request fails due to an unknown host, unauthorized credentials, or another reason, or - * if there is an invalid queue response. - */ - @Nonnull - private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) - throws IOException { - - if (context.effectiveRemoteServer.getAddress() == null) { - throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); - } - String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getAddress(), queueId); - ConnectionResponse response = sendHTTPCall( queueQuery, "GET", context, 1 ); - JSONObject queueResponse = response.getBody(); - - if (queueResponse == null || queueResponse.isNullObject()) { - throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", response.getResponseCode(), queueQuery)); - } - - QueueItemData queueItem = new QueueItemData(); - queueItem.update(context, queueResponse); - - if (queueItem.isBlocked()) - context.logger.println(String.format("The remote job is blocked. %s.", queueItem.getWhy())); - - if (queueItem.isPending()) - context.logger.println(String.format("The remote job is pending. %s.", queueItem.getWhy())); - - if (queueItem.isBuildable()) - context.logger.println(String.format("The remote job is buildable. %s.", queueItem.getWhy())); - - if (queueItem.isCancelled()) - throw new AbortException("The remote job was canceled"); - - return queueItem; - } - - @Nonnull - public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonnull BuildContext context) throws IOException { - - if (buildInfo.isNotTriggered()) return buildInfo; - - if (buildInfo.isQueued()) { - String queueId = buildInfo.getQueueId(); - if (queueId == null) { - throw new AbortException(String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); - } - QueueItemData queueItem = getQueueItemData(queueId, context); - if (queueItem.isExecuted()) { - buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); - } - return buildInfo; - } - - // Only avoid url cache while loop inquiry - String buildUrlString = String.format("%sapi/json/?seed=%d", buildInfo.getBuildURL(), System.currentTimeMillis()); - JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", context); - - try { - if (responseObject == null || responseObject.getString("result") == null && !responseObject.getBoolean("building")) { - return buildInfo; - } else if (responseObject.getBoolean("building")) { - buildInfo.setBuildStatus(RemoteBuildStatus.RUNNING); - } else if (responseObject.getString("result") != null) { - buildInfo.setBuildResult(responseObject.getString("result")); - } else { - context.logger.println("WARNING: Unhandled condition!"); - } - } catch (Exception ex) { - } - return buildInfo; - } - - private String getConsoleOutput(URL url, BuildContext context) - throws IOException { - - return getConsoleOutput( url, context, 1 ); - } - - /** - * Orchestrates all calls to the remote server. - * Also takes care of any credentials or failed-connection retries. - * - * @param urlString - * the URL that needs to be called. - * @param requestType - * the type of request (GET, POST, etc). - * @param context - * the context of this Builder/BuildStep. - * @return JSONObject - * a valid JSON object, or null. - * @throws IOException - * if there is an error identifying the remote host, or - * if there is an error setting the authorization header, or - * if the request fails due to an unknown host, unauthorized credentials, or another reason. - */ - public JSONObject sendHTTPCall(String urlString, String requestType, BuildContext context) - throws IOException { - - return sendHTTPCall( urlString, requestType, context, 1 ).getBody(); - } - - private String getConsoleOutput(URL url, BuildContext context, int numberOfAttempts) - throws IOException { - - int retryLimit = this.getConnectionRetryLimit(); - - String consoleOutput = null; - - URL buildUrl = new URL(url, "consoleText"); - - HttpURLConnection connection = getAuthorizedConnection(context, buildUrl); - - int responseCode = 0; - try { - connection.setDoInput(true); - connection.setRequestProperty("Accept", "application/json"); - connection.setRequestMethod("GET"); - // wait up to 5 seconds for the connection to be open - connection.setConnectTimeout(5000); - connection.connect(); - responseCode = connection.getResponseCode(); - if(responseCode == 401) { - throw new UnauthorizedException(buildUrl); - } else if(responseCode == 403) { - throw new ForbiddenException(buildUrl); - } else if(responseCode == 404) { - throw new UrlNotFoundException(buildUrl); - } else { - consoleOutput = readInputStream(connection); - } - } catch (UnknownHostException e) { - this.failBuild(e, context.logger); - } catch (UnauthorizedException e) { - this.failBuild(e, context.logger); - } catch (ForbiddenException e) { - this.failBuild(e, context.logger); - } catch (IOException e) { - - //If we have connectionRetryLimit set to > 0 then retry that many times. - if( numberOfAttempts <= retryLimit) { - context.logger.println(String.format( - "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s", - (responseCode == 0 ? "" : "[" + responseCode + "]"), this.pollInterval, url)); - e.printStackTrace(); - - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException ex) { - this.failBuild(ex, context.logger); - } - - context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); - numberOfAttempts++; - consoleOutput = getConsoleOutput(url, context, numberOfAttempts); - } else if(numberOfAttempts > retryLimit){ - //reached the maximum number of retries, time to fail - this.failBuild(new Exception("Max number of connection retries have been exeeded."), context.logger); - } else{ - //something failed with the connection and we retried the max amount of times... so throw an exception to mark the build as failed. - this.failBuild(e, context.logger); - } - - } finally { - // always make sure we close the connection - if (connection != null) { - connection.disconnect(); - } - } - return consoleOutput; - } - - /** - * Same as sendHTTPCall, but keeps track of the number of failed connection attempts (aka: the number of times this - * method has been called). - * In the case of a failed connection, the method calls it self recursively and increments the number of attempts. - * - * @see sendHTTPCall - * @param urlString - * the URL that needs to be called. - * @param requestType - * the type of request (GET, POST, etc). - * @param context - * the context of this Builder/BuildStep. - * @param numberOfAttempts - * number of time that the connection has been attempted. - * @return {@link ConnectionResponse} - * the response to the HTTP request. - * @throws IOException - * if there is an error identifying the remote host, or - * if there is an error setting the authorization header, or - * if the request fails due to an unknown host or unauthorized credentials, or - * if the request fails due to another reason and the number of attempts is exceeded. - */ - private ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, int numberOfAttempts) - throws IOException { - - int retryLimit = this.getConnectionRetryLimit(); - - JSONObject responseObject = null; - Map> responseHeader = null; - int responseCode = 0; - - URL url = new URL(urlString); - HttpURLConnection connection = getAuthorizedConnection(context, url); - - try { - connection.setDoInput(true); - connection.setRequestProperty("Accept", "application/json"); - connection.setRequestMethod(requestType); - addCrumbToConnection(connection, context); - // wait up to 5 seconds for the connection to be open - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - connection.connect(); - responseHeader = connection.getHeaderFields(); - responseCode = connection.getResponseCode(); - if(responseCode == 401) { - throw new UnauthorizedException(url); - } else if(responseCode == 403) { - throw new ForbiddenException(url); - } else if(responseCode == 404) { - throw new UrlNotFoundException(url); - } else { - String response = trimToNull(readInputStream(connection)); - - // JSONSerializer serializer = new JSONSerializer(); - // need to parse the data we get back into struct - //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); - - //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 - //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. - //But in newer versions of Jenkins, it just returns an empty response. - //So we need to compensate and check for both. - if ( responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { - return new ConnectionResponse(responseHeader, responseCode); - } else { - responseObject = (JSONObject) JSONSerializer.toJSON(response); - } - } - - } catch (UnknownHostException e) { - this.failBuild(e, context.logger); - } catch (UnauthorizedException e) { - this.failBuild(e, context.logger); - } catch (ForbiddenException e) { - this.failBuild(e, context.logger); - } catch (UrlNotFoundException e) { - this.failBuild(e, context.logger); - } catch (IOException e) { - - //E.g. "HTTP/1.1 403 No valid crumb was included in the request" - List hints = responseHeader != null ? responseHeader.get(null) : null; - String hintsString = (hints != null && hints.size() > 0) ? " - " + hints.toString() : ""; - - context.logger.println(e.getMessage() + hintsString); - //If we have connectionRetryLimit set to > 0 then retry that many times. - if( numberOfAttempts <= retryLimit) { - context.logger.println(String.format( - "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s", - (responseCode == 0 ? "" : "[" + responseCode + "]"), this.pollInterval, urlString)); - e.printStackTrace(); - - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException ex) { - this.failBuild(ex, context.logger); - } - - context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); - numberOfAttempts++; - responseObject = sendHTTPCall(urlString, requestType, context, numberOfAttempts).getBody(); - }else if(numberOfAttempts > retryLimit){ - //reached the maximum number of retries, time to fail - this.failBuild(new Exception("Max number of connection retries have been exeeded."), context.logger); - }else{ - //something failed with the connection and we retried the max amount of times... so throw an exception to mark the build as failed. - this.failBuild(e, context.logger); - } - - } finally { - // always make sure we close the connection - if (connection != null) { - connection.disconnect(); - } - } - return new ConnectionResponse(responseHeader, responseObject, responseCode); - } - - /** - * For POST requests a crumb is needed. This methods gets a crumb and sets it in the header. - * https://wiki.jenkins.io/display/JENKINS/Remote+access+API#RemoteaccessAPI-CSRFProtection - * - * @param connection - * @param context - * @throws IOException - */ - private void addCrumbToConnection(HttpURLConnection connection, BuildContext context) throws IOException - { - String method = connection.getRequestMethod(); - if(method != null && method.equalsIgnoreCase("POST")) { - JenkinsCrumb crumb = getCrumb(context); - if (crumb.isEnabledOnRemote()) { - connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); - } - } - } - - private String readInputStream(HttpURLConnection connection) throws IOException - { - BufferedReader rd = null; - try { - - InputStream is; - try { - is = connection.getInputStream(); - } catch (FileNotFoundException e) { - // In case of a e.g. 404 status - is = connection.getErrorStream(); - } - - rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); - String line; - StringBuilder response = new StringBuilder(); - while ((line = rd.readLine()) != null) { - if(response.length() > 0) response.append(NL); - response.append(line); - } - return response.toString(); - - } finally { - closeQuietly(rd); - } - } - - /** - * Tries to obtain a Jenkins Crumb from the remote Jenkins server. - * - * @param effectiveRemoteServer - * the remote Jenkins server. - * @param context - * the context of this Builder/BuildStep. - * @return {@link JenkinsCrumb} - * a JenkinsCrumb. - * @throws IOException - * if the request failed. - */ - @Nonnull - private JenkinsCrumb getCrumb(BuildContext context) throws IOException - { - String address = context.effectiveRemoteServer.getAddress(); - if (address == null) { - throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); - } - URL crumbProviderUrl; - try { - String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); - crumbProviderUrl = new URL(address.concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); - HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl); - int responseCode = connection.getResponseCode(); - if(responseCode == 401) { - throw new UnauthorizedException(crumbProviderUrl); - } else if(responseCode == 403) { - throw new ForbiddenException(crumbProviderUrl); - } else if(responseCode == 404) { - context.logger.println("CSRF protection is disabled on the remote server."); - return new JenkinsCrumb(); - } else if(responseCode == 200){ - context.logger.println("CSRF protection is enabled on the remote server."); - String response = readInputStream(connection); - String[] split = response.split(":"); - return new JenkinsCrumb(split[0], split[1]); - } else { - throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", responseCode, connection.getResponseMessage())); - } - } catch (FileNotFoundException e) { - context.logger.println("CSRF protection is disabled on the remote server."); - return new JenkinsCrumb(); - } - } - - private HttpURLConnection getAuthorizedConnection(BuildContext context, URL url) throws IOException - { - URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); - - Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); - Auth2 overrideAuth = this.getAuth2(); - - if(overrideAuth != null && !(overrideAuth instanceof NullAuth)) { - //Override Authorization Header if configured locally - overrideAuth.setAuthorizationHeader(connection, context); - } else if (serverAuth != null) { - //Set Authorization Header configured globally for remoteServer - serverAuth.setAuthorizationHeader(connection, context); - } - - return (HttpURLConnection)connection; - } - - private void logAuthInformation(BuildContext context) throws IOException { - - Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); - Auth2 localAuth = this.getAuth2(); - if(localAuth != null && !(localAuth instanceof NullAuth)) { - String authString = (context.run == null) ? localAuth.getDescriptor().getDisplayName() : localAuth.toString((Item)context.run.getParent()); - context.logger.println(String.format(" Using job-level defined " + authString )); - } else if(serverAuth != null && !(serverAuth instanceof NullAuth)) { - String authString = (context.run == null) ? serverAuth.getDescriptor().getDisplayName() : serverAuth.toString((Item)context.run.getParent()); - context.logger.println(String.format(" Using globally defined " + authString)); - } else { - context.logger.println(" No credentials configured"); - } - } - - private void logConfiguration(BuildContext context, List effectiveParams) throws IOException { - String _job = getJob(); - String _jobExpanded = getJobExpanded(context); - String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; - String _remoteJenkinsName = getRemoteJenkinsName(); - String _remoteJenkinsUrl = getRemoteJenkinsUrl(); - Auth2 _auth = getAuth2(); - int _connectionRetryLimit = getConnectionRetryLimit(); - boolean _blockBuildUntilComplete = getBlockBuildUntilComplete(); - String _parameterFile = getParameterFile(); - String _parameters = (effectiveParams == null || effectiveParams.size() <= 0) ? "" : effectiveParams.toString(); - boolean _loadParamsFromFile = getLoadParamsFromFile(); - context.logger.println("################################################################################################################"); - context.logger.println(" Parameterized Remote Trigger Configuration:"); - context.logger.println( - String.format(" - job: %s %s", _job, _jobExpandedLogEntry)); - if(!isEmpty(_remoteJenkinsName)) { - context.logger.println( - String.format(" - remoteJenkinsName: %s", _remoteJenkinsName)); - } - if(!isEmpty(_remoteJenkinsUrl)) { - context.logger.println( - String.format(" - remoteJenkinsUrl: %s", _remoteJenkinsUrl)); - } - if(_auth != null && !(_auth instanceof NullAuth)) { - String authString = context.run == null ? _auth.getDescriptor().getDisplayName() : _auth.toString((Item)context.run.getParent()); - context.logger.println( - String.format(" - auth: %s", authString)); - } - context.logger.println( - String.format(" - parameters: %s", _parameters)); - if(_loadParamsFromFile) { - context.logger.println( - String.format(" - loadParamsFromFile: %s", _loadParamsFromFile)); - context.logger.println( - String.format(" - parameterFile: %s", _parameterFile)); - } - context.logger.println( - String.format(" - blockBuildUntilComplete: %s", _blockBuildUntilComplete)); - context.logger.println( - String.format(" - connectionRetryLimit: %s", _connectionRetryLimit)); - context.logger.println("################################################################################################################"); - } - - /** - * Helper function for character encoding - * - * @param dirtyValue - * @return encoded value - */ - private static String encodeValue(String dirtyValue) { - String cleanValue = ""; - - try { - cleanValue = URLEncoder.encode(dirtyValue, "UTF-8").replace("+", "%20"); - } catch (UnsupportedEncodingException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - return cleanValue; - } - - /** - * @return the configured remote Jenkins name. That's the ID of a globally configured remote host. - */ - public String getRemoteJenkinsName() { - return remoteJenkinsName; - } - - /** - * @return the configured remote Jenkins URL. This is not necessarily the effective Jenkins URL, e.g. if a full URL is specified for job! - */ - public String getRemoteJenkinsUrl() - { - return trimToNull(remoteJenkinsUrl); - } - - /** - * @return true, if the authorization is overridden in the job configuration, otherwise false. - * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. - */ - public boolean getOverrideAuth() { - if(auth2 == null) return false; - if(auth2 instanceof NullAuth) return false; - return true; - } - - public Auth2 getAuth2() { - return (auth2 != null) ? auth2 : DEFAULT_AUTH; - } - - public boolean getShouldNotFailBuild() { - return shouldNotFailBuild; - } - - public boolean getPreventRemoteBuildQueue() { - return preventRemoteBuildQueue; - } - - public int getPollInterval() { - return pollInterval; - } - - public boolean getBlockBuildUntilComplete() { - return blockBuildUntilComplete; - } - - /** - * @return the configured job value. Can be a job name or full job URL. - */ - public String getJob() { - return trimToEmpty(job); - } - - /** - * @return job value with expanded env vars. - * @throws IOException - * if there is an error replacing tokens. - */ - private String getJobExpanded(BasicBuildContext context) throws IOException { - return TokenMacroUtils.applyTokenMacroReplacements(getJob(), context); - } - - public String getToken() { - return trimToEmpty(token); - } - - public String getParameters() { - return trimToEmpty(parameters); - } - - public boolean getEnhancedLogging() { - return enhancedLogging; - } - - public boolean getLoadParamsFromFile() { - return loadParamsFromFile; - } - - public String getParameterFile() { - return trimToEmpty(parameterFile); - } - - public int getConnectionRetryLimit() { - return connectionRetryLimit; // For now, this is a constant - } - - /** - * Same as above, but takes in to consideration if the remote server has any default parameters set or not - * @param isRemoteJobParameterized Boolean indicating if the remote job is parameterized or not - * @return A string which represents a portion of the build URL - */ - private String getBuildTypeUrl(boolean isRemoteJobParameterized) { - boolean isParameterized = false; - - if(isRemoteJobParameterized || (this.getParameters().length() > 0)) { - isParameterized = true; - } - - if (isParameterized) { - return paramerizedBuildUrl; - } else { - return normalBuildUrl; - } - } - - private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException { - - String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - remoteJobUrl += "/api/json"; - - ConnectionResponse response = sendHTTPCall( remoteJobUrl, "GET", context, 1 ); - if(response.getResponseCode() < 400 && response.getBody() != null) { - - return response.getBody(); - - } else if(response.getResponseCode() == 401 || response.getResponseCode() == 403) { - throw new AbortException("Unauthorized to trigger " + remoteJobUrl + " - status code " + response.getResponseCode()); - } else if(response.getResponseCode() == 404) { - throw new AbortException("Remote job does not exist " + remoteJobUrl + " - status code " + response.getResponseCode()); - } else { - throw new AbortException("Unexpected response from " + remoteJobUrl + " - status code " + response.getResponseCode()); - } - } - - /** - * Pokes the remote server to see if it has default parameters defined or not. - * - * @param remoteJobMetadata - * from {@link #getRemoteJobMetadata(String, BuildContext)}. - * @return true if the remote job has parameters, otherwise false. - * @throws IOException - * if it is not possible to identify if the job is parameterized. - */ - private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IOException - { - boolean isParameterized = false; - if (remoteJobMetadata != null) { - if (remoteJobMetadata.getJSONArray("actions").size() >= 1) { - for(Object obj : remoteJobMetadata.getJSONArray("actions")) { - if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { - isParameterized = true; - } - } - } - } - else { - throw new AbortException("Could not identify if job is parameterized. Job metadata not accessible or with unexpected content."); - } - return isParameterized; - } - - protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) throws AbortException - { - if(isEmpty(jobNameOrUrl)) throw new IllegalArgumentException("Invalid job name/url: " + jobNameOrUrl); - String remoteJobUrl; - String _jobNameOrUrl = jobNameOrUrl.trim(); - if(FormValidationUtils.isURL(_jobNameOrUrl)) { - remoteJobUrl = _jobNameOrUrl; - } else { - remoteJobUrl = remoteServer.getAddress(); - if (remoteJobUrl == null) { - throw new AbortException("The remote server address can not be empty, or it must be overridden on the job configuration."); - } - while(remoteJobUrl.endsWith("/")) remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length()-1); - - String[] split = _jobNameOrUrl.trim().split("/"); - for(String segment : split) { - remoteJobUrl = String.format("%s/job/%s", remoteJobUrl, encodeValue(segment)); - } - } - return remoteJobUrl; - } - - // 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 DescriptorImpl getDescriptor() { - return (DescriptorImpl) super.getDescriptor(); - } - - // This indicates to Jenkins that this is an implementation of an extension - // point. - @Extension - public static final class DescriptorImpl 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. - */ - private CopyOnWriteList remoteSites = new CopyOnWriteList(); - - /** - * In order to load the persisted global configuration, you have to call load() in the constructor. - */ - public DescriptorImpl() { - this(true); - } - - private DescriptorImpl(boolean load) { - if(load) load(); - } - - public static DescriptorImpl newInstanceForTests() - { - return new DescriptorImpl(false); - } - - /* - /** - * 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. - */ - /* - * public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException { if - * (value.length() == 0) return FormValidation.error("Please set a name"); if (value.length() < 4) return - * FormValidation.warning("Isn't the name too short?"); return FormValidation.ok(); } - */ - - public boolean isApplicable(Class aClass) { - // Indicates that this builder can be used with all kinds of project - // types - return true; - } - - /** - * This human readable name is used in the configuration screen. - */ - public String getDisplayName() { - return "Trigger a remote parameterized job"; - } - - @Override - public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { - - remoteSites.replaceBy(req.bindJSONToList(RemoteJenkinsServer.class, formData.get("remoteSites"))); - save(); - - return super.configure(req, formData); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckJob( - @QueryParameter("job") final String value, - @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, - @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); - if(result.isAffected(AffectedField.JOB_NAME_OR_URL)) return result.formValidation; - return FormValidation.ok(); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckRemoteJenkinsUrl( - @QueryParameter("remoteJenkinsUrl") final String value, - @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, - @QueryParameter("job") final String job) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); - if(result.isAffected(AffectedField.REMOTE_JENKINS_URL)) return result.formValidation; - return FormValidation.ok(); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckRemoteJenkinsName( - @QueryParameter("remoteJenkinsName") final String value, - @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, - @QueryParameter("job") final String job) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); - if(result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) return result.formValidation; - return FormValidation.ok(); - } - - @Restricted(NoExternalUse.class) - @Nonnull - public ListBoxModel doFillRemoteJenkinsNameItems() { - ListBoxModel model = new ListBoxModel(); - - model.add(""); - for (RemoteJenkinsServer site : getRemoteSites()) { - model.add(site.getDisplayName()); - } - - return model; - } - - public RemoteJenkinsServer[] getRemoteSites() { - - return remoteSites.toArray(new RemoteJenkinsServer[this.remoteSites.size()]); - } - - public void setRemoteSites(RemoteJenkinsServer... remoteSites) { - this.remoteSites.replaceBy(remoteSites); - } - - public static List getAuth2Descriptors() { - return Auth2.all(); - } - - public static Auth2Descriptor getDefaultAuth2Descriptor() { - return NullAuth.DESCRIPTOR; - } - - } + try { + if (br != null) { + br.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return getCleanedParameters(parameterList); + } + + /** + * Strip out any empty strings from the parameterList + */ + private void removeEmptyElements(Collection collection) { + collection.removeAll(Arrays.asList(null, "")); + collection.removeAll(Arrays.asList(null, " ")); + } + + /** + * Same as "getParameterList", but removes comments and empty strings Notice + * that no type of character encoding is happening at this step. All encoding + * happens in the "buildUrlQueryString" method. + * + * @param List + * parameters + * @return List of build parameters + */ + private List getCleanedParameters(List parameters) { + List params = new ArrayList(parameters); + removeEmptyElements(params); + removeCommentsFromParameters(params); + return params; + } + + /** + * Strip out any comments (lines that start with a #) from the collection that + * is passed in. + */ + private void removeCommentsFromParameters(Collection collection) { + List itemsToRemove = new ArrayList(); + + for (String parameter : collection) { + if (parameter.indexOf("#") == 0) { + itemsToRemove.add(parameter); + } + } + collection.removeAll(itemsToRemove); + } + + /** + * Tries to identify the effective Remote Host configuration based on the + * different parameters like remoteJenkinsName and the globally + * configured remote host, remoteJenkinsURL which overrides the + * address locally or job which can be a full job URL. + * + * @param context + * the context of this Builder/BuildStep. + * @return {@link RemoteJenkinsServer} a RemoteJenkinsServer object, never null. + * @throws AbortException + * if no server found and remoteJenkinsUrl empty. + * @throws MalformedURLException + * if remoteJenkinsName no valid URL or + * job an URL but nor valid. + */ + @Nonnull + public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context) throws IOException { + RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); + RemoteJenkinsServer server = globallyConfiguredServer; + String expandedJob = getJobExpanded(context); + boolean isJobEmpty = isEmpty(trimToNull(expandedJob)); + boolean isJobUrl = FormValidationUtils.isURL(expandedJob); + boolean isRemoteUrlEmpty = isEmpty(trimToNull(this.remoteJenkinsUrl)); + boolean isRemoteUrlSet = FormValidationUtils.isURL(this.remoteJenkinsUrl); + boolean isRemoteNameEmpty = isEmpty(trimToNull(this.remoteJenkinsName)); + + if (isJobEmpty) + throw new AbortException("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified."); + if (!isRemoteUrlEmpty && !isRemoteUrlSet) + throw new AbortException(String.format( + "The 'Override remote host URL' parameter value (remoteJenkinsUrl: '%s') is no valid URL", + this.remoteJenkinsUrl)); + + if (isJobUrl) { + // Full job URL configured - get remote Jenkins root URL from there + if (server == null) + server = new RemoteJenkinsServer(); + server.setAddress(getRootUrlFromJobUrl(expandedJob)); + + } else if (isRemoteUrlSet) { + // Remote Jenkins root URL overridden locally in Job/Pipeline + if (server == null) + server = new RemoteJenkinsServer(); + server.setAddress(this.remoteJenkinsUrl); + + } + + if (server == null) { + if (!isJobUrl) { + if (!isRemoteUrlSet && isRemoteNameEmpty) + throw new AbortException("Configuration of the remote Jenkins host is missing."); + if (!isRemoteUrlSet && !isRemoteNameEmpty && globallyConfiguredServer == null) + throw new AbortException(String.format( + "Could get remote host with ID '%s' configured in Jenkins global configuration. Please check your global configuration.", + this.remoteJenkinsName)); + } + // Generic error message + throw new AbortException(String.format( + "Could not identify remote host - neither via 'Remote Job Name or URL' (job:'%s'), globally configured" + + " remote host (remoteJenkinsName:'%s') nor 'Override remote host URL' (remoteJenkinsUrl:'%s').", + expandedJob, this.remoteJenkinsName, this.remoteJenkinsUrl)); + } + return server; + } + + /** + * Lookup up the globally configured Remote Jenkins Server based on display name + * + * @param displayName + * Name of the configuration you are looking for + * @return A deep-copy of the RemoteJenkinsServer object configured globally + */ + public @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { + if (isEmpty(displayName)) + return null; + RemoteJenkinsServer server = null; + for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { + // if we find a match, then stop looping + if (displayName.equals(host.getDisplayName())) { + try { + server = host.clone(); + break; + } catch (CloneNotSupportedException e) { + // Clone is supported by RemoteJenkinsServer + throw new RuntimeException(e); + } + } + } + return server; + } + + protected static String removeTrailingSlashes(String string) { + if (isEmpty(string)) + return string; + string = string.trim(); + while (string.endsWith("/")) + string = string.substring(0, string.length() - 1); + return string; + } + + protected static String removeQueryParameters(String string) { + if (isEmpty(string)) + return string; + string = string.trim(); + int idx = string.indexOf("?"); + if (idx > 0) + string = string.substring(0, idx); + return string; + } + + protected static String removeHashParameters(String string) { + if (isEmpty(string)) + return string; + string = string.trim(); + int idx = string.indexOf("#"); + if (idx > 0) + string = string.substring(0, idx); + return string; + } + + private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException { + if (isEmpty(jobUrl)) + return null; + if (FormValidationUtils.isURL(jobUrl)) { + int index = jobUrl.indexOf("/job/"); + if (index < 0) + throw new MalformedURLException("Expected '/job/' element in job URL but was: " + jobUrl); + return jobUrl.substring(0, index); + } else { + return null; + } + } + + /** + * Convenience function to mark the build as failed. It's intended to only be + * called from this.perform(). + * + * @param e + * exception that caused the build to fail. + * @param logger + * build listener. + * @throws IOException + * if the build fails and shouldNotFailBuild is not + * set. + */ + protected void failBuild(Exception e, PrintStream logger) throws IOException { + StringBuilder msg = new StringBuilder(); + if (e instanceof InterruptedException) { + Thread current = Thread.currentThread(); + msg.append(String.format("[Thread: %s/%s]: ", current.getId(), current.getName())); + } + msg.append(String.format("Remote build failed with '%s' for the following reason: '%s'.%s", + e.getClass().getSimpleName(), e.getMessage(), + this.getShouldNotFailBuild() ? " But the build will continue." : "")); + if (enhancedLogging) { + msg.append(NL).append(ExceptionUtils.getFullStackTrace(e)); + } + if (logger != null) + logger.println("ERROR: " + msg.toString()); + if (!this.getShouldNotFailBuild()) { + throw new AbortException(e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) + throws InterruptedException, IOException, IllegalArgumentException { + FilePath workspace = build.getWorkspace(); + if (workspace == null) + throw new IllegalArgumentException("The workspace can not be null"); + perform(build, workspace, launcher, listener); + return true; + } + + /** + * Triggers the remote job and, waits until completion if + * blockBuildUntilComplete is set. + * + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if there is an error retrieving the remote build data, or, if + * there is an error retrieving the remote build status, or, if + * there is an error retrieving the console output of the remote + * build, or, if the remote build does not succeed. + */ + @Override + public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) + throws InterruptedException, IOException { + RemoteJenkinsServer effectiveRemoteServer = evaluateEffectiveRemoteHost( + new BasicBuildContext(build, workspace, listener)); + BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), + effectiveRemoteServer); + Handle handle = performTriggerAndGetQueueId(context); + performWaitForBuild(context, handle); + } + + /** + * Triggers the remote job, identifies the queue ID and, returns a + * Handle to this remote execution. + * + * @param context + * the context of this Builder/BuildStep. + * @return Handle to further tracking of the remote build status. + * @throws IOException + * if there is an error triggering the remote job. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * + */ + public Handle performTriggerAndGetQueueId(BuildContext context) throws IOException, InterruptedException { + List cleanedParams = getCleanedParameters(getParameterList(context)); + String jobNameOrUrl = this.getJob(); + String securityToken = this.getToken(); + try { + cleanedParams = TokenMacroUtils.applyTokenMacroReplacements(cleanedParams, context); + jobNameOrUrl = TokenMacroUtils.applyTokenMacroReplacements(jobNameOrUrl, context); + securityToken = TokenMacroUtils.applyTokenMacroReplacements(securityToken, context); + } catch (IOException e) { + this.failBuild(e, context.logger); + } + + logConfiguration(context, cleanedParams); + + final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); + boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); + + final String triggerUrlString = HttpHelper.buildTriggerUrl(jobNameOrUrl, securityToken, null, + isRemoteParameterized, context); + + // token shouldn't be exposed in the console + final String jobUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); + context.logger.println(String.format("Triggering %s remote job '%s'", + (isRemoteParameterized ? "parameterized" : "non-parameterized"), jobUrlString)); + + logAuthInformation(context); + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + + context.logger.println("Triggering remote job now."); + + try { + ConnectionResponse responseRemoteJob = HttpHelper.post(triggerUrlString, context, cleanedParams, + this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2()); + QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); + buildInfo.setQueueId(queueItem.getId()); + buildInfo = updateBuildInfo(buildInfo, context); + } catch (IOException|InterruptedException e) { + this.failBuild(e, context.logger); + } + + return new Handle(this, buildInfo, context.currentItem, context.effectiveRemoteServer, remoteJobMetadata); + } + + /** + * Checks the remote build status and, waits for completion if + * blockBuildUntilComplete is set. + * + * @param context + * the context of this Builder/BuildStep. + * @param handle + * the handle to the remote execution. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if any HTTP error or business logic error + */ + public void performWaitForBuild(BuildContext context, Handle handle) throws IOException, InterruptedException { + String jobName = handle.getJobName(); + + RemoteBuildInfo buildInfo = handle.getBuildInfo(); + String queueId = buildInfo.getQueueId(); + if (queueId == null) { + throw new AbortException( + String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); + } + context.logger.println(" Remote job queue number: " + buildInfo.getQueueId()); + + if (buildInfo.isQueued()) { + context.logger.println("Waiting for remote build to be executed..."); + } + + while (buildInfo.isQueued()) { + context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); + Thread.sleep(this.pollInterval * 1000); + buildInfo = updateBuildInfo(buildInfo, context); + handle.setBuildInfo(buildInfo); + } + + URL jobURL = buildInfo.getBuildURL(); + int jobNumber = buildInfo.getBuildNumber(); + + if (jobURL == null || jobNumber == 0) { + throw new AbortException(String.format("Unexpected status: %s", buildInfo.toString())); + } + + context.logger.println("Remote build started!"); + context.logger.println(" Remote build URL: " + jobURL); + context.logger.println(" Remote build number: " + jobNumber); + + if (context.run != null) + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, + buildInfo); + + if (this.getBlockBuildUntilComplete()) { + context.logger.println("Blocking local job until remote job completes."); + + buildInfo = updateBuildInfo(buildInfo, context); + handle.setBuildInfo(buildInfo); + + if (buildInfo.isRunning()) { + context.logger.println("Waiting for remote build to finish ..."); + } + + while (buildInfo.isRunning()) { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + Thread.sleep(this.pollInterval * 1000); + buildInfo = updateBuildInfo(buildInfo, context); + handle.setBuildInfo(buildInfo); + } + + context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); + if (context.run != null) + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, + buildInfo); + + if (this.getEnhancedLogging()) { + String consoleOutput = getConsoleOutput(jobURL, context); + + context.logger.println(); + context.logger.println("Console output of remote job:"); + context.logger + .println("--------------------------------------------------------------------------------"); + context.logger.println(consoleOutput); + context.logger + .println("--------------------------------------------------------------------------------"); + } + + // If build did not finish with 'success' or 'unstable' then fail build step. + if (buildInfo.getResult() != Result.SUCCESS && buildInfo.getResult() != Result.UNSTABLE) { + // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so + // will decide how to + // handle the failure. + this.failBuild(new Exception("The remote job did not succeed."), context.logger); + } + } else { + context.logger.println("Not blocking local job until remote job completes - fire and forget."); + } + } + + /** + * Sends a HTTP request to the API of the remote server requesting a queue item. + * + * @param queueId + * the id of the remote job on the queue. + * @param context + * the context of this Builder/BuildStep. + * @return {@link QueueItemData} the queue item data. + * @throws IOException + * if there is an error identifying the remote host, or if there is + * an error setting the authorization header, or if the request + * fails due to an unknown host, unauthorized credentials, or + * another reason, or if there is an invalid queue response. + * @throws InterruptedException + * if any thread has interrupted the current thread. + */ + @Nonnull + private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) + throws IOException, InterruptedException { + + if (context.effectiveRemoteServer.getAddress() == null) { + throw new AbortException( + "The remote server address can not be empty, or it must be overridden on the job configuration."); + } + String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getAddress(), + queueId); + ConnectionResponse response = doGet(queueQuery, context); + JSONObject queueResponse = response.getBody(); + + if (queueResponse == null || queueResponse.isNullObject()) { + throw new AbortException(String.format("Unexpected queue item response: code %s for request %s", + response.getResponseCode(), queueQuery)); + } + + QueueItemData queueItem = new QueueItemData(); + queueItem.update(context, queueResponse); + + if (queueItem.isBlocked()) + context.logger.println(String.format("The remote job is blocked. %s.", queueItem.getWhy())); + + if (queueItem.isPending()) + context.logger.println(String.format("The remote job is pending. %s.", queueItem.getWhy())); + + if (queueItem.isBuildable()) + context.logger.println(String.format("The remote job is buildable. %s.", queueItem.getWhy())); + + if (queueItem.isCancelled()) + throw new AbortException("The remote job was canceled"); + + return queueItem; + } + + @Nonnull + public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonnull BuildContext context) + throws IOException, InterruptedException { + + if (buildInfo.isNotTriggered()) + return buildInfo; + + if (buildInfo.isQueued()) { + String queueId = buildInfo.getQueueId(); + if (queueId == null) { + throw new AbortException( + String.format("Unexpected status: %s. The queue id was not found.", buildInfo.toString())); + } + QueueItemData queueItem = getQueueItemData(queueId, context); + if (queueItem.isExecuted()) { + buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); + } + return buildInfo; + } + + // Only avoid url cache while loop inquiry + String buildUrlString = String.format("%sapi/json/?seed=%d", buildInfo.getBuildURL(), + System.currentTimeMillis()); + JSONObject responseObject = doGet(buildUrlString, context).getBody(); + + try { + if (responseObject == null + || responseObject.getString("result") == null && !responseObject.getBoolean("building")) { + return buildInfo; + } else if (responseObject.getBoolean("building")) { + buildInfo.setBuildStatus(RemoteBuildStatus.RUNNING); + } else if (responseObject.getString("result") != null) { + buildInfo.setBuildResult(responseObject.getString("result")); + } else { + context.logger.println("WARNING: Unhandled condition!"); + } + } catch (Exception ex) { + } + return buildInfo; + } + + private String getConsoleOutput(URL url, BuildContext context) throws IOException, InterruptedException { + URL buildUrl = new URL(url, "consoleText"); + return HttpHelper.getRawResp(buildUrl.toString(), HttpHelper.HTTP_GET, context, null, 1, this.getPollInterval(), + this.getConnectionRetryLimit(), this.getAuth2()); + } + + /** + * Orchestrates all calls to the remote server. Also takes care of any + * credentials or failed-connection retries. + * + * @param urlString + * the URL that needs to be called. + * @param context + * the context of this Builder/BuildStep. + * @return JSONObject a valid JSON object, or null. + * @throws InterruptedException + * if any thread has interrupted the current thread. + * @throws IOException + * if any HTTP error occurred. + */ + public ConnectionResponse doGet(String urlString, BuildContext context) throws IOException, InterruptedException { + return HttpHelper.get(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), + this.getAuth2()); + } + + private void logAuthInformation(BuildContext context) throws IOException { + + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); + Auth2 localAuth = this.getAuth2(); + if (localAuth != null && !(localAuth instanceof NullAuth)) { + String authString = (context.run == null) ? localAuth.getDescriptor().getDisplayName() + : localAuth.toString((Item) context.run.getParent()); + context.logger.println(String.format(" Using job-level defined " + authString)); + } else if (serverAuth != null && !(serverAuth instanceof NullAuth)) { + String authString = (context.run == null) ? serverAuth.getDescriptor().getDisplayName() + : serverAuth.toString((Item) context.run.getParent()); + context.logger.println(String.format(" Using globally defined " + authString)); + } else { + context.logger.println(" No credentials configured"); + } + } + + private void logConfiguration(BuildContext context, List effectiveParams) throws IOException { + String _job = getJob(); + String _jobExpanded = getJobExpanded(context); + String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; + String _remoteJenkinsName = getRemoteJenkinsName(); + String _remoteJenkinsUrl = getRemoteJenkinsUrl(); + Auth2 _auth = getAuth2(); + int _connectionRetryLimit = getConnectionRetryLimit(); + boolean _blockBuildUntilComplete = getBlockBuildUntilComplete(); + String _parameterFile = getParameterFile(); + String _parameters = (effectiveParams == null || effectiveParams.size() <= 0) ? "" : effectiveParams.toString(); + boolean _loadParamsFromFile = getLoadParamsFromFile(); + context.logger.println( + "################################################################################################################"); + context.logger.println(" Parameterized Remote Trigger Configuration:"); + context.logger.println(String.format(" - job: %s %s", _job, _jobExpandedLogEntry)); + if (!isEmpty(_remoteJenkinsName)) { + context.logger.println(String.format(" - remoteJenkinsName: %s", _remoteJenkinsName)); + } + if (!isEmpty(_remoteJenkinsUrl)) { + context.logger.println(String.format(" - remoteJenkinsUrl: %s", _remoteJenkinsUrl)); + } + if (_auth != null && !(_auth instanceof NullAuth)) { + String authString = context.run == null ? _auth.getDescriptor().getDisplayName() + : _auth.toString((Item) context.run.getParent()); + context.logger.println(String.format(" - auth: %s", authString)); + } + context.logger.println(String.format(" - parameters: %s", _parameters)); + if (_loadParamsFromFile) { + context.logger.println(String.format(" - loadParamsFromFile: %s", _loadParamsFromFile)); + context.logger.println(String.format(" - parameterFile: %s", _parameterFile)); + } + context.logger.println(String.format(" - blockBuildUntilComplete: %s", _blockBuildUntilComplete)); + context.logger.println(String.format(" - connectionRetryLimit: %s", _connectionRetryLimit)); + context.logger.println( + "################################################################################################################"); + } + + /** + * @return the configured remote Jenkins name. That's the ID of a globally + * configured remote host. + */ + public String getRemoteJenkinsName() { + return remoteJenkinsName; + } + + /** + * @return the configured remote Jenkins URL. This is not necessarily the + * effective Jenkins URL, e.g. if a full URL is specified for + * job! + */ + public String getRemoteJenkinsUrl() { + return trimToNull(remoteJenkinsUrl); + } + + /** + * @return true, if the authorization is overridden in the job configuration, + * otherwise false. + * @deprecated since 2.3.0-SNAPSHOT - use {@link #getAuth2()} instead. + */ + public boolean getOverrideAuth() { + if (auth2 == null) + return false; + if (auth2 instanceof NullAuth) + return false; + return true; + } + + public Auth2 getAuth2() { + return (auth2 != null) ? auth2 : DEFAULT_AUTH; + } + + public boolean getShouldNotFailBuild() { + return shouldNotFailBuild; + } + + public boolean getPreventRemoteBuildQueue() { + return preventRemoteBuildQueue; + } + + public int getPollInterval() { + return pollInterval; + } + + public boolean getBlockBuildUntilComplete() { + return blockBuildUntilComplete; + } + + /** + * @return the configured job value. Can be a job name or full job + * URL. + */ + public String getJob() { + return trimToEmpty(job); + } + + /** + * @return job value with expanded env vars. + * @throws IOException + * if there is an error replacing tokens. + */ + private String getJobExpanded(BasicBuildContext context) throws IOException { + return TokenMacroUtils.applyTokenMacroReplacements(getJob(), context); + } + + public String getToken() { + return trimToEmpty(token); + } + + public String getParameters() { + return trimToEmpty(parameters); + } + + public boolean getEnhancedLogging() { + return enhancedLogging; + } + + public boolean getLoadParamsFromFile() { + return loadParamsFromFile; + } + + public String getParameterFile() { + return trimToEmpty(parameterFile); + } + + public int getConnectionRetryLimit() { + return connectionRetryLimit; // For now, this is a constant + } + + private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) + throws IOException, InterruptedException { + + String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); + remoteJobUrl += "/api/json"; + + ConnectionResponse response = doGet(remoteJobUrl, context); + if (response.getResponseCode() < 400 && response.getBody() != null) { + + return response.getBody(); + + } else if (response.getResponseCode() == 401 || response.getResponseCode() == 403) { + throw new AbortException( + "Unauthorized to trigger " + remoteJobUrl + " - status code " + response.getResponseCode()); + } else if (response.getResponseCode() == 404) { + throw new AbortException( + "Remote job does not exist " + remoteJobUrl + " - status code " + response.getResponseCode()); + } else { + throw new AbortException( + "Unexpected response from " + remoteJobUrl + " - status code " + response.getResponseCode()); + } + } + + /** + * Pokes the remote server to see if it has default parameters defined or not. + * + * @param remoteJobMetadata + * from {@link #getRemoteJobMetadata(String, BuildContext)}. + * @return true if the remote job has parameters, otherwise false. + * @throws IOException + * if it is not possible to identify if the job is parameterized. + */ + private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IOException { + boolean isParameterized = false; + if (remoteJobMetadata != null) { + if (remoteJobMetadata.getJSONArray("actions").size() >= 1) { + for (Object obj : remoteJobMetadata.getJSONArray("actions")) { + if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { + isParameterized = true; + } + } + } + } else { + throw new AbortException( + "Could not identify if job is parameterized. Job metadata not accessible or with unexpected content."); + } + return isParameterized; + } + + protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) + throws AbortException { + if (isEmpty(jobNameOrUrl)) + throw new IllegalArgumentException("Invalid job name/url: " + jobNameOrUrl); + String remoteJobUrl; + String _jobNameOrUrl = jobNameOrUrl.trim(); + if (FormValidationUtils.isURL(_jobNameOrUrl)) { + remoteJobUrl = _jobNameOrUrl; + } else { + remoteJobUrl = remoteServer.getAddress(); + if (remoteJobUrl == null) { + throw new AbortException( + "The remote server address can not be empty, or it must be overridden on the job configuration."); + } + while (remoteJobUrl.endsWith("/")) + remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length() - 1); + + String[] split = _jobNameOrUrl.trim().split("/"); + for (String segment : split) { + remoteJobUrl = String.format("%s/job/%s", remoteJobUrl, HttpHelper.encodeValue(segment)); + } + } + return remoteJobUrl; + } + + // 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 DescriptorImpl getDescriptor() { + return (DescriptorImpl) super.getDescriptor(); + } + + // This indicates to Jenkins that this is an implementation of an extension + // point. + @Extension + public static final class DescriptorImpl 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. + */ + private CopyOnWriteList remoteSites = new CopyOnWriteList(); + + /** + * In order to load the persisted global configuration, you have to call load() + * in the constructor. + */ + public DescriptorImpl() { + this(true); + } + + private DescriptorImpl(boolean load) { + if (load) + load(); + } + + public static DescriptorImpl newInstanceForTests() { + return new DescriptorImpl(false); + } + + /* + * /** 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. + */ + /* + * public FormValidation doCheckName(@QueryParameter String value) throws + * IOException, ServletException { if (value.length() == 0) return + * FormValidation.error("Please set a name"); if (value.length() < 4) return + * FormValidation.warning("Isn't the name too short?"); return + * FormValidation.ok(); } + */ + + public boolean isApplicable(@SuppressWarnings("rawtypes") Class aClass) { + // Indicates that this builder can be used with all kinds of project + // types + return true; + } + + /** + * This human readable name is used in the configuration screen. + */ + public String getDisplayName() { + return "Trigger a remote parameterized job"; + } + + @Override + public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { + + remoteSites.replaceBy(req.bindJSONToList(RemoteJenkinsServer.class, formData.get("remoteSites"))); + save(); + + return super.configure(req, formData); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckJob(@QueryParameter("job") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, + remoteJenkinsName, value); + if (result.isAffected(AffectedField.JOB_NAME_OR_URL)) + return result.formValidation; + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckRemoteJenkinsUrl(@QueryParameter("remoteJenkinsUrl") final String value, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, + remoteJenkinsName, job); + if (result.isAffected(AffectedField.REMOTE_JENKINS_URL)) + return result.formValidation; + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckRemoteJenkinsName(@QueryParameter("remoteJenkinsName") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, + job); + if (result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) + return result.formValidation; + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + @Nonnull + public ListBoxModel doFillRemoteJenkinsNameItems() { + ListBoxModel model = new ListBoxModel(); + + model.add(""); + for (RemoteJenkinsServer site : getRemoteSites()) { + model.add(site.getDisplayName()); + } + + return model; + } + + public RemoteJenkinsServer[] getRemoteSites() { + + return remoteSites.toArray(new RemoteJenkinsServer[this.remoteSites.size()]); + } + + public void setRemoteSites(RemoteJenkinsServer... remoteSites) { + this.remoteSites.replaceBy(remoteSites); + } + + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NullAuth.DESCRIPTOR; + } + + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java new file mode 100644 index 00000000..37da8801 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java @@ -0,0 +1,17 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions; + +import java.io.IOException; + +public class ExceedRetryLimitException extends IOException { + + /** + * + */ + private static final long serialVersionUID = 7817258508279153509L; + + @Override + public String getMessage() { + return "Max number of connection retries have been exeeded."; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 7ff86816..6f9cb37a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -19,6 +19,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.HttpHelper; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import hudson.model.Result; @@ -357,7 +358,7 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, PrintStreamWrapper log = new PrintStreamWrapper(); try { BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); - return remoteBuildConfiguration.sendHTTPCall(fileUrl.toString(), "GET", context); + return remoteBuildConfiguration.doGet(fileUrl.toString(), context); } finally { lastLog = log.getContent(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java new file mode 100644 index 00000000..aab4266f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -0,0 +1,532 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.apache.commons.io.IOUtils.closeQuietly; +import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.trimToNull; +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nonnull; + +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.JenkinsCrumb; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ExceedRetryLimitException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ForbiddenException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UnauthorizedException; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.UrlNotFoundException; + +import hudson.AbortException; +import hudson.ProxyConfiguration; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; +import net.sf.json.util.JSONUtils; + +public class HttpHelper { + + private static final String paramerizedBuildUrl = "/buildWithParameters"; + private static final String normalBuildUrl = "/build"; + private static final String buildTokenRootUrl = "/buildByToken"; + public static final String HTTP_GET = "GET"; + public static final String HTTP_POST = "POST"; + + private static Logger logger = Logger.getLogger(HttpHelper.class.getName()); + + /** + * Helper function to allow values to be added to the query string from any + * method. + * + * @param item + */ + private static String addToQueryString(String queryString, String item) { + if (isBlank(queryString)) { + return item; + } else { + return queryString + "&" + item; + } + } + + /** + * Return the Collection in an encoded query-string. + * + * @param parameters + * the parameters needed to trigger the remote job. + * @return query-parameter-formated URL-encoded string. + */ + private static String buildUrlQueryString(Collection parameters) { + + // List to hold the encoded parameters + List encodedParameters = new ArrayList(); + + if (parameters != null) { + for (String parameter : parameters) { + + // Step #1 - break apart the parameter-pairs (because we don't want to encode + // the "=" character) + String[] splitParameters = parameter.split("="); + + // List to hold each individually encoded parameter item + List encodedItems = new ArrayList(); + for (String item : splitParameters) { + try { + // Step #2 - encode each individual parameter item add the encoded item to its + // corresponding list + + encodedItems.add(encodeValue(item)); + + } catch (Exception e) { + // do nothing + // because we are "hard-coding" the encoding type, there is a 0% chance that + // this will fail. + logger.warning(e.toString()); + } + + } + + // Step #3 - reunite the previously separated parameter items and add them to + // the corresponding list + encodedParameters.add(StringUtils.join(encodedItems, "=")); + } + } + return StringUtils.join(encodedParameters, "&"); + } + + /** + * Same as above, but takes in to consideration if the remote server has any + * default parameters set or not + * + * @param isRemoteJobParameterized + * Boolean indicating if the remote job is parameterized or not + * @return A string which represents a portion of the build URL + */ + private static String getBuildTypeUrl(boolean isRemoteJobParameterized, Collection params) { + boolean isParameterized = false; + + if (isRemoteJobParameterized || (params != null && params.size() > 0)) { + isParameterized = true; + } + + if (isParameterized) { + return paramerizedBuildUrl; + } else { + return normalBuildUrl; + } + } + + protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String jobNameOrUrl) + throws AbortException { + if (isEmpty(jobNameOrUrl)) + throw new IllegalArgumentException("Invalid job name/url: " + jobNameOrUrl); + String remoteJobUrl; + String _jobNameOrUrl = jobNameOrUrl.trim(); + if (FormValidationUtils.isURL(_jobNameOrUrl)) { + remoteJobUrl = _jobNameOrUrl; + } else { + remoteJobUrl = remoteServer.getAddress(); + if (remoteJobUrl == null) { + throw new AbortException( + "The remote server address can not be empty, or it must be overridden on the job configuration."); + } + while (remoteJobUrl.endsWith("/")) + remoteJobUrl = remoteJobUrl.substring(0, remoteJobUrl.length() - 1); + + String[] split = _jobNameOrUrl.trim().split("/"); + for (String segment : split) { + remoteJobUrl = String.format("%s/job/%s", remoteJobUrl, encodeValue(segment)); + } + } + return remoteJobUrl; + } + + /** + * Helper function for character encoding + * + * @param dirtyValue + * something that wasn't encoded in UTF-8 + * @return encoded value + */ + public static String encodeValue(String dirtyValue) { + String cleanValue = ""; + + try { + cleanValue = URLEncoder.encode(dirtyValue, "UTF-8").replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return cleanValue; + } + + private static String readInputStream(HttpURLConnection connection) throws IOException { + BufferedReader rd = null; + try { + + InputStream is; + try { + is = connection.getInputStream(); + } catch (FileNotFoundException e) { + // In case of a e.g. 404 status + is = connection.getErrorStream(); + } + + rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String line; + StringBuilder response = new StringBuilder(); + while ((line = rd.readLine()) != null) { + if (response.length() > 0) + response.append(NL); + response.append(line); + } + return response.toString(); + + } finally { + closeQuietly(rd); + } + } + + /** + * Tries to obtain a Jenkins Crumb from the remote Jenkins server. + * + * @param context + * the context of this Builder/BuildStep. + * @return {@link JenkinsCrumb} a JenkinsCrumb. + * @throws IOException + * if the request failed. + */ + @Nonnull + private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) throws IOException { + String address = context.effectiveRemoteServer.getAddress(); + if (address == null) { + throw new AbortException( + "The remote server address can not be empty, or it must be overridden on the job configuration."); + } + URL crumbProviderUrl; + try { + String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); + crumbProviderUrl = new URL(address.concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); + HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); + int responseCode = connection.getResponseCode(); + if (responseCode == 401) { + throw new UnauthorizedException(crumbProviderUrl); + } else if (responseCode == 403) { + throw new ForbiddenException(crumbProviderUrl); + } else if (responseCode == 404) { + context.logger.println("CSRF protection is disabled on the remote server."); + return new JenkinsCrumb(); + } else if (responseCode == 200) { + context.logger.println("CSRF protection is enabled on the remote server."); + String response = readInputStream(connection); + String[] split = response.split(":"); + return new JenkinsCrumb(split[0], split[1]); + } else { + throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", + responseCode, connection.getResponseMessage())); + } + } catch (FileNotFoundException e) { + context.logger.println("CSRF protection is disabled on the remote server."); + return new JenkinsCrumb(); + } + } + + /** + * For POST requests a crumb is needed. This methods gets a crumb and sets it in + * the header. + * https://wiki.jenkins.io/display/JENKINS/Remote+access+API#RemoteaccessAPI-CSRFProtection + * + * @param connection + * @param context + * @throws IOException + */ + private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth) + throws IOException { + String method = connection.getRequestMethod(); + if (method != null && method.equalsIgnoreCase("POST")) { + JenkinsCrumb crumb = getCrumb(context, overrideAuth); + if (crumb.isEnabledOnRemote()) { + connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); + } + } + } + + private static HttpURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) + throws IOException { + URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) + : url.openConnection(); + + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); + + if (overrideAuth != null && !(overrideAuth instanceof NullAuth)) { + // Override Authorization Header if configured locally + overrideAuth.setAuthorizationHeader(connection, context); + } else if (serverAuth != null) { + // Set Authorization Header configured globally for remoteServer + serverAuth.setAuthorizationHeader(connection, context); + } + + return (HttpURLConnection) connection; + } + + private static String getUrlWithoutParameters(String url) { + String result = url; + try { + URI uri = new URI(url); + result = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), null, uri.getFragment()).toString(); + } catch (URISyntaxException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + return result; + } + + /** + * Build the proper URL to trigger the remote build + * + * All passed in string have already had their tokens replaced with real values. + * All 'params' also have the proper character encoding + * + * @param jobNameOrUrl + * Name of the remote job + * @param securityToken + * Security token used to trigger remote job + * @param params + * Parameters for the remote job + * @param isRemoteJobParameterized + * Is the remote job parameterized + * @param context + * The build context used in this plugin + * @return fully formed, fully qualified remote trigger URL + * @throws IOException + * throw when it can't pass data checking + */ + public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, + boolean isRemoteJobParameterized, BuildContext context) throws IOException { + + String triggerUrlString; + String query = ""; + + if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { + // start building the proper URL based on known capabiltiies of the remote + // server + if (context.effectiveRemoteServer.getAddress() == null) { + throw new AbortException( + "The remote server address can not be empty, or it must be overridden on the job configuration."); + } + triggerUrlString = context.effectiveRemoteServer.getAddress(); + triggerUrlString += buildTokenRootUrl; + triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, params); + query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); // TODO: does it work with full URL? + + } else { + triggerUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); + triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, params); + } + + // don't try to include a security token in the URL if none is provided + if (!securityToken.equals("")) { + query = addToQueryString(query, "token=" + encodeValue(securityToken)); + } + + // turn our Collection into a query string + String buildParams = buildUrlQueryString(params); + + if (!buildParams.isEmpty()) { + query = addToQueryString(query, buildParams); + } + + // by adding "delay=0", this will (theoretically) force this job to the top of + // the remote queue + query = addToQueryString(query, "delay=0"); + + triggerUrlString += "?" + query; + + return triggerUrlString; + } + + public static String getRawResp(String urlString, String requestType, BuildContext context, + Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth) + throws IOException, InterruptedException { + StringBuilder resp = new StringBuilder(); + sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, retryLimit, + overrideAuth, resp); + return resp.toString(); + } + + /** + * Same as sendHTTPCall, but keeps track of the number of failed connection + * attempts (aka: the number of times this method has been called). In the case + * of a failed connection, the method calls it self recursively and increments + * the number of attempts. + * + * @see sendHTTPCall + * @param urlString + * the URL that needs to be called. + * @param requestType + * the type of request (GET, POST, etc). + * @param context + * the context of this Builder/BuildStep. + * @param postParams + * parameters to post + * @param numberOfAttempts + * number of time that the connection has been attempted + * @param pollInterval + * interval between each retry + * @param retryLimit + * the retry uplimit + * @param overrideAuth + * auth used to overwrite the default auth + * @param rawRespRef + * the raw http response + * @return {@link ConnectionResponse} the response to the HTTP request. + * @throws IOException + * all the possibilities of HTTP exceptions + * @throws InterruptedException + * if any thread has interrupted the current thread. + * + */ + public static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, + Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, + StringBuilder rawRespRef) throws IOException, InterruptedException { + + JSONObject responseObject = null; + Map> responseHeader = null; + int responseCode = 0; + + byte[] postDataBytes = new byte[] {}; + String parmsString = ""; + if (HTTP_POST.equalsIgnoreCase(requestType) && postParams != null && postParams.size() > 0) { + parmsString = buildUrlQueryString(postParams); + postDataBytes = parmsString.getBytes("UTF-8"); + } + + URL url = new URL(urlString); + HttpURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); + + try { + conn.setDoInput(true); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Accept-Language", "UTF-8"); + conn.setRequestMethod(requestType); + addCrumbToConnection(conn, context, overrideAuth); + // wait up to 5 seconds for the connection to be open + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + if (HTTP_POST.equalsIgnoreCase(requestType)) { + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); + conn.setDoOutput(true); + conn.getOutputStream().write(postDataBytes); + } + + conn.connect(); + responseHeader = conn.getHeaderFields(); + responseCode = conn.getResponseCode(); + if (responseCode == 401) { + throw new UnauthorizedException(url); + } else if (responseCode == 403) { + throw new ForbiddenException(url); + } else if (responseCode == 404) { + throw new UrlNotFoundException(url); + } else { + String response = trimToNull(readInputStream(conn)); + if (rawRespRef != null) { + rawRespRef.append(response); + } + + // JSONSerializer serializer = new JSONSerializer(); + // need to parse the data we get back into struct + // listener.getLogger().println("Called URL: '" + urlString + "', got response: + // '" + response.toString() + "'"); + + // Solving issue reported in this comment: + // https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 + // Seems like in Jenkins version 1.547, when using "/build" (job API for + // non-parameterized jobs), it returns a string indicating the status. + // But in newer versions of Jenkins, it just returns an empty response. + // So we need to compensate and check for both. + if (responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { + return new ConnectionResponse(responseHeader, responseCode); + } else { + responseObject = (JSONObject) JSONSerializer.toJSON(response); + } + } + + } catch (IOException e) { + + // E.g. "HTTP/1.1 403 No valid crumb was included in the request" + List hints = responseHeader != null ? responseHeader.get(null) : null; + String hintsString = (hints != null && hints.size() > 0) ? " - " + hints.toString() : ""; + + // Shouldn't expose the token in console + logger.log(Level.WARNING, e.getMessage() + hintsString, e); + // If we have connectionRetryLimit set to > 0 then retry that many times. + if (numberOfAttempts <= retryLimit) { + context.logger.println(String.format( + "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s, parameters: %s", + (responseCode == 0 ? "" : "[" + responseCode + "]"), pollInterval, + getUrlWithoutParameters(urlString), parmsString)); + + // Sleep for 'pollInterval' seconds. + // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds + // (x 1000) + try { + // Could do with a better way of sleeping... + Thread.sleep(pollInterval * 1000); + } catch (InterruptedException ex) { + throw ex; + } + + context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); + numberOfAttempts++; + responseObject = sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, + pollInterval, retryLimit, overrideAuth, null).getBody(); + } else if (numberOfAttempts > retryLimit) { + // reached the maximum number of retries, time to fail + throw new ExceedRetryLimitException(); + } else { + // something failed with the connection and we retried the max amount of + // times... so throw an exception to mark the build as failed. + throw e; + } + + } finally { + // always make sure we close the connection + if (conn != null) { + conn.disconnect(); + } + } + return new ConnectionResponse(responseHeader, responseObject, responseCode); + } + + public static ConnectionResponse post(String urlString, BuildContext context, Collection params, + int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { + return sendHTTPCall(urlString, HTTP_POST, context, params, 1, pollInterval, retryLimit, overrideAuth, null); + } + + public static ConnectionResponse get(String urlString, BuildContext context, int pollInterval, int retryLimit, + Auth2 overrideAuth) throws IOException, InterruptedException { + return sendHTTPCall(urlString, HTTP_GET, context, null, 1, pollInterval, retryLimit, overrideAuth, null); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 2cdb7dd4..84a35029 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -2,9 +2,9 @@ import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -12,7 +12,9 @@ import java.io.IOException; import java.lang.reflect.Field; import java.net.MalformedURLException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; @@ -88,6 +90,13 @@ public void testRemoteBuildWithCrumb() throws Exception { } private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject) throws Exception { + Map parms = new HashMap<>(); + parms.put("parameterName1", "value1"); + parms.put("parameterName2", "value2"); + this._testRemoteBuild(authenticate, withParam, remoteProject, parms); + } + + private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject, Map parms) throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); @@ -106,7 +115,11 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle configuration.setPollInterval(1); configuration.setEnhancedLogging(true); if (withParam){ - configuration.setParameters("parameterName1=value1" + NL_UNIX + "parameterName2=value2"); + String parmString = ""; + for (Map.Entry p : parms.entrySet()) { + parmString += p.getKey() + "=" + p.getValue() + NL_UNIX; + } + configuration.setParameters(parmString); } if(authenticate) { TokenAuth tokenAuth = new TokenAuth(); @@ -131,8 +144,9 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle assertNotNull("lastBuild null", lastBuild); if (withParam){ EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); - assertEquals("value1", remoteEnv.get("parameterName1")); - assertEquals("value2", remoteEnv.get("parameterName2")); + for (Map.Entry p : parms.entrySet()) { + assertEquals(p.getValue(), remoteEnv.get(p.getKey())); + } } else { assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); } @@ -448,5 +462,18 @@ public void testRemoteFolderedBuildWithoutParameters() throws Exception { FreeStyleProject remoteProject = remoteJobFolder.createProject(FreeStyleProject.class, "someJobName1"); this._testRemoteBuild(false, false, remoteProject); } + + @Test + public void testRemoteBuildWith5KByteString() throws Exception { + enableAuth(); + FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); + remoteProject.addProperty( + new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), + new StringParameterDefinition("parameterName2", "default2"))); + Map parms = new HashMap<>(); + parms.put("parameterName1", TestConst.garbled5KString1); + parms.put("parameterName2", TestConst.garbled5KString2); + _testRemoteBuild(true, true, remoteProject, parms); + } } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/TestConst.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/TestConst.java new file mode 100644 index 00000000..71465b08 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/TestConst.java @@ -0,0 +1,81 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +public class TestConst { + public static final String garbled5KString1 = "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"; + public static final String garbled5KString2 = "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" + + "EUUSMAgWw"; + + +} From 517420267c0e918ddbbd9373d6ae5b3c322b741c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 18 Jul 2018 00:27:35 +0800 Subject: [PATCH 101/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 572a6b19..9383ce4c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.2-SNAPSHOT + 3.0.2 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.2 From 15fd8d25903346d060be5eced096ddba8fe5d88d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 18 Jul 2018 00:27:46 +0800 Subject: [PATCH 102/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9383ce4c..f3457164 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.2 + 3.0.3-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.2 + HEAD From 5f7df4f41fc074a38aa75b5218c680dc0091548d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 19 Jul 2018 20:50:47 +0800 Subject: [PATCH 103/262] add connection lock to prevent remote server from blocking --- .../RemoteBuildConfiguration.java | 57 ++++++++++++-- .../pipeline/RemoteBuildPipelineStep.java | 9 +++ .../utils/HttpHelper.java | 78 +++++++++++++++---- .../RemoteBuildConfiguration/config.jelly | 4 + .../help-maxConn.html | 5 ++ .../RemoteBuildPipelineStep/config.jelly | 4 + 6 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index ca8141dd..b8f6b76f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -15,7 +15,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; @@ -103,8 +107,10 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean enhancedLogging; private boolean loadParamsFromFile; private String parameterFile; + private int maxConn; + private Map hostLocks = new HashMap<>(); + private Map hostPermits = new HashMap<>(); - @SuppressWarnings("unused") private static Logger logger = Logger.getLogger(RemoteBuildConfiguration.class.getName()); @DataBoundConstructor @@ -130,6 +136,11 @@ protected Object readResolve() { return this; } + @DataBoundSetter + public void setMaxConn(int maxConn) { + this.maxConn = (maxConn > 5) ? 5 : maxConn; + } + @DataBoundSetter public void setRemoteJenkinsName(String remoteJenkinsName) { this.remoteJenkinsName = trimToNull(remoteJenkinsName); @@ -364,9 +375,35 @@ public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context + " remote host (remoteJenkinsName:'%s') nor 'Override remote host URL' (remoteJenkinsUrl:'%s').", expandedJob, this.remoteJenkinsName, this.remoteJenkinsUrl)); } + + try { + URL url = new URL(server.getAddress()); + Semaphore s = hostLocks.get(url.getHost()); + Integer lastPermit = hostPermits.get(url.getHost()); + int maxConn = getMaxConn(); + if (s == null || lastPermit == null || maxConn != lastPermit) { + s = new Semaphore(maxConn); + hostLocks.put(url.getHost(), s); + hostPermits.put(url.getHost(), maxConn); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to setup resource lock", e); + } + return server; } + private Semaphore getLock(String addr) { + Semaphore s = null; + try { + URL url = new URL(addr); + s = hostLocks.get(url.getHost()); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to setup resource lock", e); + } + return s; + } + /** * Lookup up the globally configured Remote Jenkins Server based on display name * @@ -544,12 +581,12 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti context.logger.println("Triggering remote job now."); try { - ConnectionResponse responseRemoteJob = HttpHelper.post(triggerUrlString, context, cleanedParams, - this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2()); + ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, + this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString)); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); buildInfo.setQueueId(queueItem.getId()); buildInfo = updateBuildInfo(buildInfo, context); - } catch (IOException|InterruptedException e) { + } catch (IOException | InterruptedException e) { this.failBuild(e, context.logger); } @@ -747,8 +784,8 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn private String getConsoleOutput(URL url, BuildContext context) throws IOException, InterruptedException { URL buildUrl = new URL(url, "consoleText"); - return HttpHelper.getRawResp(buildUrl.toString(), HttpHelper.HTTP_GET, context, null, 1, this.getPollInterval(), - this.getConnectionRetryLimit(), this.getAuth2()); + return HttpHelper.tryGetRawResp(buildUrl.toString(), context, this.getPollInterval(), + this.getConnectionRetryLimit(), this.getAuth2(), getLock(buildUrl.toString())); } /** @@ -766,8 +803,8 @@ private String getConsoleOutput(URL url, BuildContext context) throws IOExceptio * if any HTTP error occurred. */ public ConnectionResponse doGet(String urlString, BuildContext context) throws IOException, InterruptedException { - return HttpHelper.get(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), - this.getAuth2()); + return HttpHelper.tryGet(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), + this.getAuth2(), getLock(urlString)); } private void logAuthInformation(BuildContext context) throws IOException { @@ -825,6 +862,10 @@ private void logConfiguration(BuildContext context, List effectiveParams "################################################################################################################"); } + public int getMaxConn() { + return maxConn; + } + /** * @return the configured remote Jenkins name. That's the ID of a globally * configured remote host. diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index c358ea26..776ba0fa 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -72,6 +72,15 @@ public RemoteBuildPipelineStep(String job) { remoteBuildConfig.setShouldNotFailBuild(false); //We need to get notified. Failure feedback is collected async then. remoteBuildConfig.setBlockBuildUntilComplete(true); //default for Pipeline Step } + + @DataBoundSetter + public void setMaxConn(int maxConn) { + remoteBuildConfig.setMaxConn(maxConn); + } + + public int getMaxConn() { + return remoteBuildConfig.getMaxConn(); + } @DataBoundSetter public void setAuth(Auth2 auth) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index aab4266f..03f6f1aa 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -365,15 +367,6 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, return triggerUrlString; } - public static String getRawResp(String urlString, String requestType, BuildContext context, - Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth) - throws IOException, InterruptedException { - StringBuilder resp = new StringBuilder(); - sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, retryLimit, - overrideAuth, resp); - return resp.toString(); - } - /** * Same as sendHTTPCall, but keeps track of the number of failed connection * attempts (aka: the number of times this method has been called). In the case @@ -392,7 +385,7 @@ public static String getRawResp(String urlString, String requestType, BuildConte * @param numberOfAttempts * number of time that the connection has been attempted * @param pollInterval - * interval between each retry + * interval between each retry in second * @param retryLimit * the retry uplimit * @param overrideAuth @@ -406,7 +399,7 @@ public static String getRawResp(String urlString, String requestType, BuildConte * if any thread has interrupted the current thread. * */ - public static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, + private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef) throws IOException, InterruptedException { @@ -520,13 +513,72 @@ public static ConnectionResponse sendHTTPCall(String urlString, String requestTy return new ConnectionResponse(responseHeader, responseObject, responseCode); } + private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, + Collection params, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, + Semaphore lock) throws IOException, InterruptedException { + if (lock == null) { + context.logger.println("calling remote without locking..."); + return sendHTTPCall(urlString, method, context, null, 1, pollInterval, retryLimit, overrideAuth, + rawRespRef); + } + Boolean isAccquired = null; + try { + try { + isAccquired = lock.tryAcquire(pollInterval, TimeUnit.SECONDS); + logger.log(Level.FINE, String.format("calling %s in semaphore...", urlString)); + context.logger.println("calling remote in semaphore..."); + } catch (InterruptedException e) { + context.logger.println("fail to accquire lock because of interrupt, skip locking..."); + } + if (isAccquired != null && !isAccquired) { + context.logger.println("fail to accquire lock because of timeout, skip locking..."); + } + + ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, + overrideAuth, rawRespRef); + Thread.sleep(50); + return cr; + + } finally { + if (isAccquired != null && isAccquired) { + lock.release(); + } + } + } + + public static ConnectionResponse tryPost(String urlString, BuildContext context, Collection params, + int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) + throws IOException, InterruptedException { + + return tryCall(urlString, HTTP_POST, context, params, pollInterval, retryLimit, overrideAuth, null, lock); + } + + public static ConnectionResponse tryGet(String urlString, BuildContext context, int pollInterval, int retryLimit, + Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { + return tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, null, lock); + } + + public static String tryGetRawResp(String urlString, BuildContext context, int pollInterval, int retryLimit, + Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { + StringBuilder resp = new StringBuilder(); + tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, resp, lock); + return resp.toString(); + } + public static ConnectionResponse post(String urlString, BuildContext context, Collection params, int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { - return sendHTTPCall(urlString, HTTP_POST, context, params, 1, pollInterval, retryLimit, overrideAuth, null); + return tryPost(urlString, context, params, pollInterval, retryLimit, overrideAuth, null); } public static ConnectionResponse get(String urlString, BuildContext context, int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { - return sendHTTPCall(urlString, HTTP_GET, context, null, 1, pollInterval, retryLimit, overrideAuth, null); + return tryGet(urlString, context, pollInterval, retryLimit, overrideAuth, null); + } + + public static String getRawResp(String urlString, String requestType, BuildContext context, + Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth) + throws IOException, InterruptedException { + return tryGetRawResp(urlString, context, pollInterval, retryLimit, overrideAuth, null); } + } diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index a339c144..09c51a59 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -41,6 +41,10 @@ + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html new file mode 100644 index 00000000..305e4d97 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html @@ -0,0 +1,5 @@ +

+The max concurrent connections to the remote host, default is 1, max is 5. It's no use if you set it greater than 5. +Note: Set this field with caution, too many concurrent requests will not only failed your local jobs, but also block the remote server. + +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 5416fa32..c8ac5157 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -42,6 +42,10 @@ + + + + From 7c41a00827f1fb108df1d736b13a597c1302e8ca Mon Sep 17 00:00:00 2001 From: Chris White Date: Fri, 20 Jul 2018 07:51:51 -0700 Subject: [PATCH 104/262] Updated parameterized build detection (#44) Checks the property job metadata field in addition to the actions array Fixes #JENKINS-52673 --- .../RemoteBuildConfiguration.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b8f6b76f..f002ea47 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -999,6 +999,14 @@ private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IO } } } + + if (!isParameterized && remoteJobMetadata.getJSONArray("property").size() >= 1) { + for (Object obj : remoteJobMetadata.getJSONArray("property")) { + if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { + isParameterized = true; + } + } + } } else { throw new AbortException( "Could not identify if job is parameterized. Job metadata not accessible or with unexpected content."); From fb2ddd0e3b335226a9416fb63106e814bbf40808 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 20 Jul 2018 15:22:56 +0800 Subject: [PATCH 105/262] 1. enable crumb & job info cache to support more scalable and safe remote requests 2. fix the old recursion bug 3. request need to know job info to improve the request efficiency --- .../RemoteBuildConfiguration.java | 30 ++--- .../utils/DropCachePeriodicWork.java | 103 ++++++++++++++++++ .../utils/HttpHelper.java | 67 +++++++++--- 3 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index f002ea47..8ce31c30 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -37,6 +37,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfoExporterAction; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.DropCachePeriodicWork; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; @@ -376,8 +377,9 @@ public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context expandedJob, this.remoteJenkinsName, this.remoteJenkinsUrl)); } - try { - URL url = new URL(server.getAddress()); + String addr = server.getAddress(); + if (addr != null) { + URL url = new URL(addr); Semaphore s = hostLocks.get(url.getHost()); Integer lastPermit = hostPermits.get(url.getHost()); int maxConn = getMaxConn(); @@ -386,8 +388,6 @@ public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context hostLocks.put(url.getHost(), s); hostPermits.put(url.getHost(), maxConn); } - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to setup resource lock", e); } return server; @@ -546,7 +546,7 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task * @throws IOException * if there is an error triggering the remote job. * @throws InterruptedException - * if any thread has interrupted the current thread. + * if any thread has interrupted the current thread. * */ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOException, InterruptedException { @@ -602,9 +602,9 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti * @param handle * the handle to the remote execution. * @throws InterruptedException - * if any thread has interrupted the current thread. + * if any thread has interrupted the current thread. * @throws IOException - * if any HTTP error or business logic error + * if any HTTP error or business logic error */ public void performWaitForBuild(BuildContext context, Handle handle) throws IOException, InterruptedException { String jobName = handle.getJobName(); @@ -703,7 +703,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx * fails due to an unknown host, unauthorized credentials, or * another reason, or if there is an invalid queue response. * @throws InterruptedException - * if any thread has interrupted the current thread. + * if any thread has interrupted the current thread. */ @Nonnull private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) @@ -798,9 +798,9 @@ private String getConsoleOutput(URL url, BuildContext context) throws IOExceptio * the context of this Builder/BuildStep. * @return JSONObject a valid JSON object, or null. * @throws InterruptedException - * if any thread has interrupted the current thread. + * if any thread has interrupted the current thread. * @throws IOException - * if any HTTP error occurred. + * if any HTTP error occurred. */ public ConnectionResponse doGet(String urlString, BuildContext context) throws IOException, InterruptedException { return HttpHelper.tryGet(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), @@ -961,12 +961,16 @@ public int getConnectionRetryLimit() { throws IOException, InterruptedException { String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - remoteJobUrl += "/api/json"; + remoteJobUrl += "/api/json?tree=actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url"; + + JSONObject jsonObject = DropCachePeriodicWork.safeGetJobInfo(remoteJobUrl); + if (jsonObject != null) { + return jsonObject; + } ConnectionResponse response = doGet(remoteJobUrl, context); if (response.getResponseCode() < 400 && response.getBody() != null) { - - return response.getBody(); + return DropCachePeriodicWork.safePutJobInfo(remoteJobUrl, response.getBody()); } else if (response.getResponseCode() == 401 || response.getResponseCode() == 403) { throw new AbortException( diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java new file mode 100644 index 00000000..0bb942ee --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java @@ -0,0 +1,103 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.JenkinsCrumb; + +import hudson.Extension; +import hudson.model.PeriodicWork; +import net.sf.json.JSONObject; + +@Extension +public class DropCachePeriodicWork extends PeriodicWork { + + private static Map crumbMap = new HashMap<>(); + private static Map jobInfoMap = new HashMap<>(); + + private static Logger logger = Logger.getLogger(DropCachePeriodicWork.class.getName()); + private static Lock jobInfoLock = new ReentrantLock(); + private static Lock crumbLock = new ReentrantLock(); + + @Override + public long getRecurrencePeriod() { + return TimeUnit.MINUTES.toMillis(10); + } + + public static JenkinsCrumb safePutCrumb(String key, JenkinsCrumb jenkinsCrumb) { + try { + crumbLock.lock(); + crumbMap.put(key, jenkinsCrumb); + return jenkinsCrumb; + } finally { + crumbLock.unlock(); + } + } + + public static JenkinsCrumb safeGetCrumb(String key) { + try { + crumbLock.lock(); + if (crumbMap.containsKey(key)) { + return crumbMap.get(key); + } else { + return null; + } + } finally { + crumbLock.unlock(); + } + } + + public static JSONObject safePutJobInfo(String key, JSONObject jobInfo) { + try { + jobInfoLock.lock(); + jobInfoMap.put(key, jobInfo); + return jobInfo; + } finally { + jobInfoLock.unlock(); + } + } + + public static JSONObject safeGetJobInfo(String key) { + try { + jobInfoLock.lock(); + if (jobInfoMap.containsKey(key)) { + return jobInfoMap.get(key); + } else { + return null; + } + } finally { + jobInfoLock.unlock(); + } + } + + @Override + protected void doRun() throws Exception { + logger.log(Level.INFO, "begin schedule clean..."); + + try { + crumbLock.lock(); + crumbMap.clear(); + } catch (Exception e) { + logger.log(Level.WARNING, "Fail to clear crumb cache", e); + } finally { + crumbLock.unlock(); + } + + try { + jobInfoLock.lock(); + jobInfoMap.clear(); + } catch (Exception e) { + logger.log(Level.WARNING, "Fail to clear job info cache", e); + } finally { + jobInfoLock.unlock(); + } + + logger.log(Level.INFO, "end schedule clean..."); + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 03f6f1aa..51d316e5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -18,8 +18,12 @@ import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; @@ -167,7 +171,7 @@ protected static String generateJobUrl(RemoteJenkinsServer remoteServer, String * Helper function for character encoding * * @param dirtyValue - * something that wasn't encoded in UTF-8 + * something that wasn't encoded in UTF-8 * @return encoded value */ public static String encodeValue(String dirtyValue) { @@ -176,7 +180,6 @@ public static String encodeValue(String dirtyValue) { try { cleanValue = URLEncoder.encode(dirtyValue, "UTF-8").replace("+", "%20"); } catch (UnsupportedEncodingException e) { - // TODO Auto-generated catch block e.printStackTrace(); } @@ -226,10 +229,21 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) t throw new AbortException( "The remote server address can not be empty, or it must be overridden on the job configuration."); } + URL crumbProviderUrl; + String globalHost = ""; try { String xpathValue = URLEncoder.encode("concat(//crumbRequestField,\":\",//crumb)", "UTF-8"); crumbProviderUrl = new URL(address.concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); + globalHost = crumbProviderUrl.getHost(); + + + JenkinsCrumb jenkinsCrumb = DropCachePeriodicWork.safeGetCrumb(globalHost); + if (jenkinsCrumb != null) { + context.logger.println("reuse cached crumb: " + globalHost); + return jenkinsCrumb; + } + HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { @@ -238,19 +252,19 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) t throw new ForbiddenException(crumbProviderUrl); } else if (responseCode == 404) { context.logger.println("CSRF protection is disabled on the remote server."); - return new JenkinsCrumb(); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb()); } else if (responseCode == 200) { context.logger.println("CSRF protection is enabled on the remote server."); String response = readInputStream(connection); String[] split = response.split(":"); - return new JenkinsCrumb(split[0], split[1]); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb(split[0], split[1])); } else { throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", responseCode, connection.getResponseMessage())); } } catch (FileNotFoundException e) { context.logger.println("CSRF protection is disabled on the remote server."); - return new JenkinsCrumb(); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb()); } } @@ -318,10 +332,10 @@ private static String getUrlWithoutParameters(String url) { * @param isRemoteJobParameterized * Is the remote job parameterized * @param context - * The build context used in this plugin + * The build context used in this plugin * @return fully formed, fully qualified remote trigger URL * @throws IOException - * throw when it can't pass data checking + * throw when it can't pass data checking */ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, boolean isRemoteJobParameterized, BuildContext context) throws IOException { @@ -367,6 +381,11 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, return triggerUrlString; } + static { + java.net.CookieManager cm = new java.net.CookieManager(); + java.net.CookieHandler.setDefault(cm); + } + /** * Same as sendHTTPCall, but keeps track of the number of failed connection * attempts (aka: the number of times this method has been called). In the case @@ -391,13 +410,13 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * @param overrideAuth * auth used to overwrite the default auth * @param rawRespRef - * the raw http response + * the raw http response * @return {@link ConnectionResponse} the response to the HTTP request. * @throws IOException - * all the possibilities of HTTP exceptions + * all the possibilities of HTTP exceptions * @throws InterruptedException - * if any thread has interrupted the current thread. - * + * if any thread has interrupted the current thread. + * */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, @@ -425,7 +444,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT addCrumbToConnection(conn, context, overrideAuth); // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); + conn.setReadTimeout(10000); if (HTTP_POST.equalsIgnoreCase(requestType)) { conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); @@ -433,9 +452,20 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT conn.getOutputStream().write(postDataBytes); } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + logger.finer(String.format("%s begin: %s", urlString, sdf.format(new Date()))); + Instant before = Instant.now(); + conn.connect(); + + Instant after = Instant.now(); + logger.finer( + String.format("%s end: elapsed [%s] ms", urlString, Duration.between(before, after).toMillis())); + responseHeader = conn.getHeaderFields(); responseCode = conn.getResponseCode(); + if (responseCode == 401) { throw new UnauthorizedException(url); } else if (responseCode == 403) { @@ -471,7 +501,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // E.g. "HTTP/1.1 403 No valid crumb was included in the request" List hints = responseHeader != null ? responseHeader.get(null) : null; String hintsString = (hints != null && hints.size() > 0) ? " - " + hints.toString() : ""; - + // Shouldn't expose the token in console logger.log(Level.WARNING, e.getMessage() + hintsString, e); // If we have connectionRetryLimit set to > 0 then retry that many times. @@ -493,8 +523,9 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); numberOfAttempts++; - responseObject = sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, - pollInterval, retryLimit, overrideAuth, null).getBody(); + return sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, + retryLimit, overrideAuth, rawRespRef); + } else if (numberOfAttempts > retryLimit) { // reached the maximum number of retries, time to fail throw new ExceedRetryLimitException(); @@ -526,17 +557,19 @@ private static ConnectionResponse tryCall(String urlString, String method, Build try { isAccquired = lock.tryAcquire(pollInterval, TimeUnit.SECONDS); logger.log(Level.FINE, String.format("calling %s in semaphore...", urlString)); - context.logger.println("calling remote in semaphore..."); + + // if we can't lock, just let it go. } catch (InterruptedException e) { + logger.log(Level.WARNING, "fail to accquire lock because of interrupt, skip locking...", e); context.logger.println("fail to accquire lock because of interrupt, skip locking..."); } if (isAccquired != null && !isAccquired) { + logger.warning("fail to accquire lock because of timeout, skip locking..."); context.logger.println("fail to accquire lock because of timeout, skip locking..."); } ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, overrideAuth, rawRespRef); - Thread.sleep(50); return cr; } finally { From febbcd376cd3ceeb7a442550012ef0f30a17ad8d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 20 Jul 2018 23:55:50 +0800 Subject: [PATCH 106/262] update pom for jdk version & compatible configuration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f3457164..4a0b7981 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ 1.642.3 - 7 + 8 Parameterized-Remote-Trigger @@ -43,7 +43,7 @@ maven-hpi-plugin - 3.0.1-SNAPSHOT + 3.0.3-SNAPSHOT From a89a4279054ab42113a4f3a408e0960d2eeff111 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 00:55:03 +0800 Subject: [PATCH 107/262] add switch for cache --- .../RemoteBuildConfiguration.java | 29 +++++++++-- .../utils/DropCachePeriodicWork.java | 16 +++++-- .../utils/HttpHelper.java | 48 +++++++++---------- .../RemoteBuildConfiguration/config.jelly | 7 ++- .../help-useCrumbCache.html | 5 ++ .../help-useJobInfoCache.html | 5 ++ .../RemoteBuildPipelineStep/config.jelly | 7 ++- .../RemoteBuildPipelineStep/help-maxConn.html | 5 ++ .../help-useCrumbCache.html | 5 ++ .../help-useJobInfoCache.html | 5 ++ .../RemoteBuildConfigurationTest.java | 4 +- 11 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 8ce31c30..9a325122 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -109,6 +109,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean loadParamsFromFile; private String parameterFile; private int maxConn; + private boolean useCrumbCache; + private boolean useJobInfoCache; private Map hostLocks = new HashMap<>(); private Map hostPermits = new HashMap<>(); @@ -582,7 +584,8 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti try { ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, - this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString)); + this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), + isUseCrumbCache()); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); buildInfo.setQueueId(queueItem.getId()); buildInfo = updateBuildInfo(buildInfo, context); @@ -963,14 +966,14 @@ public int getConnectionRetryLimit() { String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); remoteJobUrl += "/api/json?tree=actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url"; - JSONObject jsonObject = DropCachePeriodicWork.safeGetJobInfo(remoteJobUrl); + JSONObject jsonObject = DropCachePeriodicWork.safeGetJobInfo(remoteJobUrl, isUseJobInfoCache()); if (jsonObject != null) { return jsonObject; } ConnectionResponse response = doGet(remoteJobUrl, context); if (response.getResponseCode() < 400 && response.getBody() != null) { - return DropCachePeriodicWork.safePutJobInfo(remoteJobUrl, response.getBody()); + return DropCachePeriodicWork.safePutJobInfo(remoteJobUrl, response.getBody(), isUseJobInfoCache()); } else if (response.getResponseCode() == 401 || response.getResponseCode() == 403) { throw new AbortException( @@ -1006,7 +1009,7 @@ private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IO if (!isParameterized && remoteJobMetadata.getJSONArray("property").size() >= 1) { for (Object obj : remoteJobMetadata.getJSONArray("property")) { - if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { + if (obj instanceof JSONObject && ((JSONObject) obj).get("parameterDefinitions") != null) { isParameterized = true; } } @@ -1051,6 +1054,24 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } + public boolean isUseCrumbCache() { + return useCrumbCache; + } + + @DataBoundSetter + public void setUseCrumbCache(boolean useCrumbCache) { + this.useCrumbCache = useCrumbCache; + } + + public boolean isUseJobInfoCache() { + return useJobInfoCache; + } + + @DataBoundSetter + public void setUseJobInfoCache(boolean useJobInfoCache) { + this.useJobInfoCache = useJobInfoCache; + } + // This indicates to Jenkins that this is an implementation of an extension // point. @Extension diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java index 0bb942ee..a976ce9c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java @@ -29,7 +29,9 @@ public long getRecurrencePeriod() { return TimeUnit.MINUTES.toMillis(10); } - public static JenkinsCrumb safePutCrumb(String key, JenkinsCrumb jenkinsCrumb) { + public static JenkinsCrumb safePutCrumb(String key, JenkinsCrumb jenkinsCrumb, boolean isCacheEnable) { + if (!isCacheEnable) + return jenkinsCrumb; try { crumbLock.lock(); crumbMap.put(key, jenkinsCrumb); @@ -39,7 +41,9 @@ public static JenkinsCrumb safePutCrumb(String key, JenkinsCrumb jenkinsCrumb) { } } - public static JenkinsCrumb safeGetCrumb(String key) { + public static JenkinsCrumb safeGetCrumb(String key, boolean isCacheEnable) { + if (!isCacheEnable) + return null; try { crumbLock.lock(); if (crumbMap.containsKey(key)) { @@ -52,7 +56,9 @@ public static JenkinsCrumb safeGetCrumb(String key) { } } - public static JSONObject safePutJobInfo(String key, JSONObject jobInfo) { + public static JSONObject safePutJobInfo(String key, JSONObject jobInfo, boolean isCacheEnable) { + if (!isCacheEnable) + return jobInfo; try { jobInfoLock.lock(); jobInfoMap.put(key, jobInfo); @@ -62,7 +68,9 @@ public static JSONObject safePutJobInfo(String key, JSONObject jobInfo) { } } - public static JSONObject safeGetJobInfo(String key) { + public static JSONObject safeGetJobInfo(String key, boolean isCacheEnable) { + if (!isCacheEnable) + return null; try { jobInfoLock.lock(); if (jobInfoMap.containsKey(key)) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 51d316e5..a9fd9426 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -223,7 +223,8 @@ private static String readInputStream(HttpURLConnection connection) throws IOExc * if the request failed. */ @Nonnull - private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) throws IOException { + private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) + throws IOException { String address = context.effectiveRemoteServer.getAddress(); if (address == null) { throw new AbortException( @@ -237,13 +238,11 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) t crumbProviderUrl = new URL(address.concat("/crumbIssuer/api/xml?xpath=").concat(xpathValue)); globalHost = crumbProviderUrl.getHost(); - - JenkinsCrumb jenkinsCrumb = DropCachePeriodicWork.safeGetCrumb(globalHost); + JenkinsCrumb jenkinsCrumb = DropCachePeriodicWork.safeGetCrumb(globalHost, isCacheEnabled); if (jenkinsCrumb != null) { context.logger.println("reuse cached crumb: " + globalHost); return jenkinsCrumb; } - HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { @@ -252,19 +251,20 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) t throw new ForbiddenException(crumbProviderUrl); } else if (responseCode == 404) { context.logger.println("CSRF protection is disabled on the remote server."); - return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb()); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb(), isCacheEnabled); } else if (responseCode == 200) { context.logger.println("CSRF protection is enabled on the remote server."); String response = readInputStream(connection); String[] split = response.split(":"); - return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb(split[0], split[1])); + JenkinsCrumb crumb = new JenkinsCrumb(split[0], split[1]); + return DropCachePeriodicWork.safePutCrumb(globalHost, crumb, isCacheEnabled); } else { throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", responseCode, connection.getResponseMessage())); } } catch (FileNotFoundException e) { context.logger.println("CSRF protection is disabled on the remote server."); - return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb()); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb(), isCacheEnabled); } } @@ -277,11 +277,11 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth) t * @param context * @throws IOException */ - private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth) - throws IOException { + private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth, + boolean isCacheEnabled) throws IOException { String method = connection.getRequestMethod(); if (method != null && method.equalsIgnoreCase("POST")) { - JenkinsCrumb crumb = getCrumb(context, overrideAuth); + JenkinsCrumb crumb = getCrumb(context, overrideAuth, isCacheEnabled); if (crumb.isEnabledOnRemote()) { connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); } @@ -420,7 +420,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, - StringBuilder rawRespRef) throws IOException, InterruptedException { + StringBuilder rawRespRef, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { JSONObject responseObject = null; Map> responseHeader = null; @@ -441,7 +441,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept-Language", "UTF-8"); conn.setRequestMethod(requestType); - addCrumbToConnection(conn, context, overrideAuth); + addCrumbToConnection(conn, context, overrideAuth, isCrubmCacheEnabled); // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); conn.setReadTimeout(10000); @@ -524,7 +524,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); numberOfAttempts++; return sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, - retryLimit, overrideAuth, rawRespRef); + retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); } else if (numberOfAttempts > retryLimit) { // reached the maximum number of retries, time to fail @@ -546,18 +546,18 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, Collection params, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, - Semaphore lock) throws IOException, InterruptedException { + Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { if (lock == null) { context.logger.println("calling remote without locking..."); - return sendHTTPCall(urlString, method, context, null, 1, pollInterval, retryLimit, overrideAuth, - rawRespRef); + return sendHTTPCall(urlString, method, context, null, 1, pollInterval, retryLimit, overrideAuth, rawRespRef, + isCrubmCacheEnabled); } Boolean isAccquired = null; try { try { isAccquired = lock.tryAcquire(pollInterval, TimeUnit.SECONDS); logger.log(Level.FINE, String.format("calling %s in semaphore...", urlString)); - + // if we can't lock, just let it go. } catch (InterruptedException e) { logger.log(Level.WARNING, "fail to accquire lock because of interrupt, skip locking...", e); @@ -569,7 +569,7 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, - overrideAuth, rawRespRef); + overrideAuth, rawRespRef, isCrubmCacheEnabled); return cr; } finally { @@ -580,27 +580,27 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } public static ConnectionResponse tryPost(String urlString, BuildContext context, Collection params, - int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) + int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { - return tryCall(urlString, HTTP_POST, context, params, pollInterval, retryLimit, overrideAuth, null, lock); + return tryCall(urlString, HTTP_POST, context, params, pollInterval, retryLimit, overrideAuth, null, lock,isCrubmCacheEnabled); } public static ConnectionResponse tryGet(String urlString, BuildContext context, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { - return tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, null, lock); + return tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, null, lock, false); } public static String tryGetRawResp(String urlString, BuildContext context, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { StringBuilder resp = new StringBuilder(); - tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, resp, lock); + tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, resp, lock, false); return resp.toString(); } public static ConnectionResponse post(String urlString, BuildContext context, Collection params, - int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { - return tryPost(urlString, context, params, pollInterval, retryLimit, overrideAuth, null); + int pollInterval, int retryLimit, Auth2 overrideAuth, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { + return tryPost(urlString, context, params, pollInterval, retryLimit, overrideAuth, null, isCrubmCacheEnabled); } public static ConnectionResponse get(String urlString, BuildContext context, int pollInterval, int retryLimit, diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index 09c51a59..f366cad2 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -49,7 +49,12 @@ - + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html new file mode 100644 index 00000000..ed08beab --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html @@ -0,0 +1,5 @@ +
+Set this field to enable cache of the crumb of remote server.
+It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
+This cache will be updated every 10 minutes. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html new file mode 100644 index 00000000..9269382f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html @@ -0,0 +1,5 @@ +
+Set this field to enable cache of the job info of remote server.
+It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
+This cache will be updated every 10 minutes. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index c8ac5157..2b033d74 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -50,7 +50,12 @@ - + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html new file mode 100644 index 00000000..305e4d97 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html @@ -0,0 +1,5 @@ +
+The max concurrent connections to the remote host, default is 1, max is 5. It's no use if you set it greater than 5. +Note: Set this field with caution, too many concurrent requests will not only failed your local jobs, but also block the remote server. + +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html new file mode 100644 index 00000000..ed08beab --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html @@ -0,0 +1,5 @@ +
+Set this field to enable cache of the crumb of remote server.
+It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
+This cache will be updated every 10 minutes. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html new file mode 100644 index 00000000..9269382f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html @@ -0,0 +1,5 @@ +
+Set this field to enable cache of the job info of remote server.
+It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
+This cache will be updated every 10 minutes. +
diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 84a35029..1ce2bd75 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -112,7 +112,9 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); configuration.setPreventRemoteBuildQueue(false); configuration.setBlockBuildUntilComplete(true); - configuration.setPollInterval(1); + configuration.setPollInterval(5); + configuration.setUseCrumbCache(false); + configuration.setUseJobInfoCache(false); configuration.setEnhancedLogging(true); if (withParam){ String parmString = ""; From d9f2ead31ffe437b757a8005475626d6f8b21e02 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 09:40:19 +0800 Subject: [PATCH 108/262] refine the wording in help file --- .../RemoteBuildConfiguration/help-useCrumbCache.html | 2 +- .../RemoteBuildConfiguration/help-useJobInfoCache.html | 2 +- .../pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html | 2 +- .../pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html index ed08beab..02b5df87 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html @@ -1,5 +1,5 @@
Set this field to enable cache of the crumb of remote server.
It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
-This cache will be updated every 10 minutes. +This cache will be cleared every 10 minutes.
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html index 9269382f..c3e0cf17 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useJobInfoCache.html @@ -1,5 +1,5 @@
Set this field to enable cache of the job info of remote server.
It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
-This cache will be updated every 10 minutes. +This cache will be cleared every 10 minutes.
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html index ed08beab..02b5df87 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useCrumbCache.html @@ -1,5 +1,5 @@
Set this field to enable cache of the crumb of remote server.
It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
-This cache will be updated every 10 minutes. +This cache will be cleared every 10 minutes.
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html index 9269382f..c3e0cf17 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-useJobInfoCache.html @@ -1,5 +1,5 @@
Set this field to enable cache of the job info of remote server.
It'll be more efficient for the local job execution & more stable for remote server when massive concurrent jobs are triggered.
-This cache will be updated every 10 minutes. +This cache will be cleared every 10 minutes.
From 4c577be42e8a2b280547afb8624d81f2a866a413 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 21:21:19 +0800 Subject: [PATCH 109/262] update change log --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb876b7..7869e0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +# 3.0.3 (Jul 18th, 2018) +### New feature + +* None + +### Improvement + +* Add concurrent connection restriction to prevent remote servers from blocking +* Add job info. & crumb cache to reduce the dummy inquiries when parallel triggering + +### Bug fixes + +* [JENKINS-52673](https://issues.jenkins-ci.org/browse/JENKINS-52673) + +### Important change + +* jdk version must be at least v1.8 + + +# 3.0.2 (Jul 18th, 2018) +### New feature + +* None + +### Improvement + +* HTTP utility reorganized + * post with form-data + +### Bug fixes + +* Fix parameters are too long (HTTP status 414) + + +# 3.0.1 (Jul 9th, 2018) +### New feature +* Support triggering remote jobs via Jenkins proxy + +### Improvement +- code refinement + +### Bug fixes +- [JENKINS-47919 ](https://issues.jenkins-ci.org/browse/JENKINS-47919) (clarified & fixed) + + # 3.0.0 (May 17th, 2018) ### New feature * Pipeline support From 94b20a97963bb9485eb24e85fa85b98832d4453b Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 22:07:50 +0800 Subject: [PATCH 110/262] enable cache by default --- .../RemoteBuildConfiguration/config.jelly | 4 ++-- .../pipeline/RemoteBuildPipelineStep/config.jelly | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index f366cad2..3e8e3350 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -50,10 +50,10 @@
- + - + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 2b033d74..50ef099e 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -51,10 +51,10 @@ - + - + From 15df19d962933fec18bbd32afdac136bd3d4a893 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 22:13:22 +0800 Subject: [PATCH 111/262] fix typo in change log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7869e0e3..5b7b7a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 3.0.3 (Jul 18th, 2018) +# 3.0.3 (Jul 23th, 2018) ### New feature * None From eb0e63fef382bd0d403b8b6f3e70a97b767513f1 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 22:13:43 +0800 Subject: [PATCH 112/262] reset the test polling interval to 1 --- .../RemoteBuildConfigurationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 1ce2bd75..a0d1cc02 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -112,7 +112,7 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); configuration.setPreventRemoteBuildQueue(false); configuration.setBlockBuildUntilComplete(true); - configuration.setPollInterval(5); + configuration.setPollInterval(1); configuration.setUseCrumbCache(false); configuration.setUseJobInfoCache(false); configuration.setEnhancedLogging(true); From 0aa14e7a99c4b87eba4ab846ab1639337cc057ac Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 22:21:04 +0800 Subject: [PATCH 113/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4a0b7981..2bb33974 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.3-SNAPSHOT + 3.0.3 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.3 From 050f4b077edf909f49dbf823e5cb86d78e3d3a94 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 23 Jul 2018 22:21:15 +0800 Subject: [PATCH 114/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2bb33974..5f7c7680 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.3 + 3.0.4-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.3 + HEAD From 5fd83a115d6efe022f994d208b5a85edfbf096f5 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 24 Jul 2018 14:26:11 +0800 Subject: [PATCH 115/262] refine the wording --- .../pipeline/RemoteBuildPipelineStep/help-maxConn.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html index 305e4d97..f0e85cb1 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html @@ -1,5 +1,6 @@
-The max concurrent connections to the remote host, default is 1, max is 5. It's no use if you set it greater than 5. -Note: Set this field with caution, too many concurrent requests will not only failed your local jobs, but also block the remote server. +The max concurrent connections to the remote host, default is 1, max is 5. It'll be 5 even if you set it greater than 5. +Note: Set this field with caution, too many concurrent requests will not only fail your local jobs,
+ but also block the remote server.
From ce67d855a846753d45898c19cdf1322554d6ed5b Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 24 Jul 2018 14:27:22 +0800 Subject: [PATCH 116/262] refine the wording in max connection help page --- .../RemoteBuildConfiguration/help-maxConn.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html index 305e4d97..2d9f9592 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html @@ -1,5 +1,6 @@
-The max concurrent connections to the remote host, default is 1, max is 5. It's no use if you set it greater than 5. -Note: Set this field with caution, too many concurrent requests will not only failed your local jobs, but also block the remote server. +The max concurrent connections to the remote host, default is 1, max is 5. It'll be 5 even if you set it greater than 5. +Note: Set this field with caution, too many concurrent requests will not only fail your local jobs,
+ but also block the remote server.
From 29bb63f37ad119c801296b067e786b96d30f7712 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 24 Jul 2018 15:29:40 +0800 Subject: [PATCH 117/262] fix that pipeline cache didn't work --- .../pipeline/RemoteBuildPipelineStep.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 776ba0fa..d557c8d3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -146,6 +146,16 @@ public void setParameterFile(String parameterFile) { remoteBuildConfig.setParameterFile(parameterFile); } + @DataBoundSetter + public void setUseJobInfoCache(boolean useJobInfoCache) { + remoteBuildConfig.setUseJobInfoCache(useJobInfoCache); + } + + @DataBoundSetter + public void setUseCrumbCache(boolean useCrumbCache) { + remoteBuildConfig.setUseCrumbCache(useCrumbCache); + } + @Override public StepExecution start(StepContext context) throws Exception { return new Execution(context, remoteBuildConfig); @@ -293,4 +303,12 @@ public String getParameterFile() { public int getConnectionRetryLimit() { return remoteBuildConfig.getConnectionRetryLimit(); } + + public boolean isUseCrumbCache() { + return remoteBuildConfig.isUseCrumbCache(); + } + + public boolean isUseJobInfoCache() { + return remoteBuildConfig.isUseJobInfoCache(); + } } From e36b13a382cdff6f59047930930751a09dc3698d Mon Sep 17 00:00:00 2001 From: Benoit Beaudin <31477008+wincrasher@users.noreply.github.com> Date: Fri, 27 Jul 2018 15:04:20 -0400 Subject: [PATCH 118/262] Update Handle.java (#45) Comportement change from version 3.0.2 to 3.0.3 in commit ffab7282197147fd3cd5f9fc32410fdc6f0a4878 Return the ConnectionResponse instead of a JSONObject --- .../plugins/ParameterizedRemoteTrigger/pipeline/Handle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 6f9cb37a..285653b0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -358,7 +358,7 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, PrintStreamWrapper log = new PrintStreamWrapper(); try { BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); - return remoteBuildConfiguration.doGet(fileUrl.toString(), context); + return remoteBuildConfiguration.doGet(fileUrl.toString(), context).getBody(); } finally { lastLog = log.getContent(); } From 15cfd8382190fe6fa9f677eaddc1dfea5d3fa12b Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:16:34 +0800 Subject: [PATCH 119/262] Support remote job abortion --- .../RemoteBuildConfiguration.java | 50 ++++++++++++++++--- .../pipeline/RemoteBuildPipelineStep.java | 43 ++++++++++------ .../utils/RestUtils.java | 50 +++++++++++++++++++ .../RemoteBuildConfiguration/config.jelly | 6 ++- .../RemoteBuildPipelineStep/config.jelly | 6 ++- 5 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 9a325122..c9cce783 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -42,6 +42,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.HttpHelper; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.RestUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -111,6 +112,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private int maxConn; private boolean useCrumbCache; private boolean useJobInfoCache; + private boolean abortTriggeredJob; + private Map hostLocks = new HashMap<>(); private Map hostPermits = new HashMap<>(); @@ -138,6 +141,11 @@ protected Object readResolve() { auth = null; return this; } + + @DataBoundSetter + public void setAbortTriggeredJob(boolean abortTriggeredJob) { + this.abortTriggeredJob = abortTriggeredJob; + } @DataBoundSetter public void setMaxConn(int maxConn) { @@ -395,7 +403,7 @@ public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context return server; } - private Semaphore getLock(String addr) { + public Semaphore getLock(String addr) { Semaphore s = null; try { URL url = new URL(addr); @@ -504,6 +512,22 @@ protected void failBuild(Exception e, PrintStream logger) throws IOException { throw new AbortException(e.getClass().getSimpleName() + ": " + e.getMessage()); } } + + public void abortRemoteTask(RemoteJenkinsServer remoteServer, Handle handle, BuildContext context) + throws IOException, InterruptedException { + if (isAbortTriggeredJob() && context != null && handle != null && !handle.isFinished()) { + try { + if (handle.isQueued()) { + RestUtils.cancelQueueItem(remoteServer.getAddress(), handle, context, this); + } else { + RestUtils.stopRemoteJob(handle, context, this); + } + } catch (IOException ex) { + context.logger.println("Fail to abort remote job: " + ex.getMessage()); + logger.log(Level.WARNING, "Fail to abort remote job", ex); + } + } + } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) @@ -530,12 +554,20 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen @Override public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { - RemoteJenkinsServer effectiveRemoteServer = evaluateEffectiveRemoteHost( - new BasicBuildContext(build, workspace, listener)); - BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), - effectiveRemoteServer); - Handle handle = performTriggerAndGetQueueId(context); - performWaitForBuild(context, handle); + Handle handle = null; + BuildContext context = null; + RemoteJenkinsServer effectiveRemoteServer = null; + try { + effectiveRemoteServer = evaluateEffectiveRemoteHost( + new BasicBuildContext(build, workspace, listener)); + context = new BuildContext(build, workspace, listener, listener.getLogger(), + effectiveRemoteServer); + handle = performTriggerAndGetQueueId(context); + performWaitForBuild(context, handle); + } catch(InterruptedException e) { + this.abortRemoteTask(effectiveRemoteServer, handle, context); + throw e; + } } /** @@ -864,6 +896,10 @@ private void logConfiguration(BuildContext context, List effectiveParams context.logger.println( "################################################################################################################"); } + + public boolean isAbortTriggeredJob() { + return abortTriggeredJob; + } public int getMaxConn() { return maxConn; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index d557c8d3..a7c399a8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -64,7 +64,6 @@ public class RemoteBuildPipelineStep extends Step { private RemoteBuildConfiguration remoteBuildConfig; - @DataBoundConstructor public RemoteBuildPipelineStep(String job) { remoteBuildConfig = new RemoteBuildConfiguration(); @@ -73,24 +72,20 @@ public RemoteBuildPipelineStep(String job) { remoteBuildConfig.setBlockBuildUntilComplete(true); //default for Pipeline Step } + @DataBoundSetter + public void setAbortTriggeredJob(boolean abortTriggeredJob) { + remoteBuildConfig.setAbortTriggeredJob(abortTriggeredJob); + } + @DataBoundSetter public void setMaxConn(int maxConn) { remoteBuildConfig.setMaxConn(maxConn); } - - public int getMaxConn() { - return remoteBuildConfig.getMaxConn(); - } - @DataBoundSetter public void setAuth(Auth2 auth) { remoteBuildConfig.setAuth2(auth); } - public Auth2 getAuth() { - return remoteBuildConfig.getAuth2(); - } - @DataBoundSetter public void setRemoteJenkinsName(String remoteJenkinsName) { remoteBuildConfig.setRemoteJenkinsName(remoteJenkinsName); @@ -244,10 +239,17 @@ public static class Execution extends SynchronousNonBlockingStepExecution
- + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 50ef099e..d7d19fed 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -18,7 +18,11 @@ - + + + + + From 4bd783b909d5566c6c110bbc9de9e6ce7298e81d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:19:36 +0800 Subject: [PATCH 120/262] format the inconsistent indent in pipeline/RemoteBuildPipelineStep.java --- .../pipeline/RemoteBuildPipelineStep.java | 503 +++++++++--------- 1 file changed, 258 insertions(+), 245 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index a7c399a8..69fc33dc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -62,84 +62,86 @@ public class RemoteBuildPipelineStep extends Step { - private RemoteBuildConfiguration remoteBuildConfig; - - @DataBoundConstructor - public RemoteBuildPipelineStep(String job) { - remoteBuildConfig = new RemoteBuildConfiguration(); - remoteBuildConfig.setJob(job); - remoteBuildConfig.setShouldNotFailBuild(false); //We need to get notified. Failure feedback is collected async then. - remoteBuildConfig.setBlockBuildUntilComplete(true); //default for Pipeline Step - } - + private RemoteBuildConfiguration remoteBuildConfig; + + @DataBoundConstructor + public RemoteBuildPipelineStep(String job) { + remoteBuildConfig = new RemoteBuildConfiguration(); + remoteBuildConfig.setJob(job); + remoteBuildConfig.setShouldNotFailBuild(false); // We need to get notified. Failure feedback is collected async + // then. + remoteBuildConfig.setBlockBuildUntilComplete(true); // default for Pipeline Step + } + @DataBoundSetter public void setAbortTriggeredJob(boolean abortTriggeredJob) { remoteBuildConfig.setAbortTriggeredJob(abortTriggeredJob); } - - @DataBoundSetter - public void setMaxConn(int maxConn) { - remoteBuildConfig.setMaxConn(maxConn); - } - @DataBoundSetter - public void setAuth(Auth2 auth) { - remoteBuildConfig.setAuth2(auth); - } - - @DataBoundSetter - public void setRemoteJenkinsName(String remoteJenkinsName) { - remoteBuildConfig.setRemoteJenkinsName(remoteJenkinsName); - } - - @DataBoundSetter - public void setRemoteJenkinsUrl(String remoteJenkinsUrl) { - remoteBuildConfig.setRemoteJenkinsUrl(remoteJenkinsUrl); - } - - @DataBoundSetter - public void setShouldNotFailBuild(boolean shouldNotFailBuild) { - remoteBuildConfig.setShouldNotFailBuild(shouldNotFailBuild); - } - - @DataBoundSetter - public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { - remoteBuildConfig.setPreventRemoteBuildQueue(preventRemoteBuildQueue); - } - - @DataBoundSetter - public void setPollInterval(int pollInterval) { - remoteBuildConfig.setPollInterval(pollInterval); - } - - @DataBoundSetter - public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { - remoteBuildConfig.setBlockBuildUntilComplete(blockBuildUntilComplete); - } - - @DataBoundSetter - public void setToken(String token) { - remoteBuildConfig.setToken(token); - } - - @DataBoundSetter - public void setParameters(String parameters) { - remoteBuildConfig.setParameters(parameters); - } - - @DataBoundSetter - public void setEnhancedLogging(boolean enhancedLogging) { - remoteBuildConfig.setEnhancedLogging(enhancedLogging); - } - - @DataBoundSetter - public void setLoadParamsFromFile(boolean loadParamsFromFile) { - remoteBuildConfig.setLoadParamsFromFile(loadParamsFromFile); - } - - @DataBoundSetter - public void setParameterFile(String parameterFile) { - remoteBuildConfig.setParameterFile(parameterFile); - } + + @DataBoundSetter + public void setMaxConn(int maxConn) { + remoteBuildConfig.setMaxConn(maxConn); + } + + @DataBoundSetter + public void setAuth(Auth2 auth) { + remoteBuildConfig.setAuth2(auth); + } + + @DataBoundSetter + public void setRemoteJenkinsName(String remoteJenkinsName) { + remoteBuildConfig.setRemoteJenkinsName(remoteJenkinsName); + } + + @DataBoundSetter + public void setRemoteJenkinsUrl(String remoteJenkinsUrl) { + remoteBuildConfig.setRemoteJenkinsUrl(remoteJenkinsUrl); + } + + @DataBoundSetter + public void setShouldNotFailBuild(boolean shouldNotFailBuild) { + remoteBuildConfig.setShouldNotFailBuild(shouldNotFailBuild); + } + + @DataBoundSetter + public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { + remoteBuildConfig.setPreventRemoteBuildQueue(preventRemoteBuildQueue); + } + + @DataBoundSetter + public void setPollInterval(int pollInterval) { + remoteBuildConfig.setPollInterval(pollInterval); + } + + @DataBoundSetter + public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { + remoteBuildConfig.setBlockBuildUntilComplete(blockBuildUntilComplete); + } + + @DataBoundSetter + public void setToken(String token) { + remoteBuildConfig.setToken(token); + } + + @DataBoundSetter + public void setParameters(String parameters) { + remoteBuildConfig.setParameters(parameters); + } + + @DataBoundSetter + public void setEnhancedLogging(boolean enhancedLogging) { + remoteBuildConfig.setEnhancedLogging(enhancedLogging); + } + + @DataBoundSetter + public void setLoadParamsFromFile(boolean loadParamsFromFile) { + remoteBuildConfig.setLoadParamsFromFile(loadParamsFromFile); + } + + @DataBoundSetter + public void setParameterFile(String parameterFile) { + remoteBuildConfig.setParameterFile(parameterFile); + } @DataBoundSetter public void setUseJobInfoCache(boolean useJobInfoCache) { @@ -150,180 +152,191 @@ public void setUseJobInfoCache(boolean useJobInfoCache) { public void setUseCrumbCache(boolean useCrumbCache) { remoteBuildConfig.setUseCrumbCache(useCrumbCache); } - - @Override - public StepExecution start(StepContext context) throws Exception { - return new Execution(context, remoteBuildConfig); - } - - @Extension(optional = true) - public static final class DescriptorImpl extends StepDescriptor { - - @Override public String getFunctionName() { - return "triggerRemoteJob"; - } - - @Override public String getDisplayName() { - return "Trigger Remote Job"; - } - - @Override public Set> getRequiredContext() { - Set> set = new HashSet>(); - Collections.addAll(set, Run.class, FilePath.class, Launcher.class, TaskListener.class); - return set; - } - - @Restricted(NoExternalUse.class) - @Nonnull - public ListBoxModel doFillRemoteJenkinsNameItems() { - RemoteBuildConfiguration.DescriptorImpl descriptor = Descriptor.findByDescribableClassName( - ExtensionList.lookup(RemoteBuildConfiguration.DescriptorImpl.class), RemoteBuildConfiguration.class.getName()); - if(descriptor == null) throw new RuntimeException("Could not get descriptor for RemoteBuildConfiguration"); - return descriptor.doFillRemoteJenkinsNameItems(); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckJob( - @QueryParameter("job") final String value, - @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, - @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, remoteJenkinsName, value); - if(result.isAffected(AffectedField.JOB_NAME_OR_URL)) return result.formValidation; - return FormValidation.ok(); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckRemoteJenkinsUrl( - @QueryParameter("remoteJenkinsUrl") final String value, - @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, - @QueryParameter("job") final String job) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, remoteJenkinsName, job); - if(result.isAffected(AffectedField.REMOTE_JENKINS_URL)) return result.formValidation; - return FormValidation.ok(); - } - - @Restricted(NoExternalUse.class) - public FormValidation doCheckRemoteJenkinsName( - @QueryParameter("remoteJenkinsName") final String value, - @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, - @QueryParameter("job") final String job) { - RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, job); - if(result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) return result.formValidation; - return FormValidation.ok(); - } - - public static List getAuth2Descriptors() { - return Auth2.all(); - } - - public static Auth2Descriptor getDefaultAuth2Descriptor() { - return NullAuth.DESCRIPTOR; - } - } - - public static class Execution extends SynchronousNonBlockingStepExecution { - - private static final long serialVersionUID = 5339071667093320735L; - - private final RemoteBuildConfiguration remoteBuildConfig; - - Execution(StepContext context, RemoteBuildConfiguration remoteBuildConfig) { - super(context); - this.remoteBuildConfig = remoteBuildConfig; - } - - @Override protected Handle run() throws Exception { - StepContext stepContext = getContext(); - Run build = stepContext.get(Run.class); - FilePath workspace = stepContext.get(FilePath.class); - TaskListener listener = stepContext.get(TaskListener.class); - RemoteJenkinsServer effectiveRemoteServer = remoteBuildConfig.evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); - BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); - Handle handle = null; - try { - handle = remoteBuildConfig.performTriggerAndGetQueueId(context); - if(remoteBuildConfig.getBlockBuildUntilComplete()) { - remoteBuildConfig.performWaitForBuild(context, handle); - } - - } catch(InterruptedException e) { - remoteBuildConfig.abortRemoteTask(effectiveRemoteServer, handle, context); - throw e; - } - return handle; - } - } - - public String getRemoteJenkinsName() { - return remoteBuildConfig.getRemoteJenkinsName(); - } - - public String getRemoteJenkinsUrl() { - return remoteBuildConfig.getRemoteJenkinsUrl(); - } - - public String getJob() { - return remoteBuildConfig.getJob(); - } - - public boolean getShouldNotFailBuild() { - return remoteBuildConfig.getShouldNotFailBuild(); - } - - public boolean getPreventRemoteBuildQueue() { - return remoteBuildConfig.getPreventRemoteBuildQueue(); - } - - public int getPollInterval() { - return remoteBuildConfig.getPollInterval(); - } - - public boolean getBlockBuildUntilComplete() { - return remoteBuildConfig.getBlockBuildUntilComplete(); - } - - public String getToken() { - return remoteBuildConfig.getToken(); - } - - public String getParameters() { - return remoteBuildConfig.getParameters(); - } - - public boolean getEnhancedLogging() { - return remoteBuildConfig.getEnhancedLogging(); - } - - public boolean getLoadParamsFromFile() { - return remoteBuildConfig.getLoadParamsFromFile(); - } - - public String getParameterFile() { - return remoteBuildConfig.getParameterFile(); - } - - public int getConnectionRetryLimit() { - return remoteBuildConfig.getConnectionRetryLimit(); - } - - public boolean isUseCrumbCache() { - return remoteBuildConfig.isUseCrumbCache(); - } - - public boolean isUseJobInfoCache() { - return remoteBuildConfig.isUseJobInfoCache(); - } - + + @Override + public StepExecution start(StepContext context) throws Exception { + return new Execution(context, remoteBuildConfig); + } + + @Extension(optional = true) + public static final class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "triggerRemoteJob"; + } + + @Override + public String getDisplayName() { + return "Trigger Remote Job"; + } + + @Override + public Set> getRequiredContext() { + Set> set = new HashSet>(); + Collections.addAll(set, Run.class, FilePath.class, Launcher.class, TaskListener.class); + return set; + } + + @Restricted(NoExternalUse.class) + @Nonnull + public ListBoxModel doFillRemoteJenkinsNameItems() { + RemoteBuildConfiguration.DescriptorImpl descriptor = Descriptor.findByDescribableClassName( + ExtensionList.lookup(RemoteBuildConfiguration.DescriptorImpl.class), + RemoteBuildConfiguration.class.getName()); + if (descriptor == null) + throw new RuntimeException("Could not get descriptor for RemoteBuildConfiguration"); + return descriptor.doFillRemoteJenkinsNameItems(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckJob(@QueryParameter("job") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, + remoteJenkinsName, value); + if (result.isAffected(AffectedField.JOB_NAME_OR_URL)) + return result.formValidation; + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckRemoteJenkinsUrl(@QueryParameter("remoteJenkinsUrl") final String value, + @QueryParameter("remoteJenkinsName") final String remoteJenkinsName, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(value, + remoteJenkinsName, job); + if (result.isAffected(AffectedField.REMOTE_JENKINS_URL)) + return result.formValidation; + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckRemoteJenkinsName(@QueryParameter("remoteJenkinsName") final String value, + @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, + @QueryParameter("job") final String job) { + RemoteURLCombinationsResult result = FormValidationUtils.checkRemoteURLCombinations(remoteJenkinsUrl, value, + job); + if (result.isAffected(AffectedField.REMOTE_JENKINS_NAME)) + return result.formValidation; + return FormValidation.ok(); + } + + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NullAuth.DESCRIPTOR; + } + } + + public static class Execution extends SynchronousNonBlockingStepExecution { + + private static final long serialVersionUID = 5339071667093320735L; + + private final RemoteBuildConfiguration remoteBuildConfig; + + Execution(StepContext context, RemoteBuildConfiguration remoteBuildConfig) { + super(context); + this.remoteBuildConfig = remoteBuildConfig; + } + + @Override + protected Handle run() throws Exception { + StepContext stepContext = getContext(); + Run build = stepContext.get(Run.class); + FilePath workspace = stepContext.get(FilePath.class); + TaskListener listener = stepContext.get(TaskListener.class); + RemoteJenkinsServer effectiveRemoteServer = remoteBuildConfig + .evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), + effectiveRemoteServer); + Handle handle = null; + try { + handle = remoteBuildConfig.performTriggerAndGetQueueId(context); + if (remoteBuildConfig.getBlockBuildUntilComplete()) { + remoteBuildConfig.performWaitForBuild(context, handle); + } + + } catch (InterruptedException e) { + remoteBuildConfig.abortRemoteTask(effectiveRemoteServer, handle, context); + throw e; + } + return handle; + } + } + + public String getRemoteJenkinsName() { + return remoteBuildConfig.getRemoteJenkinsName(); + } + + public String getRemoteJenkinsUrl() { + return remoteBuildConfig.getRemoteJenkinsUrl(); + } + + public String getJob() { + return remoteBuildConfig.getJob(); + } + + public boolean getShouldNotFailBuild() { + return remoteBuildConfig.getShouldNotFailBuild(); + } + + public boolean getPreventRemoteBuildQueue() { + return remoteBuildConfig.getPreventRemoteBuildQueue(); + } + + public int getPollInterval() { + return remoteBuildConfig.getPollInterval(); + } + + public boolean getBlockBuildUntilComplete() { + return remoteBuildConfig.getBlockBuildUntilComplete(); + } + + public String getToken() { + return remoteBuildConfig.getToken(); + } + + public String getParameters() { + return remoteBuildConfig.getParameters(); + } + + public boolean getEnhancedLogging() { + return remoteBuildConfig.getEnhancedLogging(); + } + + public boolean getLoadParamsFromFile() { + return remoteBuildConfig.getLoadParamsFromFile(); + } + + public String getParameterFile() { + return remoteBuildConfig.getParameterFile(); + } + + public int getConnectionRetryLimit() { + return remoteBuildConfig.getConnectionRetryLimit(); + } + + public boolean isUseCrumbCache() { + return remoteBuildConfig.isUseCrumbCache(); + } + + public boolean isUseJobInfoCache() { + return remoteBuildConfig.isUseJobInfoCache(); + } + public boolean isAbortTriggeredJob() { return remoteBuildConfig.isAbortTriggeredJob(); } - - public int getMaxConn() { - return remoteBuildConfig.getMaxConn(); - } - public Auth2 getAuth() { - return remoteBuildConfig.getAuth2(); - } + public int getMaxConn() { + return remoteBuildConfig.getMaxConn(); + } + + public Auth2 getAuth() { + return remoteBuildConfig.getAuth2(); + } } From 02598da1d3c51cbed17f3b6dfb9fb19d4a629913 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:23:39 +0800 Subject: [PATCH 121/262] update the compatible version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f7c7680..6ec4350a 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ maven-hpi-plugin - 3.0.3-SNAPSHOT + 3.0.4-SNAPSHOT From 093a3d8244a383e815684f7c6bfe5fcaf06c8da7 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:28:53 +0800 Subject: [PATCH 122/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6ec4350a..de2f364b 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.4-SNAPSHOT + 3.0.4 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.4 From a1aa0cc0e5ad4a9f8e31c72bec212cbe98c482c5 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:29:04 +0800 Subject: [PATCH 123/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index de2f364b..de3e642a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.4 + 3.0.5-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.4 + HEAD From 208ea8c5f3f47b6b45cea0627a1bba02843f1576 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 30 Jul 2018 00:35:58 +0800 Subject: [PATCH 124/262] Update change log --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7b7a51..a268bea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# 3.0.4 (Jul 30th, 2018) +### New feature + +* Support to abort remote job + +### Improvement + +* None + +### Bug fixes + +* [PR #45](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/45) + + # 3.0.3 (Jul 23th, 2018) ### New feature From a049e10d4da3837b1b554ce52cf8fb5cb1b2eb68 Mon Sep 17 00:00:00 2001 From: Benoit Beaudin <31477008+wincrasher@users.noreply.github.com> Date: Wed, 8 Aug 2018 21:06:51 -0400 Subject: [PATCH 125/262] Update RemoteBuildConfiguration.java (#46) * Update RemoteBuildConfiguration.java When using triggerRemoteJob from pipeline, if we enter multiple parameters, by default, the pipeline script editor add multiple tabulation to indent the line correctly. When processing them we need to remove those unnecessary tabulation. --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index c9cce783..9d3005e2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -3,6 +3,7 @@ import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; +import static org.apache.commons.lang.StringUtils.stripAll; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; import java.io.BufferedReader; @@ -238,6 +239,7 @@ public List getParameterList(BuildContext context) { String params = getParameters(); if (!params.isEmpty()) { String[] parameterArray = params.split("\n"); + parameterArray = stripAll(parameterArray); return new ArrayList(Arrays.asList(parameterArray)); } else if (loadParamsFromFile) { return loadExternalParameterFile(context); From 0f0af69849538dcb53eae44c420827b5ff624367 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 20 Aug 2018 23:06:54 +0800 Subject: [PATCH 126/262] remove the dummy global cookie manager --- .../ParameterizedRemoteTrigger/utils/HttpHelper.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index a9fd9426..a294f1b9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -380,12 +380,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, return triggerUrlString; } - - static { - java.net.CookieManager cm = new java.net.CookieManager(); - java.net.CookieHandler.setDefault(cm); - } - + /** * Same as sendHTTPCall, but keeps track of the number of failed connection * attempts (aka: the number of times this method has been called). In the case From 11127a8e3db2f3734ff07cf38be76384f9bd2fd1 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 20 Aug 2018 23:41:43 +0800 Subject: [PATCH 127/262] update readme for release --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a268bea3..317a12a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 3.0.5 (Aug 20th, 2018) +### New feature + +* None + +### Improvement + +* None + +### Bug fixes + +* [PR #46](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/46) +* [JENKINS-53125](https://issues.jenkins-ci.org/browse/JENKINS-53125) + + # 3.0.4 (Jul 30th, 2018) ### New feature From ec403209bd454b6ffb2d65d0d951428d229c966b Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 20 Aug 2018 23:45:44 +0800 Subject: [PATCH 128/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index de3e642a..dfbfd2f6 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.5-SNAPSHOT + 3.0.5 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.5 From 34d70a6ff588aa6f94dfdd0ac80e8d5c3a5b0de8 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 20 Aug 2018 23:45:55 +0800 Subject: [PATCH 129/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index dfbfd2f6..339f20eb 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.5 + 3.0.6-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.5 + HEAD From d20700909a33dab00295b9b5b80ecd13741af76b Mon Sep 17 00:00:00 2001 From: Robert Hencke Date: Thu, 13 Sep 2018 21:18:41 -0400 Subject: [PATCH 130/262] [FIX JENKINS-52810] Ensure older configurations work when deserialized. (#47) When deserializing a job configuration from an older version, the hostLocks and hostPermits fields will be null as they were not present in the older versions. This can cause a NullPointerException when running a job with an older configuration. Fix this by ensuring older configurations get proper default values for these fields, if missing. --- .../RemoteBuildConfiguration.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 9d3005e2..b430a54c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -140,6 +140,12 @@ protected Object readResolve() { } } auth = null; + if (hostLocks == null) { + hostLocks = new HashMap<>(); + } + if (hostPermits == null) { + hostPermits = new HashMap<>(); + } return this; } From a52ca84ca956750428665670d1406e813de67404 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 18 Sep 2018 00:50:11 +0800 Subject: [PATCH 131/262] support disabling step (no need to remove steps) --- .../RemoteBuildConfiguration.java | 44 ++++++++++++++----- .../pipeline/RemoteBuildPipelineStep.java | 18 ++++++-- .../RemoteBuildConfiguration/config.jelly | 3 ++ .../RemoteBuildPipelineStep/config.jelly | 3 ++ 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b430a54c..9954989b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -114,7 +114,9 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean useCrumbCache; private boolean useJobInfoCache; private boolean abortTriggeredJob; + private boolean disabled; + private Map hostLocks = new HashMap<>(); private Map hostPermits = new HashMap<>(); @@ -240,6 +242,22 @@ public void setParameterFile(String parameterFile) { else this.parameterFile = parameterFile; } + + @DataBoundSetter + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + @DataBoundSetter + public void setUseJobInfoCache(boolean useJobInfoCache) { + this.useJobInfoCache = useJobInfoCache; + } + + + @DataBoundSetter + public void setUseCrumbCache(boolean useCrumbCache) { + this.useCrumbCache = useCrumbCache; + } public List getParameterList(BuildContext context) { String params = getParameters(); @@ -543,10 +561,20 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen FilePath workspace = build.getWorkspace(); if (workspace == null) throw new IllegalArgumentException("The workspace can not be null"); - perform(build, workspace, launcher, listener); + if (!isStepDisabled(listener.getLogger())) { + perform(build, workspace, launcher, listener); + } return true; } + public boolean isStepDisabled(PrintStream printStream) { + if (isDisabled()) { + printStream.println("remote trigger step was disabled. skipping..."); + return true; + } + return false; + } + /** * Triggers the remote job and, waits until completion if * blockBuildUntilComplete is set. @@ -1004,6 +1032,10 @@ public int getConnectionRetryLimit() { return connectionRetryLimit; // For now, this is a constant } + public boolean isDisabled() { + return disabled; + } + private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) throws IOException, InterruptedException { @@ -1102,20 +1134,10 @@ public boolean isUseCrumbCache() { return useCrumbCache; } - @DataBoundSetter - public void setUseCrumbCache(boolean useCrumbCache) { - this.useCrumbCache = useCrumbCache; - } - public boolean isUseJobInfoCache() { return useJobInfoCache; } - @DataBoundSetter - public void setUseJobInfoCache(boolean useJobInfoCache) { - this.useJobInfoCache = useJobInfoCache; - } - // This indicates to Jenkins that this is an implementation of an extension // point. @Extension diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 69fc33dc..5e5bfb80 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -152,6 +152,11 @@ public void setUseJobInfoCache(boolean useJobInfoCache) { public void setUseCrumbCache(boolean useCrumbCache) { remoteBuildConfig.setUseCrumbCache(useCrumbCache); } + + @DataBoundSetter + public void setDisabled(boolean disabled) { + remoteBuildConfig.setDisabled(disabled); + } @Override public StepExecution start(StepContext context) throws Exception { @@ -254,9 +259,11 @@ protected Handle run() throws Exception { effectiveRemoteServer); Handle handle = null; try { - handle = remoteBuildConfig.performTriggerAndGetQueueId(context); - if (remoteBuildConfig.getBlockBuildUntilComplete()) { - remoteBuildConfig.performWaitForBuild(context, handle); + if (!remoteBuildConfig.isStepDisabled(listener.getLogger())) { + handle = remoteBuildConfig.performTriggerAndGetQueueId(context); + if (remoteBuildConfig.getBlockBuildUntilComplete()) { + remoteBuildConfig.performWaitForBuild(context, handle); + } } } catch (InterruptedException e) { @@ -338,5 +345,10 @@ public int getMaxConn() { public Auth2 getAuth() { return remoteBuildConfig.getAuth2(); } + + public boolean isDisabled() { + return remoteBuildConfig.isDisabled(); + } + } diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index e13c31b2..b4753230 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -59,6 +59,9 @@ + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index d7d19fed..25b919b6 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -60,6 +60,9 @@ + + + From f0e7a390cb433d4f8ab4610646bbdd9781b0c252 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 18 Sep 2018 21:56:50 +0800 Subject: [PATCH 132/262] add description of disable field --- .../RemoteBuildConfiguration/help-disabled.html | 3 +++ .../pipeline/RemoteBuildPipelineStep/help-disabled.html | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-disabled.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-disabled.html diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-disabled.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-disabled.html new file mode 100644 index 00000000..00814715 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-disabled.html @@ -0,0 +1,3 @@ +
+Set this field to disable the job step instead of removing it from job configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-disabled.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-disabled.html new file mode 100644 index 00000000..00814715 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-disabled.html @@ -0,0 +1,3 @@ +
+Set this field to disable the job step instead of removing it from job configuration. +
From d9fb4e94b31efd4bed058853380d850dd83d1e8c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 18 Sep 2018 23:56:13 +0800 Subject: [PATCH 133/262] update release note for v3.0.6 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 317a12a3..0cdd1743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# 3.0.6 (Sep 18th, 2018) +### New feature + +* Disable remote trigger job step instead of removing it + +### Improvement + +* None + +### Bug fixes + +* [JENKINS-52810](https://issues.jenkins-ci.org/browse/JENKINS-52810) + + # 3.0.5 (Aug 20th, 2018) ### New feature From 2149adc85084f718eb203c34eacbe472ff250f75 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 19 Sep 2018 00:03:20 +0800 Subject: [PATCH 134/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.6 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 339f20eb..80cd6e47 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.6-SNAPSHOT + 3.0.6 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.6 From 96b5221b265fc2aba99fecb9478dda149cd3ea5f Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 19 Sep 2018 00:03:32 +0800 Subject: [PATCH 135/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 80cd6e47..eb2ed788 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.6 + 3.0.7-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.6 + HEAD From cc6ed0f61acf0abf54202fff72035ff47f41dbc8 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 11 Oct 2018 18:20:35 +0200 Subject: [PATCH 136/262] Update README_PipelineConfiguration.md (#48) Update readme according to the current state of /src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java --- README_PipelineConfiguration.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md index f7284d0f..79d353d6 100644 --- a/README_PipelineConfiguration.md +++ b/README_PipelineConfiguration.md @@ -92,7 +92,9 @@ The `Handle` object provides the following methods: - `String getJobName()` returns the remote job name - `URL getBuildUrl()` returns the remote build URL including the build number - `int getBuildNumber()` returns the remote build number -- `BuildStatus getBuildStatus()` returns the current remote build status +- `RemoteBuildInfo getBuildInfo()` return information regarding the current remote build +- `RemoteBuildStatus getBuildStatus()` returns the current remote build status +- `Result getBuildResult()` return the result of the remote build - `BuildStatus getBuildStatusBlocking()` waits for completion and returns the build result - `boolean isFinished()` true if the remote build finished - `boolean isQueued()` true if the job is queued but not yet running @@ -107,13 +109,8 @@ def results = handle.readJsonFileFromBuildArchive('build-results.json') echo results.urlToTestResults //just an example ``` -The `BuildStatus` enum provides the following types and methods: - -- Custom statuses: `UNKNOWN`, `NOT_STARTED`, `QUEUED`, `RUNNING`, if the remote job did not finish yet. -- Jenkins Result statuses: `ABORTED`, `FAILURE`, `NOT_BUILT`, `SUCCESS`, `UNSTABLE`, if the remote job finished the status reflects the Jenkins build `Result`. -- `boolean isJenkinsResult()`, true if the `BuildStatus` reflects a Jenkins `Result`. -- `Result getJenkinsResult()`, the Jenkins `Result` if the status reflects one, null otherwise. -- `String toString()` +- Enum of RemoteBuildStatus may have the values: `UNKNOWN`, `NOT_STARTED`, `QUEUED`, `RUNNING`, if the remote job did not finish yet. +- Enum of Result may have the values: `ABORTED`, `FAILURE`, `NOT_BUILT`, `SUCCESS`, `UNSTABLE`, if the remote job finished the status reflects the Jenkins build `Result`.
From 17b6bdfa3366fa570ca5d83923e7fe0c971ddf0e Mon Sep 17 00:00:00 2001 From: lifemanship Date: Sat, 8 Dec 2018 19:12:23 +0300 Subject: [PATCH 137/262] Fix fails with poll interval value more than 5 min (#49) Fix releated to the issue: https://issues.jenkins-ci.org/browse/JENKINS-55038 There is a problem when we try to use poll interval parameter's value more than 300 seconds(5 minutes). We have a Jenkins pipeline which may take from 30 to 60 minutes. In some cases Jenkins `queued` item may move to the `pending` state as it described here: https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Queue.java#L139 In such case the plugin use user specified poll interval time out value to check `queued` item state: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/blob/master/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java#L696 But all `queued` items in the Jenkins have time to live only 5 minutes as you can see it here: https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Queue.java#L218 As result we have triggered `build` on the remote Jenkins server but failed build(`Max number of connection retries have been exeeded` error from the plugin) on the main Jenkins server. As fix we propose use default value of the poll interval(10 seconds) to check `queued` item state. Because in the `pending` state the `queued` item is staying only for a few seconds. --- .../RemoteBuildConfiguration.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 9954989b..a18df7ca 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -692,9 +692,13 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx context.logger.println("Waiting for remote build to be executed..."); } + int pollIntervalForQueuedItem = this.pollInterval; + if (pollIntervalForQueuedItem > DEFAULT_POLLINTERVALL) { + pollIntervalForQueuedItem = DEFAULT_POLLINTERVALL; + } while (buildInfo.isQueued()) { - context.logger.println("Waiting for " + this.pollInterval + " seconds until next poll."); - Thread.sleep(this.pollInterval * 1000); + context.logger.println("Waiting for " + pollIntervalForQueuedItem + " seconds until next poll."); + Thread.sleep(pollIntervalForQueuedItem * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } From e74a23974ae82cf96fb7f9d32e531ef63e9d5cfb Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 9 Dec 2018 12:55:58 +0800 Subject: [PATCH 138/262] suppress the warning for getting connection lock fail --- .../plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index a294f1b9..8678e43e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -556,11 +556,11 @@ private static ConnectionResponse tryCall(String urlString, String method, Build // if we can't lock, just let it go. } catch (InterruptedException e) { logger.log(Level.WARNING, "fail to accquire lock because of interrupt, skip locking...", e); - context.logger.println("fail to accquire lock because of interrupt, skip locking..."); +// context.logger.println("fail to accquire lock because of interrupt, skip locking..."); } if (isAccquired != null && !isAccquired) { logger.warning("fail to accquire lock because of timeout, skip locking..."); - context.logger.println("fail to accquire lock because of timeout, skip locking..."); +// context.logger.println("fail to accquire lock because of timeout, skip locking..."); } ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, From 614e9fbc50747d1b7ce39d548bc28f02bd28cf9f Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 9 Dec 2018 13:24:29 +0800 Subject: [PATCH 139/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.7 --- pom.xml | 208 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/pom.xml b/pom.xml index eb2ed788..c1512980 100644 --- a/pom.xml +++ b/pom.xml @@ -1,104 +1,104 @@ - - 4.0.0 - - org.jenkins-ci.plugins - plugin - 3.5 - - - - 1.642.3 - 8 - - - Parameterized-Remote-Trigger - 3.0.7-SNAPSHOT - hpi - Parameterized Remote Trigger Plugin - This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. - http://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Remote+Trigger+Plugin - - - - MIT license - All source code is under the MIT license. - - - - - - morficus - Maurice Williams - - - cashlalala - KaiHsiang Chang - - - - - - - org.jenkins-ci.tools - maven-hpi-plugin - - - 3.0.4-SNAPSHOT - - - - - - - scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git - scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git - https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD - - - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - - - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - - - - - org.jenkins-ci.plugins - credentials - 2.1.16 - - - org.jenkins-ci.plugins - token-macro - 2.3 - - - org.jenkins-ci.plugins - script-security - 1.34 - true - - - org.jenkins-ci.plugins.workflow - workflow-step-api - 2.13 - true - - - org.mockito - mockito-core - 2.18.3 - test - - - - + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 3.5 + + + + 1.642.3 + 8 + + + Parameterized-Remote-Trigger + 3.0.7 + hpi + Parameterized Remote Trigger Plugin + This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. + http://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Remote+Trigger+Plugin + + + + MIT license + All source code is under the MIT license. + + + + + + morficus + Maurice Williams + + + cashlalala + KaiHsiang Chang + + + + + + + org.jenkins-ci.tools + maven-hpi-plugin + + + 3.0.4-SNAPSHOT + + + + + + + scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git + scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git + https://github.com/jenkinsci/parameterized-remote-trigger-plugin + Parameterized-Remote-Trigger-3.0.7 + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + + repo.jenkins-ci.org + http://repo.jenkins-ci.org/public/ + + + + + + org.jenkins-ci.plugins + credentials + 2.1.16 + + + org.jenkins-ci.plugins + token-macro + 2.3 + + + org.jenkins-ci.plugins + script-security + 1.34 + true + + + org.jenkins-ci.plugins.workflow + workflow-step-api + 2.13 + true + + + org.mockito + mockito-core + 2.18.3 + test + + + + From 3efdf8797d19ef3195fc8eb21466b96fa7b393ad Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 9 Dec 2018 13:24:40 +0800 Subject: [PATCH 140/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c1512980..e3b9bbe6 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.7 + 3.0.8-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.7 + HEAD From 709542efe37ce220ab9757048c6a975e393f055c Mon Sep 17 00:00:00 2001 From: Cameron Testerman Date: Thu, 13 Dec 2018 09:55:12 -0600 Subject: [PATCH 141/262] Fixed misspellings (#50) --- .../utils/HttpHelper.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 8678e43e..f09af346 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -547,20 +547,18 @@ private static ConnectionResponse tryCall(String urlString, String method, Build return sendHTTPCall(urlString, method, context, null, 1, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); } - Boolean isAccquired = null; + Boolean isAcquired = null; try { try { - isAccquired = lock.tryAcquire(pollInterval, TimeUnit.SECONDS); + isAcquired = lock.tryAcquire(pollInterval, TimeUnit.SECONDS); logger.log(Level.FINE, String.format("calling %s in semaphore...", urlString)); // if we can't lock, just let it go. } catch (InterruptedException e) { - logger.log(Level.WARNING, "fail to accquire lock because of interrupt, skip locking...", e); -// context.logger.println("fail to accquire lock because of interrupt, skip locking..."); + logger.log(Level.WARNING, "fail to acquire lock because of interrupt, skip locking...", e); } - if (isAccquired != null && !isAccquired) { - logger.warning("fail to accquire lock because of timeout, skip locking..."); -// context.logger.println("fail to accquire lock because of timeout, skip locking..."); + if (isAcquired != null && !isAcquired) { + logger.warning("fail to acquire lock because of timeout, skip locking..."); } ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, @@ -568,7 +566,7 @@ private static ConnectionResponse tryCall(String urlString, String method, Build return cr; } finally { - if (isAccquired != null && isAccquired) { + if (isAcquired != null && isAcquired) { lock.release(); } } From 97de437b98bec1cd9d46b78047886809c1e110d2 Mon Sep 17 00:00:00 2001 From: Thorsten Willenbacher Date: Wed, 13 Feb 2019 16:00:59 +0100 Subject: [PATCH 142/262] Avoid re-POST after timeout --- .../utils/HttpHelper.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index f09af346..e4c6a8e4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -439,26 +439,33 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT addCrumbToConnection(conn, context, overrideAuth, isCrubmCacheEnabled); // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); - conn.setReadTimeout(10000); if (HTTP_POST.equalsIgnoreCase(requestType)) { + // use longer timeout during POST due to not performing retrys since POST is not idem-potent + conn.setReadTimeout(30000); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); conn.setDoOutput(true); conn.getOutputStream().write(postDataBytes); + }else { + conn.setReadTimeout(10000); } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); logger.finer(String.format("%s begin: %s", urlString, sdf.format(new Date()))); Instant before = Instant.now(); - conn.connect(); - Instant after = Instant.now(); logger.finer( String.format("%s end: elapsed [%s] ms", urlString, Duration.between(before, after).toMillis())); - responseHeader = conn.getHeaderFields(); + if (HTTP_POST.equalsIgnoreCase(requestType)) { + // if connection to the server succeeded we should not perform any further retries + // of a POST request since the data may have been transferred and since POST is not + // idem-potent reposting is not a good idea. + // Setting retryLimit to -1 will avoid potential double-POSTs due to timeouts during getResponseCode + retryLimit = -1; + } responseCode = conn.getResponseCode(); if (responseCode == 401) { From b10fb31158cba5c3f80a8a870fef411b8463f3aa Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Mon, 18 Feb 2019 21:25:59 +0100 Subject: [PATCH 143/262] reflect changed method name to docs --- README_PipelineConfiguration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md index 79d353d6..7854ed42 100644 --- a/README_PipelineConfiguration.md +++ b/README_PipelineConfiguration.md @@ -95,7 +95,7 @@ The `Handle` object provides the following methods: - `RemoteBuildInfo getBuildInfo()` return information regarding the current remote build - `RemoteBuildStatus getBuildStatus()` returns the current remote build status - `Result getBuildResult()` return the result of the remote build -- `BuildStatus getBuildStatusBlocking()` waits for completion and returns the build result +- `RemoteBuildStatus updateBuildStatusBlocking()` waits for completion and returns the build result - `boolean isFinished()` true if the remote build finished - `boolean isQueued()` true if the job is queued but not yet running - `String toString()` @@ -157,7 +157,7 @@ echo handle.getBuildStatus().toString(); Even with `blockBuildUntilComplete: false` it is possible to wait synchronously until the remote job finished: ``` def handle = triggerRemoteJob blockBuildUntilComplete: false, ... -def status = handle.getBuildStatusBlocking() +def status = handle.updateBuildStatusBlocking() ``` :warning: Currently the plugin cannot log to the pipeline log directly if used in non-blocking mode. As workaround you can use `handle.lastLog()` after each command to get the log entries. From 541365a0740f1e5b17f2615076249c4da33c34bc Mon Sep 17 00:00:00 2001 From: Christopher Fenner Date: Tue, 19 Feb 2019 00:03:49 +0100 Subject: [PATCH 144/262] correct return types in javadoc --- .../plugins/ParameterizedRemoteTrigger/pipeline/Handle.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 285653b0..854b71d9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -202,7 +202,7 @@ public RemoteBuildInfo getBuildInfo() { /** * Gets the current build status of the remote job. * - * @return {@link hudson.model.Result} the build result + * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status */ @Nonnull @Whitelisted @@ -213,8 +213,7 @@ public RemoteBuildStatus getBuildStatus() { /** * Updates the current build status of the remote job. * - * @return {@link hudson.model.Result} the build result - * + * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status * @throws IOException * if there is an error retrieving the remote build number, or, * if there is an error retrieving the remote build status, or, From 285d6573107789f3480d5a7fbc726d94a93cb917 Mon Sep 17 00:00:00 2001 From: Dean Aldinger Date: Fri, 22 Mar 2019 16:06:16 -0400 Subject: [PATCH 145/262] Changed to use effective build host when updating build info --- .../RemoteBuildConfiguration.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index a18df7ca..7a0a8731 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -12,6 +12,8 @@ import java.io.PrintStream; import java.io.Serializable; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -831,7 +833,22 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } QueueItemData queueItem = getQueueItemData(queueId, context); if (queueItem.isExecuted()) { - buildInfo.setBuildData(queueItem.getBuildNumber(), queueItem.getBuildURL()); + URL effectiveRemoteBuildURL = queueItem.getBuildURL(); + try { + URI effectiveUri = new URI(context.effectiveRemoteServer.getAddress()); + String effectiveHostname = effectiveUri.getHost(); + URL remoteURL = queueItem.getBuildURL(); + if (remoteURL != null) { + URI remoteUri = remoteURL.toURI(); + String remoteHostname = remoteUri.getHost(); + String effectiveRemoteAddress = remoteUri.toString().replaceAll(remoteHostname,effectiveHostname); + effectiveRemoteBuildURL = new URL(effectiveRemoteAddress); + } + } catch (URISyntaxException ex) { + throw new AbortException( + String.format("Unexpected syntax error: %s.", ex.toString())); + } + buildInfo.setBuildData(queueItem.getBuildNumber(), effectiveRemoteBuildURL); } return buildInfo; } From b06996d9cdc70a74bc39ad54504f86e8786ae99e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:09:56 +0800 Subject: [PATCH 146/262] code reformat on RemoteBuildConfiguration --- .../RemoteBuildConfiguration.java | 162 ++++++++---------- 1 file changed, 70 insertions(+), 92 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 7a0a8731..aeffc00d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -117,7 +117,6 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean useJobInfoCache; private boolean abortTriggeredJob; private boolean disabled; - private Map hostLocks = new HashMap<>(); private Map hostPermits = new HashMap<>(); @@ -152,7 +151,7 @@ protected Object readResolve() { } return this; } - + @DataBoundSetter public void setAbortTriggeredJob(boolean abortTriggeredJob) { this.abortTriggeredJob = abortTriggeredJob; @@ -244,7 +243,7 @@ public void setParameterFile(String parameterFile) { else this.parameterFile = parameterFile; } - + @DataBoundSetter public void setDisabled(boolean disabled) { this.disabled = disabled; @@ -254,7 +253,6 @@ public void setDisabled(boolean disabled) { public void setUseJobInfoCache(boolean useJobInfoCache) { this.useJobInfoCache = useJobInfoCache; } - @DataBoundSetter public void setUseCrumbCache(boolean useCrumbCache) { @@ -326,8 +324,7 @@ private void removeEmptyElements(Collection collection) { * that no type of character encoding is happening at this step. All encoding * happens in the "buildUrlQueryString" method. * - * @param List - * parameters + * @param List parameters * @return List of build parameters */ private List getCleanedParameters(List parameters) { @@ -358,14 +355,11 @@ private void removeCommentsFromParameters(Collection collection) { * configured remote host, remoteJenkinsURL which overrides the * address locally or job which can be a full job URL. * - * @param context - * the context of this Builder/BuildStep. + * @param context the context of this Builder/BuildStep. * @return {@link RemoteJenkinsServer} a RemoteJenkinsServer object, never null. - * @throws AbortException - * if no server found and remoteJenkinsUrl empty. - * @throws MalformedURLException - * if remoteJenkinsName no valid URL or - * job an URL but nor valid. + * @throws AbortException if no server found and remoteJenkinsUrl empty. + * @throws MalformedURLException if remoteJenkinsName no valid URL + * or job an URL but nor valid. */ @Nonnull public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context) throws IOException { @@ -445,8 +439,7 @@ public Semaphore getLock(String addr) { /** * Lookup up the globally configured Remote Jenkins Server based on display name * - * @param displayName - * Name of the configuration you are looking for + * @param displayName Name of the configuration you are looking for * @return A deep-copy of the RemoteJenkinsServer object configured globally */ public @Nullable @CheckForNull RemoteJenkinsServer findRemoteHost(String displayName) { @@ -514,13 +507,10 @@ private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException * Convenience function to mark the build as failed. It's intended to only be * called from this.perform(). * - * @param e - * exception that caused the build to fail. - * @param logger - * build listener. - * @throws IOException - * if the build fails and shouldNotFailBuild is not - * set. + * @param e exception that caused the build to fail. + * @param logger build listener. + * @throws IOException if the build fails and shouldNotFailBuild is + * not set. */ protected void failBuild(Exception e, PrintStream logger) throws IOException { StringBuilder msg = new StringBuilder(); @@ -540,8 +530,8 @@ protected void failBuild(Exception e, PrintStream logger) throws IOException { throw new AbortException(e.getClass().getSimpleName() + ": " + e.getMessage()); } } - - public void abortRemoteTask(RemoteJenkinsServer remoteServer, Handle handle, BuildContext context) + + public void abortRemoteTask(RemoteJenkinsServer remoteServer, Handle handle, BuildContext context) throws IOException, InterruptedException { if (isAbortTriggeredJob() && context != null && handle != null && !handle.isFinished()) { try { @@ -549,7 +539,7 @@ public void abortRemoteTask(RemoteJenkinsServer remoteServer, Handle handle, Bui RestUtils.cancelQueueItem(remoteServer.getAddress(), handle, context, this); } else { RestUtils.stopRemoteJob(handle, context, this); - } + } } catch (IOException ex) { context.logger.println("Fail to abort remote job: " + ex.getMessage()); logger.log(Level.WARNING, "Fail to abort remote job", ex); @@ -581,13 +571,13 @@ public boolean isStepDisabled(PrintStream printStream) { * Triggers the remote job and, waits until completion if * blockBuildUntilComplete is set. * - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if there is an error retrieving the remote build data, or, if - * there is an error retrieving the remote build status, or, if - * there is an error retrieving the console output of the remote - * build, or, if the remote build does not succeed. + * @throws InterruptedException if any thread has interrupted the current + * thread. + * @throws IOException if there is an error retrieving the remote build + * data, or, if there is an error retrieving the + * remote build status, or, if there is an error + * retrieving the console output of the remote + * build, or, if the remote build does not succeed. */ @Override public void perform(Run build, FilePath workspace, Launcher launcher, TaskListener listener) @@ -596,13 +586,11 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task BuildContext context = null; RemoteJenkinsServer effectiveRemoteServer = null; try { - effectiveRemoteServer = evaluateEffectiveRemoteHost( - new BasicBuildContext(build, workspace, listener)); - context = new BuildContext(build, workspace, listener, listener.getLogger(), - effectiveRemoteServer); + effectiveRemoteServer = evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); + context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); handle = performTriggerAndGetQueueId(context); performWaitForBuild(context, handle); - } catch(InterruptedException e) { + } catch (InterruptedException e) { this.abortRemoteTask(effectiveRemoteServer, handle, context); throw e; } @@ -612,13 +600,11 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task * Triggers the remote job, identifies the queue ID and, returns a * Handle to this remote execution. * - * @param context - * the context of this Builder/BuildStep. + * @param context the context of this Builder/BuildStep. * @return Handle to further tracking of the remote build status. - * @throws IOException - * if there is an error triggering the remote job. - * @throws InterruptedException - * if any thread has interrupted the current thread. + * @throws IOException if there is an error triggering the remote job. + * @throws InterruptedException if any thread has interrupted the current + * thread. * */ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOException, InterruptedException { @@ -670,14 +656,11 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti * Checks the remote build status and, waits for completion if * blockBuildUntilComplete is set. * - * @param context - * the context of this Builder/BuildStep. - * @param handle - * the handle to the remote execution. - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if any HTTP error or business logic error + * @param context the context of this Builder/BuildStep. + * @param handle the handle to the remote execution. + * @throws InterruptedException if any thread has interrupted the current + * thread. + * @throws IOException if any HTTP error or business logic error */ public void performWaitForBuild(BuildContext context, Handle handle) throws IOException, InterruptedException { String jobName = handle.getJobName(); @@ -769,18 +752,17 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx /** * Sends a HTTP request to the API of the remote server requesting a queue item. * - * @param queueId - * the id of the remote job on the queue. - * @param context - * the context of this Builder/BuildStep. + * @param queueId the id of the remote job on the queue. + * @param context the context of this Builder/BuildStep. * @return {@link QueueItemData} the queue item data. - * @throws IOException - * if there is an error identifying the remote host, or if there is - * an error setting the authorization header, or if the request - * fails due to an unknown host, unauthorized credentials, or - * another reason, or if there is an invalid queue response. - * @throws InterruptedException - * if any thread has interrupted the current thread. + * @throws IOException if there is an error identifying the remote + * host, or if there is an error setting the + * authorization header, or if the request fails + * due to an unknown host, unauthorized + * credentials, or another reason, or if there is + * an invalid queue response. + * @throws InterruptedException if any thread has interrupted the current + * thread. */ @Nonnull private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) @@ -833,21 +815,21 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } QueueItemData queueItem = getQueueItemData(queueId, context); if (queueItem.isExecuted()) { - URL effectiveRemoteBuildURL = queueItem.getBuildURL(); - try { - URI effectiveUri = new URI(context.effectiveRemoteServer.getAddress()); - String effectiveHostname = effectiveUri.getHost(); - URL remoteURL = queueItem.getBuildURL(); - if (remoteURL != null) { - URI remoteUri = remoteURL.toURI(); - String remoteHostname = remoteUri.getHost(); - String effectiveRemoteAddress = remoteUri.toString().replaceAll(remoteHostname,effectiveHostname); - effectiveRemoteBuildURL = new URL(effectiveRemoteAddress); - } - } catch (URISyntaxException ex) { - throw new AbortException( - String.format("Unexpected syntax error: %s.", ex.toString())); - } + URL effectiveRemoteBuildURL = queueItem.getBuildURL(); + try { + URI effectiveUri = new URI(context.effectiveRemoteServer.getAddress()); + String effectiveHostname = effectiveUri.getHost(); + URL remoteURL = queueItem.getBuildURL(); + if (remoteURL != null) { + URI remoteUri = remoteURL.toURI(); + String remoteHostname = remoteUri.getHost(); + String effectiveRemoteAddress = remoteUri.toString().replaceAll(remoteHostname, + effectiveHostname); + effectiveRemoteBuildURL = new URL(effectiveRemoteAddress); + } + } catch (URISyntaxException ex) { + throw new AbortException(String.format("Unexpected syntax error: %s.", ex.toString())); + } buildInfo.setBuildData(queueItem.getBuildNumber(), effectiveRemoteBuildURL); } return buildInfo; @@ -884,15 +866,12 @@ private String getConsoleOutput(URL url, BuildContext context) throws IOExceptio * Orchestrates all calls to the remote server. Also takes care of any * credentials or failed-connection retries. * - * @param urlString - * the URL that needs to be called. - * @param context - * the context of this Builder/BuildStep. + * @param urlString the URL that needs to be called. + * @param context the context of this Builder/BuildStep. * @return JSONObject a valid JSON object, or null. - * @throws InterruptedException - * if any thread has interrupted the current thread. - * @throws IOException - * if any HTTP error occurred. + * @throws InterruptedException if any thread has interrupted the current + * thread. + * @throws IOException if any HTTP error occurred. */ public ConnectionResponse doGet(String urlString, BuildContext context) throws IOException, InterruptedException { return HttpHelper.tryGet(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), @@ -953,7 +932,7 @@ private void logConfiguration(BuildContext context, List effectiveParams context.logger.println( "################################################################################################################"); } - + public boolean isAbortTriggeredJob() { return abortTriggeredJob; } @@ -1022,8 +1001,7 @@ public String getJob() { /** * @return job value with expanded env vars. - * @throws IOException - * if there is an error replacing tokens. + * @throws IOException if there is an error replacing tokens. */ private String getJobExpanded(BasicBuildContext context) throws IOException { return TokenMacroUtils.applyTokenMacroReplacements(getJob(), context); @@ -1087,11 +1065,11 @@ public boolean isDisabled() { /** * Pokes the remote server to see if it has default parameters defined or not. * - * @param remoteJobMetadata - * from {@link #getRemoteJobMetadata(String, BuildContext)}. + * @param remoteJobMetadata from + * {@link #getRemoteJobMetadata(String, BuildContext)}. * @return true if the remote job has parameters, otherwise false. - * @throws IOException - * if it is not possible to identify if the job is parameterized. + * @throws IOException if it is not possible to identify if the job is + * parameterized. */ private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IOException { boolean isParameterized = false; From 624b905754288a836fab1c9d5cc284306657021c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:18:18 +0800 Subject: [PATCH 147/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.8 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e3b9bbe6..12b42597 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.8-SNAPSHOT + 3.0.8 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.8 From e8f07e01f771154841e449d4f6e04f42efe2596e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:18:30 +0800 Subject: [PATCH 148/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 12b42597..9b186771 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.8 + 3.0.9-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -53,7 +53,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.8 + HEAD From 216298b8d74623f173e5445e3d21d4dc73bc866d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:41:37 +0800 Subject: [PATCH 149/262] update change log --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cdd1743..655e724f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# 3.0.8 (Mar 27th, 2019) +### New feature + +* None + +### Improvement + +* Java doc refinement: Handle.getBuildStatus, Handle.updateBuildStatus (541365a0740f1e5b17f2615076249c4da33c34bc) +* Extend POST timeout & avoid re-POST after timeout (97de437b98bec1cd9d46b78047886809c1e110d2) +* Handle proxy host to avoid fail in subsequent requests (285d6573107789f3480d5a7fbc726d94a93cb917) + +### Bug fixes + +* None + + +# 3.0.7 (Dec 2nd, 2018) +### New feature + +* None + +### Improvement + +* None + +### Bug fixes + +* [JENKINS-55038](https://issues.jenkins-ci.org/browse/JENKINS-55038) + + # 3.0.6 (Sep 18th, 2018) ### New feature From a825a6c49999ae01dfc516fda4f2f6c3c15473e6 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:44:23 +0800 Subject: [PATCH 150/262] remove previous maintainer --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index 9b186771..3441583a 100644 --- a/pom.xml +++ b/pom.xml @@ -26,10 +26,6 @@ - - morficus - Maurice Williams - cashlalala KaiHsiang Chang From 68b41a7295e4dec5c5c89a41df2a92feef214dfd Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 27 Mar 2019 23:54:01 +0800 Subject: [PATCH 151/262] update the change log --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 655e724f..439ad9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ ### Improvement -* Java doc refinement: Handle.getBuildStatus, Handle.updateBuildStatus (541365a0740f1e5b17f2615076249c4da33c34bc) -* Extend POST timeout & avoid re-POST after timeout (97de437b98bec1cd9d46b78047886809c1e110d2) -* Handle proxy host to avoid fail in subsequent requests (285d6573107789f3480d5a7fbc726d94a93cb917) +* Java doc refinement: Handle.getBuildStatus, Handle.updateBuildStatus ([541365a](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/541365a0740f1e5b17f2615076249c4da33c34bc)) +* Extend POST timeout & avoid re-POST after timeout ([97de437](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/97de437b98bec1cd9d46b78047886809c1e110d2)) +* Handle proxy host to avoid fail in subsequent requests ([285d657](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/285d6573107789f3480d5a7fbc726d94a93cb917)) ### Bug fixes From a625e4b7df875a11a2e8310c2aee781e61626f06 Mon Sep 17 00:00:00 2001 From: Jeff Good Date: Tue, 21 May 2019 17:33:52 -0700 Subject: [PATCH 152/262] Stream output rather than dump it all out at the end --- .../ConnectionResponse.java | 17 +++++++ .../RemoteBuildConfiguration.java | 50 ++++++++++++------- .../utils/HttpHelper.java | 2 +- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java index ab66694e..87d2e3ed 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -21,6 +21,9 @@ public class ConnectionResponse @Nullable @CheckForNull private final JSONObject body; + @Nullable @CheckForNull + private final String rawBody; + @Nonnull private final int responseCode; @@ -29,6 +32,15 @@ public ConnectionResponse(@Nonnull Map> header, @Nullable J { this.header = header; this.body = body; + this.rawBody = null; + this.responseCode = responseCode; + } + + public ConnectionResponse(@Nonnull Map> header, @Nullable String rawBody, @Nonnull int responseCode) + { + this.header = header; + this.body = null; + this.rawBody = rawBody; this.responseCode = responseCode; } @@ -36,6 +48,7 @@ public ConnectionResponse(@Nonnull Map> header, @Nonnull in { this.header = header; this.body = null; + this.rawBody = null; this.responseCode = responseCode; } @@ -48,6 +61,10 @@ public JSONObject getBody() { return body; } + public String getRawBody() { + return rawBody; + } + public int getResponseCode() { return responseCode; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index aeffc00d..016612c4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -713,30 +713,32 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx context.logger.println("Waiting for remote build to finish ..."); } + String consoleOffset = "0"; + if (this.getEnhancedLogging()) { + context.logger.println("--------------------------------------------------------------------------------"); + context.logger.println(); + context.logger.println("Console output of remote job:"); + consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); + } while (buildInfo.isRunning()) { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + if (this.getEnhancedLogging()) { + consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); + } else { + context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + } Thread.sleep(this.pollInterval * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } + if (this.getEnhancedLogging()) { + context.logger.println("--------------------------------------------------------------------------------"); + } context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); if (context.run != null) RemoteBuildInfoExporterAction.addBuildInfoExporterAction(context.run, jobName, jobNumber, jobURL, buildInfo); - if (this.getEnhancedLogging()) { - String consoleOutput = getConsoleOutput(jobURL, context); - - context.logger.println(); - context.logger.println("Console output of remote job:"); - context.logger - .println("--------------------------------------------------------------------------------"); - context.logger.println(consoleOutput); - context.logger - .println("--------------------------------------------------------------------------------"); - } - // If build did not finish with 'success' or 'unstable' then fail build step. if (buildInfo.getResult() != Result.SUCCESS && buildInfo.getResult() != Result.UNSTABLE) { // failBuild will check if the 'shouldNotFailBuild' parameter is set or not, so @@ -856,10 +858,24 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn return buildInfo; } - private String getConsoleOutput(URL url, BuildContext context) throws IOException, InterruptedException { - URL buildUrl = new URL(url, "consoleText"); - return HttpHelper.tryGetRawResp(buildUrl.toString(), context, this.getPollInterval(), - this.getConnectionRetryLimit(), this.getAuth2(), getLock(buildUrl.toString())); + private String printOffsetConsoleOutput(BuildContext context, String offset, RemoteBuildInfo buildInfo) throws IOException, InterruptedException { + if(offset.equals("-1")) { + return "-1"; + } + String buildUrlString = String.format("%slogText/progressiveText?start=%s", buildInfo.getBuildURL(), offset); + ConnectionResponse response = doGet(buildUrlString, context); + + String rawBody = response.getRawBody(); + if(rawBody != null && !rawBody.equals("")) { + context.logger.println(rawBody); + } + + Map> header = response.getHeader(); + if(header.containsKey("X-More-Data") && header.containsKey("X-Text-Size")){ + return header.get("X-Text-Size").get(0); + } else { + return "-1"; + } } /** diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index e4c6a8e4..5773c60e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -492,7 +492,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // But in newer versions of Jenkins, it just returns an empty response. // So we need to compensate and check for both. if (responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { - return new ConnectionResponse(responseHeader, responseCode); + return new ConnectionResponse(responseHeader, response, responseCode); } else { responseObject = (JSONObject) JSONSerializer.toJSON(response); } From 08ec3ec779839716fd080f93de03a9432f6c05fe Mon Sep 17 00:00:00 2001 From: rezaizad Date: Wed, 29 May 2019 15:23:28 +0200 Subject: [PATCH 153/262] Added the ability to trust untrusted certificates Added - A "NaiveTrustManager", which allows to bypass certificate checks -> Enables us to accept any certificate - TrustAllCertificates option for Jenkins Admins, adding new remote hosts to jenkins Refactored - Better error handling in RemoteJenkinsServer --- .../RemoteJenkinsServer.java | 58 ++++++++++++++++--- .../utils/HttpHelper.java | 54 +++++++++++++---- .../utils/NaiveTrustManager.java | 18 ++++++ .../help-trustAllCertificates.html | 21 +++++++ .../RemoteJenkinsServer/config.jelly | 3 + .../help-trustAllCertificates.html | 16 +++++ 6 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index f02d1564..e07959ec 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -3,17 +3,19 @@ import static org.apache.commons.lang.StringUtils.trimToEmpty; import java.io.Serializable; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; +import javax.net.ssl.*; import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.List; import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.NaiveTrustManager; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; @@ -50,6 +52,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl { @@ -153,6 +162,28 @@ public String getDisplayName() { return ""; } + /** + * Sets the TrustManager to be a "NaiveTrustManager", allowing us to ignore untrusted certificates + * Will set the connection to null, if a key management error occurred. + * + * ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! + * @param conn The HttpsURLConnection you want to modify. + * @param trustAllCertificates A boolean, gotten from the Remote Hosts description + */ + public void makeConnectionTrustAllCertificates(HttpsURLConnection conn, boolean trustAllCertificates) + throws NoSuchAlgorithmException, KeyManagementException { + if (trustAllCertificates) { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); + // SSLContext.setDefault(ctx); + conn.setSSLSocketFactory(ctx.getSocketFactory()); + + // Trust every hostname + HostnameVerifier allHostsValid = (hostname, session) -> true; + conn.setHostnameVerifier(allHostsValid); + } + } + /** * Validates the given address to see that it's well-formed, and is reachable. * @@ -161,7 +192,7 @@ public String getDisplayName() { * @return FormValidation object */ @Restricted(NoExternalUse.class) - public FormValidation doCheckAddress(@QueryParameter String address) { + public FormValidation doCheckAddress(@QueryParameter String address, @QueryParameter boolean trustAllCertificates) { URL host = null; @@ -180,11 +211,22 @@ public FormValidation doCheckAddress(@QueryParameter String address) { // check that the host is reachable try { - HttpURLConnection connection = (HttpURLConnection) host.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); + HttpsURLConnection conn = (HttpsURLConnection) host.openConnection(); + try { + makeConnectionTrustAllCertificates(conn, trustAllCertificates); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + return FormValidation.error(e, "A key management error occurred."); + } + conn.setConnectTimeout(5000); + conn.connect(); + + if (trustAllCertificates) { + return FormValidation.warning( + "Connection established! Accepting all certificates is potentially unsafe." + ); + } } catch (Exception e) { - return FormValidation.warning("Address looks good, but a connection could not be stablished."); + return FormValidation.warning("Address looks good, but a connection could not be established."); } return FormValidation.ok(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 5773c60e..8ce67685 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -12,12 +12,15 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; +import javax.net.ssl.*; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; @@ -186,7 +189,7 @@ public static String encodeValue(String dirtyValue) { return cleanValue; } - private static String readInputStream(HttpURLConnection connection) throws IOException { + private static String readInputStream(HttpsURLConnection connection) throws IOException { BufferedReader rd = null; try { @@ -243,7 +246,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b context.logger.println("reuse cached crumb: " + globalHost); return jenkinsCrumb; } - HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); + HttpsURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { throw new UnauthorizedException(crumbProviderUrl); @@ -277,7 +280,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b * @param context * @throws IOException */ - private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth, + private static void addCrumbToConnection(HttpsURLConnection connection, BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) throws IOException { String method = connection.getRequestMethod(); if (method != null && method.equalsIgnoreCase("POST")) { @@ -288,7 +291,19 @@ private static void addCrumbToConnection(HttpURLConnection connection, BuildCont } } - private static HttpURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) + /** + * Returns an authorized HttpsURLConnection + * If the user wanted to trust all certificates, the TrustManager and HostVerifier of the connection + * will be set properly. + * + * ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! + * @param context The build context + * @param url The url to the remote build + * @param overrideAuth + * @return An authorized connection with or without a NaiveTrustManager + * @throws IOException + */ + private static HttpsURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) throws IOException { URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); @@ -302,8 +317,24 @@ private static HttpURLConnection getAuthorizedConnection(BuildContext context, U // Set Authorization Header configured globally for remoteServer serverAuth.setAuthorizationHeader(connection, context); } + HttpsURLConnection conn = (HttpsURLConnection) connection; - return (HttpURLConnection) connection; + if (context.effectiveRemoteServer.getTrustAllCertificates()){ + // Installing the naive manage + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); + // SSLContext.setDefault(ctx); + conn.setSSLSocketFactory(ctx.getSocketFactory()); + + // Trust every hostname + HostnameVerifier allHostsValid = (hostname, session) -> true; + conn.setHostnameVerifier(allHostsValid); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + context.logger.println("Unable to trust all certificates."); + } + } + return conn; } private static String getUrlWithoutParameters(String url) { @@ -387,7 +418,6 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * of a failed connection, the method calls it self recursively and increments * the number of attempts. * - * @see sendHTTPCall * @param urlString * the URL that needs to be called. * @param requestType @@ -429,7 +459,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } URL url = new URL(urlString); - HttpURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); + HttpsURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); try { conn.setDoInput(true); @@ -440,7 +470,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); if (HTTP_POST.equalsIgnoreCase(requestType)) { - // use longer timeout during POST due to not performing retrys since POST is not idem-potent + // use longer timeout during POST due to not performing retries since POST is not idem-potent conn.setReadTimeout(30000); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); @@ -491,7 +521,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // non-parameterized jobs), it returns a string indicating the status. // But in newer versions of Jenkins, it just returns an empty response. // So we need to compensate and check for both. - if (responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) { + if (responseCode >= 400 || !JSONUtils.mayBeJSON(response)) { return new ConnectionResponse(responseHeader, response, responseCode); } else { responseObject = (JSONObject) JSONSerializer.toJSON(response); @@ -509,12 +539,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // If we have connectionRetryLimit set to > 0 then retry that many times. if (numberOfAttempts <= retryLimit) { context.logger.println(String.format( - "Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s, parameters: %s", + "Connection to remote server failed %s, waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s", (responseCode == 0 ? "" : "[" + responseCode + "]"), pollInterval, getUrlWithoutParameters(urlString), parmsString)); // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds + // Sleep takes milliseconds so need to convert this.pollInterval to milliseconds // (x 1000) try { // Could do with a better way of sleeping... diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java new file mode 100644 index 00000000..84de9ab9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/NaiveTrustManager.java @@ -0,0 +1,18 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import javax.net.ssl.*; +import java.security.cert.X509Certificate; + +// Trust every server +public class NaiveTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html new file mode 100644 index 00000000..8cdf8413 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-trustAllCertificates.html @@ -0,0 +1,21 @@ +
+
+ Trust all certificates +
+ +

+ It is possible to override/rewrite the 'Trust all certificate'-setting for each Job separately. + Setting this checkbox to 'true' will result in accepting all certificates for the given Job. +

+ +
+ If your remote Jenkins host has a + + self-signed certificate + + or its certificate is not trusted, you may want to enable this option. + It will accept untrusted certificates for the given host. +
+ +

This is unsafe and should only be used for testing or if you trust the host.

+
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index f5300952..e7313a4c 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -8,6 +8,9 @@ + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html new file mode 100644 index 00000000..caf73c61 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-trustAllCertificates.html @@ -0,0 +1,16 @@ +
+
+ Trust all certificates +
+ +
+ If your remote Jenkins host has a + + self-signed certificate + + or its certificate is not trusted, you may want to enable this option. + It will accept untrusted certificates for the given host. +
+ +

This is unsafe and should only be used for testing or if you trust the host.

+
\ No newline at end of file From 0ae58d7e6ac79237e6741dd57b0cbc28afb837dc Mon Sep 17 00:00:00 2001 From: rezaizad Date: Wed, 12 Jun 2019 11:54:41 +0200 Subject: [PATCH 154/262] Added per Build Configuration to trust all certificates Added better error handling on SSLHandshakeExceptions - Will not try to reconnect X (default: 5) times - Stops the build process, if an SSLHandshakeExceptions was thrown Overwriting global configuration of 'TrustAllCertificates' - Jelly's Optionalblock used - Allows to only change the 'TrustAllCertificates'-Setting, if wanted by the user It is now possible to change the globally set 'trustAllCertificates' parameter in Pipeline and normal project level, if wanted. Further additions: - Small addition to unit tests to create HttpsUrlConnections instead of HttpUrlConnections --- .../RemoteBuildConfiguration.java | 38 ++++++++- .../RemoteJenkinsServer.java | 10 ++- .../pipeline/RemoteBuildPipelineStep.java | 18 +++++ .../utils/HttpHelper.java | 77 +++++++++---------- .../RemoteBuildConfiguration/config.jelly | 10 ++- .../RemoteJenkinsServer/config.jelly | 3 +- .../RemoteBuildPipelineStep/config.jelly | 8 ++ .../help-trustAllCertificates.html | 21 +++++ .../RemoteBuildConfigurationTest.java | 42 ++++++++++ 9 files changed, 181 insertions(+), 46 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-trustAllCertificates.html diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 016612c4..7dd478fa 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -103,6 +103,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private String remoteJenkinsUrl; private Auth2 auth2; private boolean shouldNotFailBuild; + private boolean trustAllCertificates; + private boolean overrideTrustAllCertificates; private boolean preventRemoteBuildQueue; private int pollInterval; private boolean blockBuildUntilComplete; @@ -152,6 +154,16 @@ protected Object readResolve() { return this; } + @DataBoundSetter + public void setTrustAllCertificates(boolean trustAllCertificates) { + this.trustAllCertificates = trustAllCertificates; + } + + @DataBoundSetter + public void setOverrideTrustAllCertificates(boolean overrideTrustAllCertificates) { + this.overrideTrustAllCertificates = overrideTrustAllCertificates; + } + @DataBoundSetter public void setAbortTriggeredJob(boolean abortTriggeredJob) { this.abortTriggeredJob = abortTriggeredJob; @@ -422,6 +434,10 @@ public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context } } + if (this.overrideTrustAllCertificates) { + server.setTrustAllCertificates(this.trustAllCertificates); + } + return server; } @@ -853,8 +869,7 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } else { context.logger.println("WARNING: Unhandled condition!"); } - } catch (Exception ex) { - } + } catch (Exception ignored) {} return buildInfo; } @@ -917,6 +932,8 @@ private void logConfiguration(BuildContext context, List effectiveParams String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; String _remoteJenkinsName = getRemoteJenkinsName(); String _remoteJenkinsUrl = getRemoteJenkinsUrl(); + boolean _trustAllCertificates = context.effectiveRemoteServer.getTrustAllCertificates(); + Auth2 _auth = getAuth2(); int _connectionRetryLimit = getConnectionRetryLimit(); boolean _blockBuildUntilComplete = getBlockBuildUntilComplete(); @@ -945,6 +962,7 @@ private void logConfiguration(BuildContext context, List effectiveParams } context.logger.println(String.format(" - blockBuildUntilComplete: %s", _blockBuildUntilComplete)); context.logger.println(String.format(" - connectionRetryLimit: %s", _connectionRetryLimit)); + context.logger.println(String.format(" - trustAllCertificates: %s", _trustAllCertificates)); context.logger.println( "################################################################################################################"); } @@ -1153,6 +1171,14 @@ public boolean isUseJobInfoCache() { return useJobInfoCache; } + public boolean getTrustAllCertificates() { + return trustAllCertificates; + } + + public boolean getOverrideTrustAllCertificates() { + return overrideTrustAllCertificates; + } + // This indicates to Jenkins that this is an implementation of an extension // point. @Extension @@ -1220,6 +1246,14 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc return super.configure(req, formData); } + @Restricted(NoExternalUse.class) + public FormValidation doCheckTrustAllCertificates(@QueryParameter("trustAllCertificates") final boolean value){ + if (value) { + return FormValidation.warning("Accepting all certificates is potentially unsafe."); + } + return FormValidation.ok(); + } + @Restricted(NoExternalUse.class) public FormValidation doCheckJob(@QueryParameter("job") final String value, @QueryParameter("remoteJenkinsUrl") final String remoteJenkinsUrl, diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index e07959ec..661990f3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -53,6 +53,8 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl { @@ -175,7 +184,6 @@ public void makeConnectionTrustAllCertificates(HttpsURLConnection conn, boolean if (trustAllCertificates) { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); - // SSLContext.setDefault(ctx); conn.setSSLSocketFactory(ctx.getSocketFactory()); // Trust every hostname diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 5e5bfb80..50c8bce8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -103,6 +103,16 @@ public void setShouldNotFailBuild(boolean shouldNotFailBuild) { remoteBuildConfig.setShouldNotFailBuild(shouldNotFailBuild); } + @DataBoundSetter + public void setTrustAllCertificates(boolean trustAllCertificates) { + remoteBuildConfig.setTrustAllCertificates(trustAllCertificates); + } + + @DataBoundSetter + public void setOverrideTrustAllCertificates(boolean overrideTrustAllCertificates) { + remoteBuildConfig.setOverrideTrustAllCertificates(overrideTrustAllCertificates); + } + @DataBoundSetter public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { remoteBuildConfig.setPreventRemoteBuildQueue(preventRemoteBuildQueue); @@ -290,6 +300,14 @@ public boolean getShouldNotFailBuild() { return remoteBuildConfig.getShouldNotFailBuild(); } + public boolean getTrustAllCertificates() { + return remoteBuildConfig.getTrustAllCertificates(); + } + + public boolean getOverrideTrustAllCertificates() { + return remoteBuildConfig.getOverrideTrustAllCertificates(); + } + public boolean getPreventRemoteBuildQueue() { return remoteBuildConfig.getPreventRemoteBuildQueue(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 8ce67685..b432f035 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -13,11 +13,8 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import javax.net.ssl.*; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; +import java.net.*; +import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -189,7 +186,7 @@ public static String encodeValue(String dirtyValue) { return cleanValue; } - private static String readInputStream(HttpsURLConnection connection) throws IOException { + private static String readInputStream(HttpURLConnection connection) throws IOException { BufferedReader rd = null; try { @@ -201,7 +198,7 @@ private static String readInputStream(HttpsURLConnection connection) throws IOEx is = connection.getErrorStream(); } - rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); + rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); String line; StringBuilder response = new StringBuilder(); while ((line = rd.readLine()) != null) { @@ -246,7 +243,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b context.logger.println("reuse cached crumb: " + globalHost); return jenkinsCrumb; } - HttpsURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); + HttpURLConnection connection = (HttpURLConnection) getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { throw new UnauthorizedException(crumbProviderUrl); @@ -280,7 +277,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b * @param context * @throws IOException */ - private static void addCrumbToConnection(HttpsURLConnection connection, BuildContext context, Auth2 overrideAuth, + private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) throws IOException { String method = connection.getRequestMethod(); if (method != null && method.equalsIgnoreCase("POST")) { @@ -292,18 +289,18 @@ private static void addCrumbToConnection(HttpsURLConnection connection, BuildCon } /** - * Returns an authorized HttpsURLConnection + * Returns a URLConnection which can be casted to HttpUrlConnection or HttpsUrlConnection * If the user wanted to trust all certificates, the TrustManager and HostVerifier of the connection * will be set properly. * - * ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! + * ATTENTION: TRUSTING ALL CERTIFICATES IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! * @param context The build context * @param url The url to the remote build * @param overrideAuth * @return An authorized connection with or without a NaiveTrustManager * @throws IOException */ - private static HttpsURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) + private static URLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) throws IOException { URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); @@ -317,24 +314,26 @@ private static HttpsURLConnection getAuthorizedConnection(BuildContext context, // Set Authorization Header configured globally for remoteServer serverAuth.setAuthorizationHeader(connection, context); } - HttpsURLConnection conn = (HttpsURLConnection) connection; - if (context.effectiveRemoteServer.getTrustAllCertificates()){ - // Installing the naive manage - try { - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); - // SSLContext.setDefault(ctx); - conn.setSSLSocketFactory(ctx.getSocketFactory()); - - // Trust every hostname - HostnameVerifier allHostsValid = (hostname, session) -> true; - conn.setHostnameVerifier(allHostsValid); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - context.logger.println("Unable to trust all certificates."); + if (connection instanceof HttpsURLConnection) { + HttpsURLConnection conn = (HttpsURLConnection) connection; + if (context.effectiveRemoteServer.getTrustAllCertificates()) { + // Installing the naive manage + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom()); + conn.setSSLSocketFactory(ctx.getSocketFactory()); + + // Trust every hostname + HostnameVerifier allHostsValid = (hostname, session) -> true; + conn.setHostnameVerifier(allHostsValid); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + context.logger.println("Unable to trust all certificates."); + } } + return conn; } - return conn; + return connection; } private static String getUrlWithoutParameters(String url) { @@ -375,7 +374,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, String query = ""; if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { - // start building the proper URL based on known capabiltiies of the remote + // start building the proper URL based on known capabilities of the remote // server if (context.effectiveRemoteServer.getAddress() == null) { throw new AbortException( @@ -455,11 +454,11 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT String parmsString = ""; if (HTTP_POST.equalsIgnoreCase(requestType) && postParams != null && postParams.size() > 0) { parmsString = buildUrlQueryString(postParams); - postDataBytes = parmsString.getBytes("UTF-8"); + postDataBytes = parmsString.getBytes(StandardCharsets.UTF_8); } URL url = new URL(urlString); - HttpsURLConnection conn = getAuthorizedConnection(context, url, overrideAuth); + HttpURLConnection conn = (HttpURLConnection) getAuthorizedConnection(context, url, overrideAuth); try { conn.setDoInput(true); @@ -528,6 +527,10 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } } + } catch (SSLHandshakeException handshakeException) { + context.logger.println("An SSLHandshakeException occured. The certificate might not be trusted!\n" + + "Set 'Trust all certificates' and try again, if you want to accept untrusted certificates.\n"); + throw handshakeException; } catch (IOException e) { // E.g. "HTTP/1.1 403 No valid crumb was included in the request" @@ -546,25 +549,17 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // Sleep for 'pollInterval' seconds. // Sleep takes milliseconds so need to convert this.pollInterval to milliseconds // (x 1000) - try { - // Could do with a better way of sleeping... - Thread.sleep(pollInterval * 1000); - } catch (InterruptedException ex) { - throw ex; - } + // Could do with a better way of sleeping... + Thread.sleep(pollInterval * 1000); context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); numberOfAttempts++; return sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); - } else if (numberOfAttempts > retryLimit) { + } else { // reached the maximum number of retries, time to fail throw new ExceedRetryLimitException(); - } else { - // something failed with the connection and we retried the max amount of - // times... so throw an exception to mark the build as failed. - throw e; } } finally { diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index b4753230..7945f8d7 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -14,10 +14,11 @@ + - + @@ -62,6 +63,13 @@ + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly index e7313a4c..219bca69 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/config.jelly @@ -9,9 +9,10 @@ - + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 25b919b6..cff99dd2 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -15,6 +15,7 @@ + @@ -63,6 +64,13 @@ + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-trustAllCertificates.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-trustAllCertificates.html new file mode 100644 index 00000000..8cdf8413 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-trustAllCertificates.html @@ -0,0 +1,21 @@ +
+
+ Trust all certificates +
+ +

+ It is possible to override/rewrite the 'Trust all certificate'-setting for each Job separately. + Setting this checkbox to 'true' will result in accepting all certificates for the given Job. +

+ +
+ If your remote Jenkins host has a + + self-signed certificate + + or its certificate is not trusted, you may want to enable this option. + It will accept untrusted certificates for the given host. +
+ +

This is unsafe and should only be used for testing or if you trust the host.

+
\ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index a0d1cc02..356679d5 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -116,6 +116,7 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle configuration.setUseCrumbCache(false); configuration.setUseJobInfoCache(false); configuration.setEnhancedLogging(true); + configuration.setTrustAllCertificates(true); if (withParam){ String parmString = ""; for (Map.Entry p : parms.entrySet()) { @@ -179,6 +180,8 @@ public void testDefaults() throws IOException { assertEquals(false, config.getPreventRemoteBuildQueue()); assertEquals(null, config.getRemoteJenkinsName()); assertEquals(false, config.getShouldNotFailBuild()); + assertEquals(false, config.getOverrideTrustAllCertificates()); + assertEquals(false, config.getTrustAllCertificates()); assertEquals("", config.getToken()); } @@ -198,6 +201,8 @@ public void testDefaultsPipelineStep() throws IOException { assertEquals(false, config.getPreventRemoteBuildQueue()); assertEquals(null, config.getRemoteJenkinsName()); assertEquals(false, config.getShouldNotFailBuild()); + assertEquals(false, config.getOverrideTrustAllCertificates()); + assertEquals(false, config.getTrustAllCertificates()); assertEquals("", config.getToken()); } @@ -305,6 +310,27 @@ public void testRemoteUrlOverridesRemoteName() throws IOException { assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); } + /** + * Testing if it is possible to set the TrustAllCertificates-parameter with OverrideTrustAllCertificates set to true + * + * @throws IOException + */ + @Test @WithoutJenkins + public void testRemoteOverridesTrustAllCertificates() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setTrustAllCertificates(false); + config.setOverrideTrustAllCertificates(true); + + config = mockGlobalRemoteHost(config, + "remoteJenkinsName", + "http://globallyConfigured:8080", + true); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertTrue(config.getTrustAllCertificates()); + } + @Test @WithoutJenkins public void testEvaluateEffectiveRemoteHost_jobNameMissing() throws IOException { RemoteBuildConfiguration config = new RemoteBuildConfiguration(); @@ -387,6 +413,22 @@ private RemoteBuildConfiguration mockGlobalRemoteHost(RemoteBuildConfiguration c return spy; } + private RemoteBuildConfiguration mockGlobalRemoteHost( + RemoteBuildConfiguration config, String remoteName, String remoteUrl, boolean trustAllCertificates + ) throws MalformedURLException { + RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); + jenkinsServer.setDisplayName(remoteName); + jenkinsServer.setAddress(remoteUrl); + + RemoteBuildConfiguration spy = spy(config); + DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); + descriptor.setRemoteSites(jenkinsServer); + doReturn(descriptor).when(spy).getDescriptor(); + spy.setTrustAllCertificates(trustAllCertificates); + + return spy; + } + @Test @WithoutJenkins public void testRemoveTrailingSlashes() { assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx")); From 9415643814cbe68ff7069bb4276c8cb5bc589b9d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 17 Aug 2019 11:04:43 +0800 Subject: [PATCH 155/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.0.9 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3441583a..6f3dbb8c 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.9-SNAPSHOT + 3.0.9 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.0.9 From 7845587a6d6690b6e416c62317fb68993d678a1d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 17 Aug 2019 11:04:55 +0800 Subject: [PATCH 156/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6f3dbb8c..4d53a6e7 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.9 + 3.0.10-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.0.9 + HEAD From 01142960a21b0a920ac1366e61819edbb875b770 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 17 Aug 2019 11:16:35 +0800 Subject: [PATCH 157/262] update the change log for 3.0.9 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439ad9b3..70d000de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 3.0.9 (Aug 17th, 2019) +### New feature + +* None + +### Improvement + +* Stream output rather than dump it all out at the end ([a625e4b](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/a625e4b7df875a11a2e8310c2aee781e61626f06)) +* Added the ability to trust untrusted certificates ([08ec3ec](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/08ec3ec779839716fd080f93de03a9432f6c05fe)) + +### Bug fixes + +* None + + # 3.0.8 (Mar 27th, 2019) ### New feature From f0b36fa13900da889ebe846d6daeb978e6d7d5f6 Mon Sep 17 00:00:00 2001 From: Hui Jun Ng Date: Fri, 13 Sep 2019 14:22:49 +0800 Subject: [PATCH 158/262] added condition to handle views --- .../RemoteBuildConfiguration.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 7dd478fa..2e1b3f7e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import static java.lang.Math.min; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; @@ -510,7 +511,12 @@ private String getRootUrlFromJobUrl(String jobUrl) throws MalformedURLException if (isEmpty(jobUrl)) return null; if (FormValidationUtils.isURL(jobUrl)) { - int index = jobUrl.indexOf("/job/"); + int index; + if (jobUrl.contains("/view/")) { + index = min(jobUrl.indexOf("/view/"), jobUrl.indexOf("/job/")); + } else { + index = jobUrl.indexOf("/job/"); + } if (index < 0) throw new MalformedURLException("Expected '/job/' element in job URL but was: " + jobUrl); return jobUrl.substring(0, index); From f623d1217921d7ec52052b66fd6644aa77d58790 Mon Sep 17 00:00:00 2001 From: Hui Jun Ng Date: Fri, 13 Sep 2019 14:23:14 +0800 Subject: [PATCH 159/262] tidied docstrings --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 2e1b3f7e..6943aec9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -289,7 +289,7 @@ public List getParameterList(BuildContext context) { * Reads a file from the jobs workspace, and loads the list of parameters from * with in it. It will also call ```getCleanedParameters``` before returning. * - * @param build + * @param BuildContext context * @return List of build parameters */ private List loadExternalParameterFile(BuildContext context) { @@ -337,7 +337,7 @@ private void removeEmptyElements(Collection collection) { * that no type of character encoding is happening at this step. All encoding * happens in the "buildUrlQueryString" method. * - * @param List parameters + * @param List parameters * @return List of build parameters */ private List getCleanedParameters(List parameters) { From b644a09841fa7d1cb3b6ec0e610ebc2167d5f5aa Mon Sep 17 00:00:00 2001 From: Hui Jun Ng Date: Fri, 13 Sep 2019 14:23:53 +0800 Subject: [PATCH 160/262] added test on build with view --- .../RemoteBuildConfigurationTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 356679d5..c020cb9f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; +import hudson.model.*; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; @@ -520,4 +521,15 @@ public void testRemoteBuildWith5KByteString() throws Exception { _testRemoteBuild(true, true, remoteProject, parms); } + @Test + public void testRemoteViewBuild() throws Exception { + disableAuth(); + + FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject("test-job"); + ListView view = new ListView("test-view", jenkinsRule.getInstance()); + view.add(remoteProject); + jenkinsRule.getInstance().addView(view); + _testRemoteBuild(false, false, remoteProject); + } + } From ca4956a9642b7c686fefd2d0ab9af392a46b868b Mon Sep 17 00:00:00 2001 From: Hui Jun Ng Date: Fri, 13 Sep 2019 14:40:20 +0800 Subject: [PATCH 161/262] narrowed down import to only required --- .../RemoteBuildConfigurationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index c020cb9f..97b9677f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; -import hudson.model.*; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; @@ -37,6 +36,7 @@ import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; import hudson.model.User; +import hudson.model.ListView; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.SecurityRealm; import hudson.security.AuthorizationStrategy.Unsecured; From 6d14e6b49bc9d2692e91fa52e42300c77fe4712f Mon Sep 17 00:00:00 2001 From: Daniel Beck Date: Tue, 24 Sep 2019 12:15:12 +0200 Subject: [PATCH 162/262] Use HTTPS URLs in pom.xml --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4d53a6e7..d473b7b8 100644 --- a/pom.xml +++ b/pom.xml @@ -55,14 +55,14 @@ repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ From b44fb12031a8c0db4499b92a54ef37834ac63233 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 2 Oct 2019 17:24:09 +0800 Subject: [PATCH 163/262] Fix http 400 on Apache Tomcat/8.5 (https://tomcat.apache.org/tomcat-8.5-doc/config/http.html, relaxedQueryChars) ** The job information inquiry will cause "java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986". ** If you don't update the plugin, a workaround solution on Tomcat would be like --- .../RemoteBuildConfiguration.java | 25 +++++++++++-------- .../utils/HttpHelper.java | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 6943aec9..55695524 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -737,7 +737,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx String consoleOffset = "0"; if (this.getEnhancedLogging()) { - context.logger.println("--------------------------------------------------------------------------------"); + context.logger + .println("--------------------------------------------------------------------------------"); context.logger.println(); context.logger.println("Console output of remote job:"); consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); @@ -753,7 +754,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx handle.setBuildInfo(buildInfo); } if (this.getEnhancedLogging()) { - context.logger.println("--------------------------------------------------------------------------------"); + context.logger + .println("--------------------------------------------------------------------------------"); } context.logger.println("Remote build finished with status " + buildInfo.getResult().toString() + "."); @@ -875,24 +877,26 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } else { context.logger.println("WARNING: Unhandled condition!"); } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } return buildInfo; } - private String printOffsetConsoleOutput(BuildContext context, String offset, RemoteBuildInfo buildInfo) throws IOException, InterruptedException { - if(offset.equals("-1")) { + private String printOffsetConsoleOutput(BuildContext context, String offset, RemoteBuildInfo buildInfo) + throws IOException, InterruptedException { + if (offset.equals("-1")) { return "-1"; } String buildUrlString = String.format("%slogText/progressiveText?start=%s", buildInfo.getBuildURL(), offset); ConnectionResponse response = doGet(buildUrlString, context); String rawBody = response.getRawBody(); - if(rawBody != null && !rawBody.equals("")) { + if (rawBody != null && !rawBody.equals("")) { context.logger.println(rawBody); } - Map> header = response.getHeader(); - if(header.containsKey("X-More-Data") && header.containsKey("X-Text-Size")){ + Map> header = response.getHeader(); + if (header.containsKey("X-More-Data") && header.containsKey("X-Text-Size")) { return header.get("X-Text-Size").get(0); } else { return "-1"; @@ -1079,7 +1083,8 @@ public boolean isDisabled() { throws IOException, InterruptedException { String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - remoteJobUrl += "/api/json?tree=actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url"; + remoteJobUrl += "/api/json?" + HttpHelper.buildUrlQueryString(Arrays.asList( + "tree=actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url")); JSONObject jsonObject = DropCachePeriodicWork.safeGetJobInfo(remoteJobUrl, isUseJobInfoCache()); if (jsonObject != null) { @@ -1253,7 +1258,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc } @Restricted(NoExternalUse.class) - public FormValidation doCheckTrustAllCertificates(@QueryParameter("trustAllCertificates") final boolean value){ + public FormValidation doCheckTrustAllCertificates(@QueryParameter("trustAllCertificates") final boolean value) { if (value) { return FormValidation.warning("Accepting all certificates is potentially unsafe."); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index b432f035..21c04acd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -82,7 +82,7 @@ private static String addToQueryString(String queryString, String item) { * the parameters needed to trigger the remote job. * @return query-parameter-formated URL-encoded string. */ - private static String buildUrlQueryString(Collection parameters) { + public static String buildUrlQueryString(Collection parameters) { // List to hold the encoded parameters List encodedParameters = new ArrayList(); From 0b98fd97123d04b6c9b733604eb9e87f7304b965 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 3 Oct 2019 21:45:34 +0800 Subject: [PATCH 164/262] fix java doc error --- .../plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 21c04acd..9b97a4de 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -76,7 +76,7 @@ private static String addToQueryString(String queryString, String item) { } /** - * Return the Collection in an encoded query-string. + * Return the Collection<String> in an encoded query-string. * * @param parameters * the parameters needed to trigger the remote job. From c5e61eaa9c544dde6404848c1b0ea696d9f7188f Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 3 Oct 2019 22:02:50 +0800 Subject: [PATCH 165/262] update change log for 3.1.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d000de..a6a10399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# 3.1.0 (Oct 3rd, 2019) +### New feature + +* None + +### Improvement + +* Support "view" in job's URL ([f0b36fa](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f0b36fa13900da889ebe846d6daeb978e6d7d5f6)) + +### Bug fixes + +* [JENKINS-59456](https://issues.jenkins-ci.org/browse/JENKINS-59456) + + # 3.0.9 (Aug 17th, 2019) ### New feature From df5be3e1e41e6629b9632ee402b1562c459b020d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 3 Oct 2019 22:08:54 +0800 Subject: [PATCH 166/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d473b7b8..9daff449 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.0.10-SNAPSHOT + 3.1.0 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.0 From f431ad2269bd081a538c675663e4389b89badd8e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 3 Oct 2019 22:09:06 +0800 Subject: [PATCH 167/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9daff449..10adc0ab 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.0 + 3.1.1-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.0 + HEAD From 5cd9cb3316b050c0b378821de6167272b0363f98 Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Fri, 4 Oct 2019 18:26:39 +0800 Subject: [PATCH 168/262] Bump pom and set appropriate min jenkins version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 10adc0ab..82a8198c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,11 @@ org.jenkins-ci.plugins plugin - 3.5 + 3.50 - 1.642.3 + 2.60.3 8 From b4030f84cc2ab91f8a3d2883fd34cd81a4b4746a Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Fri, 4 Oct 2019 18:26:52 +0800 Subject: [PATCH 169/262] Mark fields transient --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 55695524..fd2f4f12 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -121,7 +121,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean abortTriggeredJob; private boolean disabled; - private Map hostLocks = new HashMap<>(); + private transient Map hostLocks = new HashMap<>(); private Map hostPermits = new HashMap<>(); private static Logger logger = Logger.getLogger(RemoteBuildConfiguration.class.getName()); From d593c5e3df21dd89ca9d9bea266098a509e391fe Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Sat, 5 Oct 2019 13:56:05 +0800 Subject: [PATCH 170/262] Revert jenkins version change --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 82a8198c..41d133d3 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ - 2.60.3 + 1.642.3 8 From d0f2739a6bf1387a1b9189390bb3aa5f3974608a Mon Sep 17 00:00:00 2001 From: Helena Butow Date: Thu, 31 Oct 2019 16:41:52 -0700 Subject: [PATCH 171/262] If console output looks like it might be JSON but it turns out it isn't, return the ConnectionResponse --- .../utils/HttpHelper.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 9b97a4de..933b99cc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -47,6 +47,7 @@ import hudson.AbortException; import hudson.ProxyConfiguration; +import net.sf.json.JSONException; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; import net.sf.json.util.JSONUtils; @@ -123,7 +124,7 @@ public static String buildUrlQueryString(Collection parameters) { /** * Same as above, but takes in to consideration if the remote server has any * default parameters set or not - * + * * @param isRemoteJobParameterized * Boolean indicating if the remote job is parameterized or not * @return A string which represents a portion of the build URL @@ -410,7 +411,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, return triggerUrlString; } - + /** * Same as sendHTTPCall, but keeps track of the number of failed connection * attempts (aka: the number of times this method has been called). In the case @@ -440,7 +441,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * all the possibilities of HTTP exceptions * @throws InterruptedException * if any thread has interrupted the current thread. - * + * */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, @@ -523,7 +524,13 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT if (responseCode >= 400 || !JSONUtils.mayBeJSON(response)) { return new ConnectionResponse(responseHeader, response, responseCode); } else { - responseObject = (JSONObject) JSONSerializer.toJSON(response); + try { + responseObject = (JSONObject) JSONSerializer.toJSON(response); + } + catch(JSONException e) { + // despite JSONUtils.mayBeJSON believing that this might be JSON, it looks like it wasn't + return new ConnectionResponse(responseHeader, response, responseCode); + } } } From 25de4bc514fb773b26eac2199d9eccf3242c71d6 Mon Sep 17 00:00:00 2001 From: Brandon Squizzato Date: Fri, 12 Apr 2019 16:20:28 -0400 Subject: [PATCH 172/262] Add BearerTokenAuth --- .../auth2/BearerTokenAuth.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java new file mode 100644 index 00000000..eeb1c28d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -0,0 +1,94 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2; + +import java.io.IOException; +import java.net.URLConnection; + +import org.jenkinsci.Symbol; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; +import hudson.model.Item; + + +public class BearerTokenAuth extends Auth2 { + + //private static final long serialVersionUID = 051380141338287L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new BearerTokenAuthDescriptor(); + + private String token; + + @DataBoundConstructor + public BearerTokenAuth() { + this.token = null; + } + + @DataBoundSetter + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return this.token; + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + connection.setRequestProperty("Authorization", "Bearer: " + getToken()); + } + + @Override + public String toString() { + return "'" + getDescriptor().getDisplayName() + "'"; + } + + @Override + public String toString(Item item) { + return toString(); + } + + @Override + public Auth2Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("BearerTokenAuth") + public static class BearerTokenAuthDescriptor extends Auth2Descriptor { + @Override + public String getDisplayName() { + return "Bearer Token Authentication"; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((token == null) ? 0 : token.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!this.getClass().isInstance(obj)) + return false; + BearerTokenAuth other = (BearerTokenAuth) obj; + if (token == null) { + if (other.token != null) + return false; + } else if (!token.equals(other.token)) { + return false; + } + return true; + } + +} From 91872ca0f7dba8112e34f6e1256a5023f7e04add Mon Sep 17 00:00:00 2001 From: Brandon Squizzato Date: Thu, 2 Jan 2020 14:06:07 -0500 Subject: [PATCH 173/262] Update README, add test, generate serialVersionUID --- README_PipelineConfiguration.md | 3 +++ .../auth2/BearerTokenAuth.java | 13 +++++-------- .../auth2/Auth2Test.java | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md index 7854ed42..8945c3e8 100644 --- a/README_PipelineConfiguration.md +++ b/README_PipelineConfiguration.md @@ -74,6 +74,9 @@ Authentication can be configured globally in the system configuration or set/ove The following authentication types are available: - **Token Authentication** The specified user id and Jenkins API token is used.
```auth: TokenAuth(apiToken: '', userName: '')``` +- **Bearer Token Authentication** The specified token is inserted to a "Authentication: Bearer" header in REST API requests.
+ This is useful when the Jenkins deployment is fronted by a token authentication mechanism (such as when running on Red Hat OpenShift)
+ ```auth: BearerTokenAuth(token: '')``` - **Credentials Authentication** The specified Jenkins Credentials are used. This can be either user/password or user/API Token.
```auth: CredentialsAuth(credentials: '')``` - **No Authentication** No Authorization header will be sent, independent of the global 'remote host' settings.
diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java index eeb1c28d..985f3af2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -6,7 +6,6 @@ import org.jenkinsci.Symbol; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.Base64Utils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -16,7 +15,7 @@ public class BearerTokenAuth extends Auth2 { - //private static final long serialVersionUID = 051380141338287L; + private static final long serialVersionUID = 3614172320192170597L; @Extension public static final Auth2Descriptor DESCRIPTOR = new BearerTokenAuthDescriptor(); @@ -83,12 +82,10 @@ public boolean equals(Object obj) { return false; BearerTokenAuth other = (BearerTokenAuth) obj; if (token == null) { - if (other.token != null) - return false; - } else if (!token.equals(other.token)) { - return false; + if (other.token == null) { + return true; + } } - return true; + return token.equals(other.token); } - } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index 816cc570..917f5074 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -9,6 +9,20 @@ public class Auth2Test { + @Test + public void testBearerTokenAuthCloneBehaviour() throws CloneNotSupportedException { + BearerTokenAuth original = new BearerTokenAuth(); + original.setToken("original"); + BearerTokenAuth clone = (BearerTokenAuth)original.clone(); + verifyEqualsHashCode(original, clone); + + //Test changing clone + clone.setToken("changed"); + verifyEqualsHashCode(original, clone, false); + assertEquals("original", original.getToken()); + assertEquals("changed", clone.getToken()); + } + @Test public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedException { CredentialsAuth original = new CredentialsAuth(); From 84344f712c575ab8835e6365b4ccafa40ddb7256 Mon Sep 17 00:00:00 2001 From: Brandon Squizzato Date: Thu, 2 Jan 2020 14:18:57 -0500 Subject: [PATCH 174/262] Minor tweak to handle potential NPE --- .../ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java index 985f3af2..19d51b00 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -84,6 +84,8 @@ public boolean equals(Object obj) { if (token == null) { if (other.token == null) { return true; + } else { + return false; } } return token.equals(other.token); From faf5af8025c9aa88260ac8e5c79a39fbe7ca931d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 4 Jan 2020 22:36:30 +0800 Subject: [PATCH 175/262] update change log for release --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a10399..b7035ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 3.1.1 (Jan 4th, 2020) +### New feature + +* Support barrier token authentication ([2b067fe](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/2b067fe963d4d68cef496062b5191e4017b715fd)) + +### Improvement + +* Enhance exception handling of json output ([4642fc6](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/4642fc6d1ee6a09de552a83ae598d0bdfecf6d41)) +* Remove semaphore from serialization ([83a8c0a](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/83a8c0a386838718b5af72c4ff67c8c0efadab3d)) + +### Bug fixes + +* None + + # 3.1.0 (Oct 3rd, 2019) ### New feature From 827844544d57da9ee5d2c7f0b48183e1e2bac7ec Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 4 Jan 2020 23:06:17 +0800 Subject: [PATCH 176/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 41d133d3..fd0ed2ea 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.1-SNAPSHOT + 3.1.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.1 From c9c35968807cc2b2544c054e25d90426055eec20 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 4 Jan 2020 23:06:30 +0800 Subject: [PATCH 177/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fd0ed2ea..c8f60ae5 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.1 + 3.1.2-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.1 + HEAD From 8268b1276d1368f5eafb7d5a611588a9100e2eb8 Mon Sep 17 00:00:00 2001 From: Ashish Dubey Date: Sun, 23 Feb 2020 17:40:19 +0530 Subject: [PATCH 178/262] add handler.updateBuildStatus() to non-blocking example --- README_PipelineConfiguration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md index 8945c3e8..b5a14d28 100644 --- a/README_PipelineConfiguration.md +++ b/README_PipelineConfiguration.md @@ -153,6 +153,7 @@ def handle = triggerRemoteJob( while( !handle.isFinished() ) { echo 'Current Status: ' + handle.getBuildStatus().toString(); sleep 5 + handle.updateBuildStatus() } echo handle.getBuildStatus().toString(); ``` From 38e2699e0e725b6ff0d176cf25d8203811c8260d Mon Sep 17 00:00:00 2001 From: Declan Curran Date: Tue, 25 Feb 2020 12:03:39 +0000 Subject: [PATCH 179/262] Fixed remote build url overriding for protocol and port --- .../RemoteBuildConfiguration.java | 36 +++++++++++-------- .../RemoteBuildConfigurationTest.java | 11 ++++++ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index fd2f4f12..66503981 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -841,21 +841,9 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } QueueItemData queueItem = getQueueItemData(queueId, context); if (queueItem.isExecuted()) { - URL effectiveRemoteBuildURL = queueItem.getBuildURL(); - try { - URI effectiveUri = new URI(context.effectiveRemoteServer.getAddress()); - String effectiveHostname = effectiveUri.getHost(); - URL remoteURL = queueItem.getBuildURL(); - if (remoteURL != null) { - URI remoteUri = remoteURL.toURI(); - String remoteHostname = remoteUri.getHost(); - String effectiveRemoteAddress = remoteUri.toString().replaceAll(remoteHostname, - effectiveHostname); - effectiveRemoteBuildURL = new URL(effectiveRemoteAddress); - } - } catch (URISyntaxException ex) { - throw new AbortException(String.format("Unexpected syntax error: %s.", ex.toString())); - } + URL remoteBuildURL = queueItem.getBuildURL(); + String effectiveRemoteServerAddress = context.effectiveRemoteServer.getAddress(); + URL effectiveRemoteBuildURL = generateEffectiveRemoteBuildURL(remoteBuildURL, effectiveRemoteServerAddress); buildInfo.setBuildData(queueItem.getBuildNumber(), effectiveRemoteBuildURL); } return buildInfo; @@ -882,6 +870,24 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn return buildInfo; } + protected static URL generateEffectiveRemoteBuildURL(URL remoteBuildURL, String effectiveRemoteServerAddress) throws AbortException { + if (effectiveRemoteServerAddress == null || remoteBuildURL == null) { + return remoteBuildURL; + } + + try { + URI effectiveUri = new URI(effectiveRemoteServerAddress); + return new URL( + effectiveUri.getScheme(), + effectiveUri.getHost(), + effectiveUri.getPort(), + remoteBuildURL.getPath() + ); + } catch (URISyntaxException | MalformedURLException ex) { + throw new AbortException(String.format("Unexpected syntax error: %s.", ex.toString())); + } + } + private String printOffsetConsoleOutput(BuildContext context, String offset, RemoteBuildInfo buildInfo) throws IOException, InterruptedException { if (offset.equals("-1")) { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 97b9677f..48b40fd5 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.net.MalformedURLException; +import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -453,6 +454,16 @@ public void testRemoveHashParameters() { assertEquals("xxx", RemoteBuildConfiguration.removeHashParameters("xxx#zzz")); } + @Test @WithoutJenkins + public void testGenerateEffectiveRemoteBuildURL() throws Exception { + URL remoteBuildURL = new URL("http://test:8080/job/Abc/3/"); + + assertEquals(new URL("https://foobar:8443/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar:8443")); + assertEquals(new URL("http://foobar:8888/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar:8888")); + assertEquals(new URL("https://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar")); + assertEquals(new URL("http://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar")); + } + @Test @WithoutJenkins public void testGenerateJobUrl() throws MalformedURLException, AbortException { RemoteJenkinsServer remoteServer = new RemoteJenkinsServer(); From ec05bbad0b47135c3f88ad051da34766bea9ff33 Mon Sep 17 00:00:00 2001 From: Nick Korsakov Date: Tue, 17 Mar 2020 04:08:37 +0300 Subject: [PATCH 180/262] Set restriction on pollinterval for items in queue The poll intervals depends from context of remote build. Regression fix for: https://issues.jenkins-ci.org/browse/JENKINS-55038 All items in Jenkins queue cache have TTL 5 minutes https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Queue.java#L217-L222 --- .../RemoteBuildConfiguration.java | 42 +++++++++++-------- .../pipeline/Handle.java | 2 +- .../pipeline/RemoteBuildPipelineStep.java | 3 +- .../utils/RestUtils.java | 5 ++- .../RemoteBuildConfigurationTest.java | 3 +- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 66503981..dca3fe2a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -90,6 +90,11 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep */ private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; + /** + * The TTL value of all `queued` items is only 5 minutes. + * That is why we have to ignore user specified poll interval for such items. + */ + private static final int QUEUED_ITEMS_POLLINTERVALL = 30; private static final int DEFAULT_POLLINTERVALL = 10; private static final int connectionRetryLimit = 5; @@ -662,7 +667,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti try { ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, - this.getPollInterval(), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), + this.getPollInterval(buildInfo.getStatus()), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), isUseCrumbCache()); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); buildInfo.setQueueId(queueItem.getId()); @@ -699,13 +704,9 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx context.logger.println("Waiting for remote build to be executed..."); } - int pollIntervalForQueuedItem = this.pollInterval; - if (pollIntervalForQueuedItem > DEFAULT_POLLINTERVALL) { - pollIntervalForQueuedItem = DEFAULT_POLLINTERVALL; - } while (buildInfo.isQueued()) { - context.logger.println("Waiting for " + pollIntervalForQueuedItem + " seconds until next poll."); - Thread.sleep(pollIntervalForQueuedItem * 1000); + context.logger.println("Waiting for " + this.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); + Thread.sleep(this.getPollInterval(buildInfo.getStatus()) * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } @@ -747,9 +748,9 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx if (this.getEnhancedLogging()) { consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); } else { - context.logger.println(" Waiting for " + this.pollInterval + " seconds until next poll."); + context.logger.println(" Waiting for " + this.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); } - Thread.sleep(this.pollInterval * 1000); + Thread.sleep(this.getPollInterval(buildInfo.getStatus()) * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); } @@ -800,7 +801,7 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo } String queueQuery = String.format("%s/queue/item/%s/api/json/", context.effectiveRemoteServer.getAddress(), queueId); - ConnectionResponse response = doGet(queueQuery, context); + ConnectionResponse response = doGet(queueQuery, context, RemoteBuildStatus.QUEUED); JSONObject queueResponse = response.getBody(); if (queueResponse == null || queueResponse.isNullObject()) { @@ -852,7 +853,7 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn // Only avoid url cache while loop inquiry String buildUrlString = String.format("%sapi/json/?seed=%d", buildInfo.getBuildURL(), System.currentTimeMillis()); - JSONObject responseObject = doGet(buildUrlString, context).getBody(); + JSONObject responseObject = doGet(buildUrlString, context, buildInfo.getStatus()).getBody(); try { if (responseObject == null @@ -894,7 +895,7 @@ private String printOffsetConsoleOutput(BuildContext context, String offset, Rem return "-1"; } String buildUrlString = String.format("%slogText/progressiveText?start=%s", buildInfo.getBuildURL(), offset); - ConnectionResponse response = doGet(buildUrlString, context); + ConnectionResponse response = doGet(buildUrlString, context, buildInfo.getStatus()); String rawBody = response.getRawBody(); if (rawBody != null && !rawBody.equals("")) { @@ -915,13 +916,14 @@ private String printOffsetConsoleOutput(BuildContext context, String offset, Rem * * @param urlString the URL that needs to be called. * @param context the context of this Builder/BuildStep. + * @param remoteBuildStatus the build status of a remote build. * @return JSONObject a valid JSON object, or null. * @throws InterruptedException if any thread has interrupted the current * thread. * @throws IOException if any HTTP error occurred. */ - public ConnectionResponse doGet(String urlString, BuildContext context) throws IOException, InterruptedException { - return HttpHelper.tryGet(urlString, context, this.getPollInterval(), this.getConnectionRetryLimit(), + public ConnectionResponse doGet(String urlString, BuildContext context, RemoteBuildStatus remoteBuildStatus) throws IOException, InterruptedException { + return HttpHelper.tryGet(urlString, context, this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), this.getAuth2(), getLock(urlString)); } @@ -1033,8 +1035,14 @@ public boolean getPreventRemoteBuildQueue() { return preventRemoteBuildQueue; } - public int getPollInterval() { - return pollInterval; + public int getPollInterval(RemoteBuildStatus remoteBuildStatus) { + switch (remoteBuildStatus) { + case NOT_TRIGGERED: + case QUEUED: + return QUEUED_ITEMS_POLLINTERVALL; + default: + return pollInterval; + } } public boolean getBlockBuildUntilComplete() { @@ -1097,7 +1105,7 @@ public boolean isDisabled() { return jsonObject; } - ConnectionResponse response = doGet(remoteJobUrl, context); + ConnectionResponse response = doGet(remoteJobUrl, context, RemoteBuildStatus.FINISHED); if (response.getResponseCode() < 400 && response.getBody() != null) { return DropCachePeriodicWork.safePutJobInfo(remoteJobUrl, response.getBody(), isUseJobInfoCache()); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 854b71d9..3d679ced 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -357,7 +357,7 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, PrintStreamWrapper log = new PrintStreamWrapper(); try { BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); - return remoteBuildConfiguration.doGet(fileUrl.toString(), context).getBody(); + return remoteBuildConfiguration.doGet(fileUrl.toString(), context, getBuildStatus()).getBody(); } finally { lastLog = log.getContent(); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 50c8bce8..f957b19d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -32,6 +32,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; @@ -313,7 +314,7 @@ public boolean getPreventRemoteBuildQueue() { } public int getPollInterval() { - return remoteBuildConfig.getPollInterval(); + return remoteBuildConfig.getPollInterval(RemoteBuildStatus.RUNNING); } public boolean getBlockBuildUntilComplete() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java index a961133d..a6ed21d7 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java @@ -9,6 +9,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.ExceedRetryLimitException; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; /* * Going to migrate all rest APIs to here @@ -23,7 +24,7 @@ public static ConnectionResponse cancelQueueItem(String rootUrl, Handle handle, String cancelQueueUrl = String.format("%s/queue/cancelItem?id=%s", rootUrl, handle.getQueueId()); ConnectionResponse resp = null; try { - resp = HttpHelper.tryPost(cancelQueueUrl, context, null, remoteConfig.getPollInterval() * 2, 0, + resp = HttpHelper.tryPost(cancelQueueUrl, context, null, remoteConfig.getPollInterval(RemoteBuildStatus.QUEUED) * 2, 0, remoteConfig.getAuth2(), remoteConfig.getLock(cancelQueueUrl), remoteConfig.isUseCrumbCache()); } catch (ExceedRetryLimitException e) { // Due to https://issues.jenkins-ci.org/browse/JENKINS-21311, we can't tell @@ -40,7 +41,7 @@ public static ConnectionResponse stopRemoteJob(Handle handle, BuildContext conte RemoteBuildInfo buildInfo = handle.getBuildInfo(); String stopJobUrl = String.format("%sstop", buildInfo.getBuildURL()); - ConnectionResponse resp = HttpHelper.tryPost(stopJobUrl, context, null, remoteConfig.getPollInterval(), + ConnectionResponse resp = HttpHelper.tryPost(stopJobUrl, context, null, remoteConfig.getPollInterval(buildInfo.getStatus()), remoteConfig.getConnectionRetryLimit(), remoteConfig.getAuth2(), remoteConfig.getLock(stopJobUrl), remoteConfig.isUseCrumbCache()); context.logger.println(String.format("Remote Job:%s was aborted!", buildInfo.getBuildURL())); diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 48b40fd5..d794b39f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -22,6 +22,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -178,7 +179,7 @@ public void testDefaults() throws IOException { assertEquals(false, config.getOverrideAuth()); assertEquals("", config.getParameterFile()); assertEquals("", config.getParameters()); - assertEquals(10, config.getPollInterval()); + assertEquals(10, config.getPollInterval(RemoteBuildStatus.RUNNING)); assertEquals(false, config.getPreventRemoteBuildQueue()); assertEquals(null, config.getRemoteJenkinsName()); assertEquals(false, config.getShouldNotFailBuild()); From b9b566ac18b76a6840f6556461e882b07a8f764a Mon Sep 17 00:00:00 2001 From: Krzysztof Witkowski Date: Tue, 7 Apr 2020 16:29:30 +0200 Subject: [PATCH 181/262] Allow customization of HTTP POST/GET timeouts --- .../RemoteBuildConfiguration.java | 39 ++++++++++- .../pipeline/RemoteBuildPipelineStep.java | 18 +++++ .../utils/HttpHelper.java | 67 ++++++++++--------- .../utils/RestUtils.java | 9 +-- .../RemoteBuildPipelineStep/config.jelly | 8 +++ .../RemoteBuildConfigurationTest.java | 6 ++ 6 files changed, 110 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index dca3fe2a..15810fb4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -90,11 +90,15 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep */ private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; + private static final int DEFAULT_HTTP_GET_READ_TIMEOUT = 10000; + private static final int DEFAULT_HTTP_POST_READ_TIMEOUT = 30000; + /** * The TTL value of all `queued` items is only 5 minutes. * That is why we have to ignore user specified poll interval for such items. */ private static final int QUEUED_ITEMS_POLLINTERVALL = 30; + private static final int DEFAULT_POLLINTERVALL = 10; private static final int connectionRetryLimit = 5; @@ -112,6 +116,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean trustAllCertificates; private boolean overrideTrustAllCertificates; private boolean preventRemoteBuildQueue; + private int httpPostReadTimeout; + private int httpGetReadTimeout; private int pollInterval; private boolean blockBuildUntilComplete; private String job; @@ -133,6 +139,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep @DataBoundConstructor public RemoteBuildConfiguration() { + httpGetReadTimeout = DEFAULT_HTTP_GET_READ_TIMEOUT; + httpPostReadTimeout = DEFAULT_HTTP_POST_READ_TIMEOUT; pollInterval = DEFAULT_POLLINTERVALL; } @@ -207,6 +215,22 @@ public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { this.preventRemoteBuildQueue = preventRemoteBuildQueue; } + @DataBoundSetter + public void setHttpGetReadTimeout(int readTimeout) { + if (readTimeout < 1000) + this.httpGetReadTimeout = DEFAULT_HTTP_GET_READ_TIMEOUT; + else + this.httpGetReadTimeout = readTimeout; + } + + @DataBoundSetter + public void setHttpPostReadTimeout(int readTimeout) { + if (readTimeout < 1000) + this.httpPostReadTimeout = DEFAULT_HTTP_POST_READ_TIMEOUT; + else + this.httpPostReadTimeout = readTimeout; + } + @DataBoundSetter public void setPollInterval(int pollInterval) { if (pollInterval <= 0) @@ -667,8 +691,8 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti try { ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, - this.getPollInterval(buildInfo.getStatus()), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), - isUseCrumbCache()); + this.getHttpPostReadTimeout(), this.getPollInterval(buildInfo.getStatus()), this.getConnectionRetryLimit(), + this.getAuth2(), getLock(triggerUrlString), isUseCrumbCache()); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); buildInfo.setQueueId(queueItem.getId()); buildInfo = updateBuildInfo(buildInfo, context); @@ -923,7 +947,8 @@ private String printOffsetConsoleOutput(BuildContext context, String offset, Rem * @throws IOException if any HTTP error occurred. */ public ConnectionResponse doGet(String urlString, BuildContext context, RemoteBuildStatus remoteBuildStatus) throws IOException, InterruptedException { - return HttpHelper.tryGet(urlString, context, this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), + return HttpHelper.tryGet(urlString, context, this.getHttpGetReadTimeout(), + this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), this.getAuth2(), getLock(urlString)); } @@ -1010,6 +1035,14 @@ public String getRemoteJenkinsUrl() { return trimToNull(remoteJenkinsUrl); } + public int getHttpGetReadTimeout() { + return httpGetReadTimeout; + } + + public int getHttpPostReadTimeout() { + return httpPostReadTimeout; + } + /** * @return true, if the authorization is overridden in the job configuration, * otherwise false. diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index f957b19d..ed4aa3c4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -119,6 +119,16 @@ public void setPreventRemoteBuildQueue(boolean preventRemoteBuildQueue) { remoteBuildConfig.setPreventRemoteBuildQueue(preventRemoteBuildQueue); } + @DataBoundSetter + public void setHttpGetReadTimeout(int readTimeout) { + remoteBuildConfig.setHttpGetReadTimeout(readTimeout); + } + + @DataBoundSetter + public void setHttpPostReadTimeout(int readTimeout) { + remoteBuildConfig.setHttpPostReadTimeout(readTimeout); + } + @DataBoundSetter public void setPollInterval(int pollInterval) { remoteBuildConfig.setPollInterval(pollInterval); @@ -313,6 +323,14 @@ public boolean getPreventRemoteBuildQueue() { return remoteBuildConfig.getPreventRemoteBuildQueue(); } + public int getHttpGetReadTimeout() { + return remoteBuildConfig.getHttpGetReadTimeout(); + } + + public int getHttpPostReadTimeout() { + return remoteBuildConfig.getHttpPostReadTimeout(); + } + public int getPollInterval() { return remoteBuildConfig.getPollInterval(RemoteBuildStatus.RUNNING); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 933b99cc..87e70852 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -428,6 +428,8 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * parameters to post * @param numberOfAttempts * number of time that the connection has been attempted + * @param readTimeout + * read timeout in milliseconds * @param pollInterval * interval between each retry in second * @param retryLimit @@ -444,8 +446,9 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, - Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, - StringBuilder rawRespRef, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { + Collection postParams, int readTimeout, int numberOfAttempts, int pollInterval, int retryLimit, + Auth2 overrideAuth, StringBuilder rawRespRef, boolean isCrubmCacheEnabled) + throws IOException, InterruptedException { JSONObject responseObject = null; Map> responseHeader = null; @@ -466,18 +469,15 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept-Language", "UTF-8"); conn.setRequestMethod(requestType); + conn.setReadTimeout(readTimeout); addCrumbToConnection(conn, context, overrideAuth, isCrubmCacheEnabled); // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); if (HTTP_POST.equalsIgnoreCase(requestType)) { - // use longer timeout during POST due to not performing retries since POST is not idem-potent - conn.setReadTimeout(30000); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length)); conn.setDoOutput(true); conn.getOutputStream().write(postDataBytes); - }else { - conn.setReadTimeout(10000); } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); @@ -561,8 +561,8 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); numberOfAttempts++; - return sendHTTPCall(urlString, requestType, context, postParams, numberOfAttempts, pollInterval, - retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); + return sendHTTPCall(urlString, requestType, context, postParams, readTimeout, + numberOfAttempts, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); } else { // reached the maximum number of retries, time to fail @@ -579,12 +579,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, - Collection params, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, + Collection params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { if (lock == null) { context.logger.println("calling remote without locking..."); - return sendHTTPCall(urlString, method, context, null, 1, pollInterval, retryLimit, overrideAuth, rawRespRef, - isCrubmCacheEnabled); + return sendHTTPCall(urlString, method, context, null, readTimeout, + 1, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); } Boolean isAcquired = null; try { @@ -600,8 +600,8 @@ private static ConnectionResponse tryCall(String urlString, String method, Build logger.warning("fail to acquire lock because of timeout, skip locking..."); } - ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, 1, pollInterval, retryLimit, - overrideAuth, rawRespRef, isCrubmCacheEnabled); + ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, readTimeout, + 1, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); return cr; } finally { @@ -612,38 +612,45 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } public static ConnectionResponse tryPost(String urlString, BuildContext context, Collection params, - int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock, boolean isCrubmCacheEnabled) - throws IOException, InterruptedException { + int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock, + boolean isCrubmCacheEnabled) throws IOException, InterruptedException { - return tryCall(urlString, HTTP_POST, context, params, pollInterval, retryLimit, overrideAuth, null, lock,isCrubmCacheEnabled); + return tryCall(urlString, HTTP_POST, context, params, readTimeout, pollInterval, retryLimit, + overrideAuth, null, lock,isCrubmCacheEnabled); } - public static ConnectionResponse tryGet(String urlString, BuildContext context, int pollInterval, int retryLimit, - Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { - return tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, null, lock, false); + public static ConnectionResponse tryGet(String urlString, BuildContext context, int readTimeout, + int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) + throws IOException, InterruptedException { + return tryCall(urlString, HTTP_GET, context, null, readTimeout, pollInterval, retryLimit, + overrideAuth, null, lock, false); } - public static String tryGetRawResp(String urlString, BuildContext context, int pollInterval, int retryLimit, - Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { + public static String tryGetRawResp(String urlString, BuildContext context, int readTimeout, + int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) + throws IOException, InterruptedException { StringBuilder resp = new StringBuilder(); - tryCall(urlString, HTTP_GET, context, null, pollInterval, retryLimit, overrideAuth, resp, lock, false); + tryCall(urlString, HTTP_GET, context, null, readTimeout, pollInterval, retryLimit, + overrideAuth, resp, lock, false); return resp.toString(); } public static ConnectionResponse post(String urlString, BuildContext context, Collection params, - int pollInterval, int retryLimit, Auth2 overrideAuth, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { - return tryPost(urlString, context, params, pollInterval, retryLimit, overrideAuth, null, isCrubmCacheEnabled); + int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, boolean isCrubmCacheEnabled) + throws IOException, InterruptedException { + return tryPost(urlString, context, params, readTimeout, pollInterval, retryLimit, overrideAuth, + null, isCrubmCacheEnabled); } - public static ConnectionResponse get(String urlString, BuildContext context, int pollInterval, int retryLimit, - Auth2 overrideAuth) throws IOException, InterruptedException { - return tryGet(urlString, context, pollInterval, retryLimit, overrideAuth, null); + public static ConnectionResponse get(String urlString, BuildContext context, int readTimeout, + int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { + return tryGet(urlString, context, readTimeout, pollInterval, retryLimit, overrideAuth, null); } public static String getRawResp(String urlString, String requestType, BuildContext context, - Collection postParams, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth) - throws IOException, InterruptedException { - return tryGetRawResp(urlString, context, pollInterval, retryLimit, overrideAuth, null); + Collection postParams, int readTimeout, int numberOfAttempts, int pollInterval, + int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { + return tryGetRawResp(urlString, context, readTimeout, pollInterval, retryLimit, overrideAuth, null); } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java index a6ed21d7..53c104f9 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java @@ -24,7 +24,8 @@ public static ConnectionResponse cancelQueueItem(String rootUrl, Handle handle, String cancelQueueUrl = String.format("%s/queue/cancelItem?id=%s", rootUrl, handle.getQueueId()); ConnectionResponse resp = null; try { - resp = HttpHelper.tryPost(cancelQueueUrl, context, null, remoteConfig.getPollInterval(RemoteBuildStatus.QUEUED) * 2, 0, + resp = HttpHelper.tryPost(cancelQueueUrl, context, null, remoteConfig.getHttpPostReadTimeout(), + remoteConfig.getPollInterval(RemoteBuildStatus.QUEUED) * 2, 0, remoteConfig.getAuth2(), remoteConfig.getLock(cancelQueueUrl), remoteConfig.isUseCrumbCache()); } catch (ExceedRetryLimitException e) { // Due to https://issues.jenkins-ci.org/browse/JENKINS-21311, we can't tell @@ -41,9 +42,9 @@ public static ConnectionResponse stopRemoteJob(Handle handle, BuildContext conte RemoteBuildInfo buildInfo = handle.getBuildInfo(); String stopJobUrl = String.format("%sstop", buildInfo.getBuildURL()); - ConnectionResponse resp = HttpHelper.tryPost(stopJobUrl, context, null, remoteConfig.getPollInterval(buildInfo.getStatus()), - remoteConfig.getConnectionRetryLimit(), remoteConfig.getAuth2(), remoteConfig.getLock(stopJobUrl), - remoteConfig.isUseCrumbCache()); + ConnectionResponse resp = HttpHelper.tryPost(stopJobUrl, context, null, remoteConfig.getHttpPostReadTimeout(), + remoteConfig.getPollInterval(buildInfo.getStatus()), remoteConfig.getConnectionRetryLimit(), + remoteConfig.getAuth2(), remoteConfig.getLock(stopJobUrl), remoteConfig.isUseCrumbCache()); context.logger.println(String.format("Remote Job:%s was aborted!", buildInfo.getBuildURL())); return resp; } diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index cff99dd2..4eb695a0 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -28,6 +28,14 @@
+ + + + + + + + diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index d794b39f..9450879f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -116,6 +116,8 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle configuration.setPreventRemoteBuildQueue(false); configuration.setBlockBuildUntilComplete(true); configuration.setPollInterval(1); + configuration.setHttpGetReadTimeout(1000); + configuration.setHttpPostReadTimeout(1000); configuration.setUseCrumbCache(false); configuration.setUseJobInfoCache(false); configuration.setEnhancedLogging(true); @@ -179,6 +181,8 @@ public void testDefaults() throws IOException { assertEquals(false, config.getOverrideAuth()); assertEquals("", config.getParameterFile()); assertEquals("", config.getParameters()); + assertEquals(10000, config.getHttpGetReadTimeout()); + assertEquals(30000, config.getHttpPostReadTimeout()); assertEquals(10, config.getPollInterval(RemoteBuildStatus.RUNNING)); assertEquals(false, config.getPreventRemoteBuildQueue()); assertEquals(null, config.getRemoteJenkinsName()); @@ -200,6 +204,8 @@ public void testDefaultsPipelineStep() throws IOException { assertTrue(config.getAuth() instanceof NullAuth); assertEquals("", config.getParameterFile()); assertEquals("", config.getParameters()); + assertEquals(10000, config.getHttpGetReadTimeout()); + assertEquals(30000, config.getHttpPostReadTimeout()); assertEquals(10, config.getPollInterval()); assertEquals(false, config.getPreventRemoteBuildQueue()); assertEquals(null, config.getRemoteJenkinsName()); From aeac218e2360c79107456b13e2c9a74ccc44ebcd Mon Sep 17 00:00:00 2001 From: Ioannis Koutras Date: Wed, 8 Apr 2020 16:44:53 +0200 Subject: [PATCH 182/262] Setup CookieHandler This commit sets up a CookieManager and makes the system-wide CookieHandler use it. Starting from Jenkins LTS 2.176.2 a valid Web Session for which the crumb was issued is also required unless configured otherwise. --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index dca3fe2a..6d3feffd 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -12,6 +12,8 @@ import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Serializable; +import java.net.CookieHandler; +import java.net.CookieManager; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -133,6 +135,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep @DataBoundConstructor public RemoteBuildConfiguration() { + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); pollInterval = DEFAULT_POLLINTERVALL; } From 67770f515ee343fbc79de603e567c2837e93ad11 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Apr 2020 22:19:30 +0800 Subject: [PATCH 183/262] update change log --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7035ce9..7f13667b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ + +# 3.1.2 (Apr 13th, 2020) +### New feature + +* Support customization of HTTP read timeout for GET & POST ([b9b566a](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/b3841c54571dedee67c6a4a08e1cefcebd5c8760)) + +### Improvement + +* Add cookie handler (Jenkins 2.176.2) ([aeac218](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/aeac218e2360c79107456b13e2c9a74ccc44ebcd)) +* Documentation: Add handler.updateBuildStatus() to non-blocking example ([8268b12](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/8268b1276d1368f5eafb7d5a611588a9100e2eb8)) + +### Bug fixes + +* Keep remote Jenkins URL scheme ([38e2699](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/38e2699e0e725b6ff0d176cf25d8203811c8260d)) +* Set the uplimit when polling queue item information. ([ec05bba](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/ec05bbad0b47135c3f88ad051da34766bea9ff33)) + # 3.1.1 (Jan 4th, 2020) ### New feature From 27c95d88b04177923468263a988aa35ec365c998 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Apr 2020 22:35:32 +0800 Subject: [PATCH 184/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c8f60ae5..c6e35220 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.2-SNAPSHOT + 3.1.2 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.2 From d84fc70bd2f2899f31828b5fd25f95fb66578ff1 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Apr 2020 22:35:44 +0800 Subject: [PATCH 185/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c6e35220..c91ad60d 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.2 + 3.1.3-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.2 + HEAD From a7e69eb7657714f899b6013ad0716adfaf74ade1 Mon Sep 17 00:00:00 2001 From: Eric Schulz Date: Tue, 21 Apr 2020 22:05:55 -0700 Subject: [PATCH 186/262] http headers should be case insensitive --- .../ConnectionResponse.java | 24 ++++-- .../remoteJob/QueueItemTest.java | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java index 87d2e3ed..38443d47 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -1,13 +1,14 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import java.util.List; -import java.util.Map; +import net.sf.json.JSONObject; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; - -import net.sf.json.JSONObject; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; /** * Http response containing header, body (JSON format) and response code. @@ -16,7 +17,7 @@ public class ConnectionResponse { @Nonnull - private final Map> header; + private final Map> header = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); @Nullable @CheckForNull private final JSONObject body; @@ -30,7 +31,7 @@ public class ConnectionResponse public ConnectionResponse(@Nonnull Map> header, @Nullable JSONObject body, @Nonnull int responseCode) { - this.header = header; + loadHeader(header); this.body = body; this.rawBody = null; this.responseCode = responseCode; @@ -38,7 +39,7 @@ public ConnectionResponse(@Nonnull Map> header, @Nullable J public ConnectionResponse(@Nonnull Map> header, @Nullable String rawBody, @Nonnull int responseCode) { - this.header = header; + loadHeader(header); this.body = null; this.rawBody = rawBody; this.responseCode = responseCode; @@ -46,12 +47,19 @@ public ConnectionResponse(@Nonnull Map> header, @Nullable S public ConnectionResponse(@Nonnull Map> header, @Nonnull int responseCode) { - this.header = header; + loadHeader(header); this.body = null; this.rawBody = null; this.responseCode = responseCode; } + private void loadHeader(Map> header) { + // null key is not compatible with the string Comparator, so we leave it out. + Map> filtered = header.entrySet().stream().filter(entry -> entry.getKey() != null).collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + this.header.putAll(filtered); + } + public Map> getHeader() { return header; diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java new file mode 100644 index 00000000..0787cad6 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java @@ -0,0 +1,80 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import hudson.AbortException; +import org.eclipse.jetty.server.Response; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class QueueItemTest { + + // QueueItem looks for "Location" so test specifically for that + final static private String key = "Location"; + final static private String id = "4848912"; + final static private String location = String.format("http://example.com/jenkins/my-jenkins1/queue/item/%s/", id); + + // invalid header missing Location + final static private Map> noLocationHeader = new HashMap>() {{ + put("Date", Collections.singletonList("Tue, 21 Apr 2020 02:26:47 GMT")); + put("Server", Collections.singletonList("envoy")); + put(null, Collections.singletonList("HTTP/1.1 201 Created")); + put("Content-Length", Collections.singletonList("0")); + put("X-Envoy-Upstream-Service-Time", Collections.singletonList("15")); + put("X-Content-Type-Options", Collections.singletonList("nosniff")); + }}; + + // Add the Location to make valid header with typical capitalization + final static private Map> locationHeader = new HashMap>(noLocationHeader) {{ + put(key, Collections.singletonList(location)); + }}; + + // valid header with all lowercase. Watch out for null key. + final static private Map> lowerCaseLocationHeader = locationHeader.entrySet().stream().collect( + Collectors.toMap(entry -> entry.getKey() == null ? null : entry.getKey().toLowerCase(), + entry -> entry.getValue())); + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { noLocationHeader, false }, + { locationHeader, true }, + { lowerCaseLocationHeader, true } + }); + } + + @Parameterized.Parameter() + public Map> header; + + @Parameterized.Parameter(1) + public boolean isValid; + + @Test + public void test() { + // ConnectionResponse creates case-insensitive map of header + ConnectionResponse connectionResponse = new ConnectionResponse(header, Response.SC_OK); + + try { + QueueItem queueItem = new QueueItem(connectionResponse.getHeader()); + assertTrue("QueueItem should have thrown exception for invalid header: " + header, isValid); + assertEquals(queueItem.getLocation(), location); + assertEquals(queueItem.getId(), id); + } catch (AbortException e) { + assertFalse("QueueItem thew exception for valid header: " + header, isValid); + } + } +} \ No newline at end of file From f414c32853128a2baad88ab42d406b59e97b99cb Mon Sep 17 00:00:00 2001 From: "Jenna Chen(I532542)" Date: Mon, 27 Apr 2020 20:39:46 +0800 Subject: [PATCH 187/262] use isNullObject() method of class JSONObject instead of using null to check if this JSONObject is null object --- .../ParameterizedRemoteTrigger/remoteJob/QueueItemData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java index 044c527a..b0ba0d4b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -115,7 +115,7 @@ public void update(@Nonnull BuildContext context, @Nonnull JSONObject queueRespo if (isLeft()) { try { JSONObject remoteJobInfo = queueResponse.getJSONObject("executable"); - if (remoteJobInfo != null) { + if (!(remoteJobInfo.isNullObject())) { try { buildNumber = remoteJobInfo.getInt("number"); } catch (JSONException e) { From bf25943af591565f610c35f6075157400a5b1218 Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Tue, 28 Apr 2020 12:07:17 +0800 Subject: [PATCH 188/262] Only use token macro on user supplied values Signed-off-by: Raihaan Shouhell --- .../auth2/CredentialsAuth.java | 2 +- .../auth2/TokenAuth.java | 2 +- .../pipeline/Handle.java | 1 - .../utils/Base64Utils.java | 8 ++++++-- .../utils/Base64UtilsTest.java | 15 +++++++++++++++ 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64UtilsTest.java diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 6ea5f616..1b7820be 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -122,7 +122,7 @@ public void setAuthorizationHeader(URLConnection connection, BuildContext contex Jenkins jenkins = Jenkins.getInstance(); if (jenkins != null) { Item item = jenkins.getItem(context.currentItem, jenkins.getItem("/")); - String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(item), getPassword(item), context); + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(item), getPassword(item), context, false); connection.setRequestProperty("Authorization", authHeaderValue); } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index 2a26640a..d3d25bf7 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -50,7 +50,7 @@ public String getApiToken() { @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken(), context); + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken(), context, true); connection.setRequestProperty("Authorization", authHeaderValue); } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index 3d679ced..be0a68f8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -19,7 +19,6 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.HttpHelper; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; import hudson.model.Result; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java index e6abf2e5..396659f7 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java @@ -34,6 +34,8 @@ public static String encode(String input) throws UnsupportedEncodingException * the user password. * @param context * the context of this Builder/BuildStep. + * @param applyMacro + * boolean to control if macro replacements occur * @return the base64 encoded authorization. * @throws IOException * if there is a failure while replacing token macros, or @@ -41,13 +43,15 @@ public static String encode(String input) throws UnsupportedEncodingException */ @Nonnull public static String generateAuthorizationHeaderValue(String authType, String user, String password, - BuildContext context) throws IOException + BuildContext context, boolean applyMacro) throws IOException { if (isEmpty(user)) throw new IllegalArgumentException("user null or empty"); if (password == null) throw new IllegalArgumentException("password null"); // is empty password allowed for Basic Auth? String authTypeKey = getAuthType(authType); String tuple = user + ":" + password; - tuple = TokenMacroUtils.applyTokenMacroReplacements(tuple, context); + if (applyMacro) { + tuple = TokenMacroUtils.applyTokenMacroReplacements(tuple, context); + } String encodedTuple = Base64Utils.encode(tuple); return authTypeKey + " " + encodedTuple; } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64UtilsTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64UtilsTest.java new file mode 100644 index 00000000..18153254 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64UtilsTest.java @@ -0,0 +1,15 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class Base64UtilsTest { + + @Test + public void testGenAuthNoToken() throws Exception { + String result = Base64Utils.generateAuthorizationHeaderValue(Base64Utils.AUTHTYPE_BASIC, "user", "$password", null, false); + assertEquals("Basic dXNlcjokcGFzc3dvcmQ=", result); + } + +} \ No newline at end of file From 5b434d443e6fcda07cc2981698aa847cbf10c122 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 10 May 2020 11:22:08 +0800 Subject: [PATCH 189/262] update change log --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f13667b..50773bbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 3.1.3 (May 10th, 2020) + +### Improvement + +* Support case insensitive HTTP header ([a7e69eb](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/68)) +* Token-macro shouldn't work on Jenkins credential auth. ([bf25943](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/71)) +* Check null json object by isNullObject instead of null checking. ([f414c32](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/70)) + # 3.1.2 (Apr 13th, 2020) ### New feature From 919275595be64f52925306403d9cee9ec6a31ec2 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 10 May 2020 11:42:19 +0800 Subject: [PATCH 190/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c91ad60d..01bc7f9a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.3-SNAPSHOT + 3.1.3 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.3 From b465abc25d1126df836fc8164ed216793aa265a6 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 10 May 2020 11:42:32 +0800 Subject: [PATCH 191/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 01bc7f9a..a41dedef 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.3 + 3.1.4-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.3 + HEAD From 2b3530530e1cc16d1a06bff600ec55cee6aba331 Mon Sep 17 00:00:00 2001 From: Raihaan Shouhell Date: Sun, 24 May 2020 22:39:17 +0800 Subject: [PATCH 192/262] Point pom to github so plugin documentation is updated --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a41dedef..fe6913c7 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. - http://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Remote+Trigger+Plugin + https://github.com/jenkinsci/parameterized-remote-trigger-plugin From 2902ef5ea6eb077f43fd25c880e4920faea4e828 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 25 May 2020 22:56:42 +0800 Subject: [PATCH 193/262] fix https://issues.jenkins-ci.org/browse/SECURITY-1625 --- .../plugins/ParameterizedRemoteTrigger/Auth.java | 5 +++-- .../ParameterizedRemoteTrigger/auth2/TokenAuth.java | 9 +++++---- .../RemoteBuildConfigurationTest.java | 3 ++- .../RemoteJenkinsServerTest.java | 8 +++++--- .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 6 ++++-- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index d7ae9ce3..c497ec2e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -26,6 +26,7 @@ import hudson.model.Item; import hudson.security.ACL; import hudson.util.ListBoxModel; +import hudson.util.Secret; /** * We need to keep this for compatibility - old config deserialization! @@ -161,7 +162,7 @@ public static Auth auth2ToAuth(Auth2 auth) { return new Auth(Auth.NONE, null, null, null); } else if (auth instanceof TokenAuth) { TokenAuth tokenAuth = (TokenAuth) auth; - return new Auth(Auth.API_TOKEN, tokenAuth.getUserName(), tokenAuth.getApiToken(), null); + return new Auth(Auth.API_TOKEN, tokenAuth.getUserName(), tokenAuth.getApiToken().getPlainText(), null); } else if (auth instanceof CredentialsAuth) { CredentialsAuth credAuth = (CredentialsAuth) auth; try { @@ -189,7 +190,7 @@ public static Auth2 authToAuth2(Auth oldAuth) { } else if (Auth.API_TOKEN.equals(authType)) { TokenAuth newAuth = new TokenAuth(); newAuth.setUserName(oldAuth.getUsername()); - newAuth.setApiToken(oldAuth.getApiToken()); + newAuth.setApiToken(Secret.fromString(oldAuth.getApiToken())); return newAuth; } else if (Auth.CREDENTIALS_PLUGIN.equals(authType)) { CredentialsAuth newAuth = new CredentialsAuth(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index d3d25bf7..1b265af3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -13,6 +13,7 @@ import hudson.Extension; import hudson.model.Item; +import hudson.util.Secret; public class TokenAuth extends Auth2 { @@ -22,7 +23,7 @@ public class TokenAuth extends Auth2 { public static final Auth2Descriptor DESCRIPTOR = new TokenAuthDescriptor(); private String userName; - private String apiToken; + private Secret apiToken; @DataBoundConstructor public TokenAuth() { @@ -40,17 +41,17 @@ public String getUserName() { } @DataBoundSetter - public void setApiToken(String apiToken) { + public void setApiToken(Secret apiToken) { this.apiToken = apiToken; } - public String getApiToken() { + public Secret getApiToken() { return this.apiToken; } @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken(), context, true); + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken().getPlainText(), context, true); connection.setRequestProperty("Authorization", authHeaderValue); } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 9450879f..b4a58975 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -44,6 +44,7 @@ import hudson.security.AuthorizationStrategy.Unsecured; import hudson.security.csrf.DefaultCrumbIssuer; import hudson.util.LogTaskListener; +import hudson.util.Secret; import jenkins.model.Jenkins; public class RemoteBuildConfigurationTest { @@ -132,7 +133,7 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle if(authenticate) { TokenAuth tokenAuth = new TokenAuth(); tokenAuth.setUserName(testUser.getId()); - tokenAuth.setApiToken(testUserToken); + tokenAuth.setApiToken(Secret.fromString(testUserToken)); configuration.setAuth2(tokenAuth); } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index 9a741577..20d8a0ea 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -10,6 +10,8 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; import org.junit.Test; +import hudson.util.Secret; + public class RemoteJenkinsServerTest { @@ -22,7 +24,7 @@ public class RemoteJenkinsServerTest { @Test public void testCloneBehaviour() throws Exception { TokenAuth auth = new TokenAuth(); - auth.setApiToken(TOKEN); + auth.setApiToken(Secret.fromString(TOKEN)); auth.setUserName(USER); RemoteJenkinsServer server = new RemoteJenkinsServer(); @@ -55,11 +57,11 @@ public void testCloneBehaviour() throws Exception { //Test if clone is deep-copy or if server fields can be modified TokenAuth cloneAuth = (TokenAuth)clone.getAuth2(); assertNotNull(cloneAuth); - cloneAuth.setApiToken("changed"); + cloneAuth.setApiToken(Secret.fromString("changed")); cloneAuth.setUserName("changed"); TokenAuth serverAuth = (TokenAuth)server.getAuth2(); assertNotNull(serverAuth); - assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken()); + assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken().getPlainText()); assertEquals("auth.userName", USER, serverAuth.getUserName()); //Test if clone.setAuth() affects original object diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index 917f5074..c512b444 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -7,6 +7,8 @@ import org.junit.Test; +import hudson.util.Secret; + public class Auth2Test { @Test @@ -40,13 +42,13 @@ public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedExceptio @Test public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { TokenAuth original = new TokenAuth(); - original.setApiToken("original"); + original.setApiToken(Secret.fromString("original")); original.setUserName("original"); TokenAuth clone = (TokenAuth)original.clone(); verifyEqualsHashCode(original, clone); //Test changing clone - clone.setApiToken("changed"); + clone.setApiToken(Secret.fromString("changed")); clone.setUserName("changed"); verifyEqualsHashCode(original, clone, false); assertEquals("original", original.getApiToken()); From 33e08b6d8a0ad49aa2532cace780969a8a74e7a7 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 26 May 2020 01:39:32 +0800 Subject: [PATCH 194/262] fix test error --- .../RemoteJenkinsServerTest.java | 5 +++++ .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java index 20d8a0ea..d87326f5 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -8,7 +8,9 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.CredentialsAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; +import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; import hudson.util.Secret; @@ -20,6 +22,9 @@ public class RemoteJenkinsServerTest { private final static String ADDRESS = "http://www.example.org:8443"; private final static String DISPLAY_NAME = "My example server."; private final static boolean HAS_BUILD_TOKEN_ROOT_SUPPORT = true; + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); @Test public void testCloneBehaviour() throws Exception { diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index c512b444..65e8673d 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -5,11 +5,16 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; import hudson.util.Secret; public class Auth2Test { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); @Test public void testBearerTokenAuthCloneBehaviour() throws CloneNotSupportedException { @@ -51,9 +56,9 @@ public void testTokenAuthCloneBehaviour() throws CloneNotSupportedException { clone.setApiToken(Secret.fromString("changed")); clone.setUserName("changed"); verifyEqualsHashCode(original, clone, false); - assertEquals("original", original.getApiToken()); + assertEquals("original", original.getApiToken().getPlainText()); assertEquals("original", original.getUserName()); - assertEquals("changed", clone.getApiToken()); + assertEquals("changed", clone.getApiToken().getPlainText()); assertEquals("changed", clone.getUserName()); } From b58ebb6f8f196aea0b233d2c636a4c069089f713 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sat, 6 Jun 2020 02:01:23 +0800 Subject: [PATCH 195/262] Use Secret for BearerTokenAuth as well. Fix the help doc. --- .../auth2/BearerTokenAuth.java | 10 +++++----- .../RemoteBuildConfiguration/help-auth2.html | 3 +++ .../RemoteJenkinsServer/help-auth2.html | 3 +++ .../auth2/BearerTokenAuth/config.jelly | 9 +++++++++ .../ParameterizedRemoteTrigger/auth2/Auth2Test.java | 8 ++++---- 5 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth/config.jelly diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java index 19d51b00..32e7489b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -4,13 +4,13 @@ import java.net.URLConnection; import org.jenkinsci.Symbol; - import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import hudson.Extension; import hudson.model.Item; +import hudson.util.Secret; public class BearerTokenAuth extends Auth2 { @@ -20,7 +20,7 @@ public class BearerTokenAuth extends Auth2 { @Extension public static final Auth2Descriptor DESCRIPTOR = new BearerTokenAuthDescriptor(); - private String token; + private Secret token; @DataBoundConstructor public BearerTokenAuth() { @@ -28,17 +28,17 @@ public BearerTokenAuth() { } @DataBoundSetter - public void setToken(String token) { + public void setToken(Secret token) { this.token = token; } - public String getToken() { + public Secret getToken() { return this.token; } @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - connection.setRequestProperty("Authorization", "Bearer: " + getToken()); + connection.setRequestProperty("Authorization", "Bearer: " + getToken().getPlainText()); } @Override diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html index 4f66f4aa..311851a1 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html @@ -13,6 +13,9 @@
  • No Authentication
    No Authorization header will be sent, independent of the global 'remote host' settings.
  • +
  • Bearer Authentication
    + The bearer token is used. +
  • Note: Jenkins API Tokens are recommended since, if stolen, they allow access only to a specific Jenkins diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html index 01dfc53d..a5210ff4 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html @@ -10,6 +10,9 @@
  • Credentials Authentication
    The specified Jenkins Credentials are used. This can be either user/password or user/API Token.
  • +
  • Bearer Authentication
    + The bearer token is used. +
  • Note: Jenkins API Tokens are recommended since, if stolen, they allow access only to a specific Jenkins diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth/config.jelly new file mode 100644 index 00000000..0a61b056 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth/config.jelly @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java index 65e8673d..8e5a2018 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -19,15 +19,15 @@ public class Auth2Test { @Test public void testBearerTokenAuthCloneBehaviour() throws CloneNotSupportedException { BearerTokenAuth original = new BearerTokenAuth(); - original.setToken("original"); + original.setToken(Secret.fromString("original")); BearerTokenAuth clone = (BearerTokenAuth)original.clone(); verifyEqualsHashCode(original, clone); //Test changing clone - clone.setToken("changed"); + clone.setToken(Secret.fromString("changed")); verifyEqualsHashCode(original, clone, false); - assertEquals("original", original.getToken()); - assertEquals("changed", clone.getToken()); + assertEquals("original", original.getToken().getPlainText()); + assertEquals("changed", clone.getToken().getPlainText()); } @Test From 33065c3e5f12a208bd5f08a4c436b765d115615d Mon Sep 17 00:00:00 2001 From: Louis Lecaroz Date: Mon, 10 Aug 2020 22:47:13 +0200 Subject: [PATCH 196/262] retrieve only needed "doGetting" build fields As the author of this PR https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/55 doesn't answer to the proposed fix in his fork, here is the appropriate change fixing the infinite loop due to the "building" status which was not retrieved in the original PR. Retrieving only requested fields meaning result & building which are used later in the code will greatly improve performances specially in large builds archiving lot of files or others contents in the returned build api/json payload as all these others informations are not needed & will increase the json marshaling & transfer time sometimes to some MB & lot of minutes. As side effect, without that optimization if the get call spends more than the timeout duration due to the size of data , it will ends with some failed retries & a final failure status while everything was smooth/ok on the jenkins server side. --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 0c05eb45..324c7493 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -879,7 +879,7 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn } // Only avoid url cache while loop inquiry - String buildUrlString = String.format("%sapi/json/?seed=%d", buildInfo.getBuildURL(), + String buildUrlString = String.format("%sapi/json/?tree=result,building&seed=%d", buildInfo.getBuildURL(), System.currentTimeMillis()); JSONObject responseObject = doGet(buildUrlString, context, buildInfo.getStatus()).getBody(); From 009f2a8ffaed40de72d5d4dd718c74fe1f433461 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Thu, 20 Aug 2020 22:52:14 +0800 Subject: [PATCH 197/262] Update change log --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50773bbf..77e99c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 3.1.4 (Aug 20th, 2020) + +### Improvement + +* Update git repo in pom ([2b35305](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/72)) +* Retrieve only needed "doGetting" build fields. ([33065c3](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/74)) +* Fix https://issues.jenkins-ci.org/browse/SECURITY-1625 + # 3.1.3 (May 10th, 2020) ### Improvement From 43f13e66921d7d2e61d92de3601eebddc3d28745 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 21 Aug 2020 00:22:18 +0800 Subject: [PATCH 198/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fe6913c7..d0078522 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.4-SNAPSHOT + 3.1.4 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.4 From c139e84da04d55fca6330d10ec18c3a37ff42bdd Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 21 Aug 2020 00:22:30 +0800 Subject: [PATCH 199/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d0078522..34ac36cc 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ Parameterized-Remote-Trigger - 3.1.4 + 3.1.5-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -49,7 +49,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.4 + HEAD From 224817e9765e83db2592fa941f0f4e7d205277b3 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 4 Oct 2020 20:18:52 +0800 Subject: [PATCH 200/262] reformat pom & add breaking change for 3.1.4 --- pom.xml | 173 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 88 insertions(+), 85 deletions(-) diff --git a/pom.xml b/pom.xml index 34ac36cc..aac4690a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,100 +1,103 @@ - - 4.0.0 - - org.jenkins-ci.plugins - plugin - 3.50 - + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 3.50 + 1.642.3 8 + 3.1.4 - Parameterized-Remote-Trigger - 3.1.5-SNAPSHOT - hpi - Parameterized Remote Trigger Plugin - This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. - https://github.com/jenkinsci/parameterized-remote-trigger-plugin + Parameterized-Remote-Trigger + 3.1.5-SNAPSHOT + hpi + Parameterized Remote Trigger Plugin + This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. + https://github.com/jenkinsci/parameterized-remote-trigger-plugin - - - MIT license - All source code is under the MIT license. - - + + + MIT license + All source code is under the MIT license. + + - - - cashlalala - KaiHsiang Chang - - + + + cashlalala + KaiHsiang Chang + + - - - - org.jenkins-ci.tools - maven-hpi-plugin - - - 3.0.4-SNAPSHOT - - - - + + + + org.jenkins-ci.tools + maven-hpi-plugin + + + 3.0.4-SNAPSHOT + + + + - - scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git - scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git - https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD - + + scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git + scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git + https://github.com/jenkinsci/parameterized-remote-trigger-plugin + HEAD + - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + - - - org.jenkins-ci.plugins - credentials - 2.1.16 - - - org.jenkins-ci.plugins - token-macro - 2.3 - - - org.jenkins-ci.plugins - script-security - 1.34 - true - - - org.jenkins-ci.plugins.workflow - workflow-step-api - 2.13 - true - - - org.mockito - mockito-core - 2.18.3 - test - - + + + org.jenkins-ci.plugins + credentials + 2.1.16 + + + org.jenkins-ci.plugins + token-macro + 2.3 + + + org.jenkins-ci.plugins + script-security + 1.34 + true + + + org.jenkins-ci.plugins.workflow + workflow-step-api + 2.13 + true + + + org.mockito + mockito-core + 2.18.3 + test + + From c05146c9af4e5c3afc65654c31f8c326790fc5ed Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 5 Oct 2020 00:24:14 +0800 Subject: [PATCH 201/262] [JENKINS-63819] Add maxConn default value & value checking --- .../RemoteBuildConfiguration.java | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 324c7493..8e830a5d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -96,8 +96,8 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private static final int DEFAULT_HTTP_POST_READ_TIMEOUT = 30000; /** - * The TTL value of all `queued` items is only 5 minutes. - * That is why we have to ignore user specified poll interval for such items. + * The TTL value of all `queued` items is only 5 minutes. That is why we have to + * ignore user specified poll interval for such items. */ private static final int QUEUED_ITEMS_POLLINTERVALL = 30; @@ -128,7 +128,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean enhancedLogging; private boolean loadParamsFromFile; private String parameterFile; - private int maxConn; + private int maxConn = 1; private boolean useCrumbCache; private boolean useJobInfoCache; private boolean abortTriggeredJob; @@ -189,7 +189,7 @@ public void setAbortTriggeredJob(boolean abortTriggeredJob) { @DataBoundSetter public void setMaxConn(int maxConn) { - this.maxConn = (maxConn > 5) ? 5 : maxConn; + this.maxConn = (maxConn > 5) ? 5 : (maxConn < 1) ? 1 : maxConn; } @DataBoundSetter @@ -695,8 +695,8 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti try { ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, - this.getHttpPostReadTimeout(), this.getPollInterval(buildInfo.getStatus()), this.getConnectionRetryLimit(), - this.getAuth2(), getLock(triggerUrlString), isUseCrumbCache()); + this.getHttpPostReadTimeout(), this.getPollInterval(buildInfo.getStatus()), + this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), isUseCrumbCache()); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); buildInfo.setQueueId(queueItem.getId()); buildInfo = updateBuildInfo(buildInfo, context); @@ -733,7 +733,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx } while (buildInfo.isQueued()) { - context.logger.println("Waiting for " + this.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); + context.logger.println( + "Waiting for " + this.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); Thread.sleep(this.getPollInterval(buildInfo.getStatus()) * 1000); buildInfo = updateBuildInfo(buildInfo, context); handle.setBuildInfo(buildInfo); @@ -776,7 +777,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx if (this.getEnhancedLogging()) { consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); } else { - context.logger.println(" Waiting for " + this.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); + context.logger.println(" Waiting for " + this.getPollInterval(buildInfo.getStatus()) + + " seconds until next poll."); } Thread.sleep(this.getPollInterval(buildInfo.getStatus()) * 1000); buildInfo = updateBuildInfo(buildInfo, context); @@ -872,7 +874,8 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn if (queueItem.isExecuted()) { URL remoteBuildURL = queueItem.getBuildURL(); String effectiveRemoteServerAddress = context.effectiveRemoteServer.getAddress(); - URL effectiveRemoteBuildURL = generateEffectiveRemoteBuildURL(remoteBuildURL, effectiveRemoteServerAddress); + URL effectiveRemoteBuildURL = generateEffectiveRemoteBuildURL(remoteBuildURL, + effectiveRemoteServerAddress); buildInfo.setBuildData(queueItem.getBuildNumber(), effectiveRemoteBuildURL); } return buildInfo; @@ -899,19 +902,16 @@ public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonn return buildInfo; } - protected static URL generateEffectiveRemoteBuildURL(URL remoteBuildURL, String effectiveRemoteServerAddress) throws AbortException { + protected static URL generateEffectiveRemoteBuildURL(URL remoteBuildURL, String effectiveRemoteServerAddress) + throws AbortException { if (effectiveRemoteServerAddress == null || remoteBuildURL == null) { return remoteBuildURL; } try { URI effectiveUri = new URI(effectiveRemoteServerAddress); - return new URL( - effectiveUri.getScheme(), - effectiveUri.getHost(), - effectiveUri.getPort(), - remoteBuildURL.getPath() - ); + return new URL(effectiveUri.getScheme(), effectiveUri.getHost(), effectiveUri.getPort(), + remoteBuildURL.getPath()); } catch (URISyntaxException | MalformedURLException ex) { throw new AbortException(String.format("Unexpected syntax error: %s.", ex.toString())); } @@ -942,18 +942,19 @@ private String printOffsetConsoleOutput(BuildContext context, String offset, Rem * Orchestrates all calls to the remote server. Also takes care of any * credentials or failed-connection retries. * - * @param urlString the URL that needs to be called. - * @param context the context of this Builder/BuildStep. + * @param urlString the URL that needs to be called. + * @param context the context of this Builder/BuildStep. * @param remoteBuildStatus the build status of a remote build. * @return JSONObject a valid JSON object, or null. * @throws InterruptedException if any thread has interrupted the current * thread. * @throws IOException if any HTTP error occurred. */ - public ConnectionResponse doGet(String urlString, BuildContext context, RemoteBuildStatus remoteBuildStatus) throws IOException, InterruptedException { + public ConnectionResponse doGet(String urlString, BuildContext context, RemoteBuildStatus remoteBuildStatus) + throws IOException, InterruptedException { return HttpHelper.tryGet(urlString, context, this.getHttpGetReadTimeout(), - this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), - this.getAuth2(), getLock(urlString)); + this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), this.getAuth2(), + getLock(urlString)); } private void logAuthInformation(BuildContext context) throws IOException { @@ -1074,11 +1075,11 @@ public boolean getPreventRemoteBuildQueue() { public int getPollInterval(RemoteBuildStatus remoteBuildStatus) { switch (remoteBuildStatus) { - case NOT_TRIGGERED: - case QUEUED: - return QUEUED_ITEMS_POLLINTERVALL; - default: - return pollInterval; + case NOT_TRIGGERED: + case QUEUED: + return QUEUED_ITEMS_POLLINTERVALL; + default: + return pollInterval; } } From 31e01aea4ed02c1e82cc1eac90931ce357fbc584 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 5 Oct 2020 01:12:48 +0800 Subject: [PATCH 202/262] update change log --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e99c47..29d950e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.1.5 (Oct 5th, 2020) + +### Improvement + +* [JENKINS-63819] Add maxConn default value & value checking +* The plain text tokens are not allowed to set from pipeline directly since 3.1.4, mark builds after 3.1.4 for breaking changes. + # 3.1.4 (Aug 20th, 2020) ### Improvement From 1e9f33b713158edef216264c077fa590362792a1 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 5 Oct 2020 01:19:56 +0800 Subject: [PATCH 203/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.5 --- pom.xml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index aac4690a..8ddf3c6f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,4 @@ - + 4.0.0 org.jenkins-ci.plugins @@ -15,7 +13,7 @@ Parameterized-Remote-Trigger - 3.1.5-SNAPSHOT + 3.1.5 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +50,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.5 From 33265496a4f289c8ccc34f557d765ac17d9f936c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 5 Oct 2020 01:20:07 +0800 Subject: [PATCH 204/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8ddf3c6f..bcaa4e97 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ Parameterized-Remote-Trigger - 3.1.5 + 3.1.6-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -50,7 +50,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.5 + HEAD From 86630868cf0d105456316d474821b01d6edfdba7 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 6 Oct 2020 21:23:58 +0800 Subject: [PATCH 205/262] remove old build plugins --- pom.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index bcaa4e97..2752bf5e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,8 @@ 1.642.3 8 - 3.1.4 + + 3.1.4-SNAPSHOT Parameterized-Remote-Trigger @@ -35,14 +36,15 @@ + - + 3.0.4-SNAPSHOT - + + --> From cb8f5fee499af5d6549d2fa0f8501deb04418e03 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 6 Oct 2020 21:35:10 +0800 Subject: [PATCH 206/262] update change log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d950e5..c0217909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.5.1 (Oct 6th, 2020) + +### Improvement + +* The plain text tokens are not allowed to set from pipeline directly since 3.1.4, mark builds after 3.1.4 for breaking changes. Remove old pom settings. + # 3.1.5 (Oct 5th, 2020) ### Improvement From 7f7e9be10d67c891031dfbc54263b4a2bbad05b4 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 6 Oct 2020 21:42:39 +0800 Subject: [PATCH 207/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.5.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 2752bf5e..d9762462 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6-SNAPSHOT + 3.1.5.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.5.1 From ef969679369894c5dd1bbb2502ae11023562f845 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 6 Oct 2020 21:42:50 +0800 Subject: [PATCH 208/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d9762462..2752bf5e 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.5.1 + 3.1.6-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.5.1 + HEAD From 421b6dd89f33e938048bec816be80755302a1ee4 Mon Sep 17 00:00:00 2001 From: LECAROZ Date: Thu, 21 Jan 2021 20:01:27 +0100 Subject: [PATCH 209/262] Adding exception message on sendHTTPCall failure First of all, only HTTP Error code was displayed on retries. In case of no error code, the exception message is also displayed. Also, on the final failure/last retry, an error message is now also reported in the job console --- .../ParameterizedRemoteTrigger/utils/HttpHelper.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 87e70852..a6a43580 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -549,8 +549,8 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // If we have connectionRetryLimit set to > 0 then retry that many times. if (numberOfAttempts <= retryLimit) { context.logger.println(String.format( - "Connection to remote server failed %s, waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s", - (responseCode == 0 ? "" : "[" + responseCode + "]"), pollInterval, + "Connection to remote server failed [%s], waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s", + (responseCode == 0 ? e.getMessage() : responseCode), pollInterval, getUrlWithoutParameters(urlString), parmsString)); // Sleep for 'pollInterval' seconds. @@ -565,6 +565,11 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT numberOfAttempts, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); } else { + context.logger.println(String.format( + "Connection to remote server failed [%s], number of retries exceeded. URL: %s, parameters: %s", + (responseCode == 0 ? e.getMessage() : responseCode), + getUrlWithoutParameters(urlString), parmsString)); + // reached the maximum number of retries, time to fail throw new ExceedRetryLimitException(); } From fef4451ad0c743d47f3282e75dfb4c9d1ffbb3c3 Mon Sep 17 00:00:00 2001 From: "Artem V. Navrotskiy" Date: Thu, 2 Sep 2021 09:54:55 +0300 Subject: [PATCH 210/262] Allow use `triggerRemoteJob` step with `agent none` Looks like agent used only if `loadParamsFromFile` attribute defined. --- .../pipeline/RemoteBuildPipelineStep.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index ed4aa3c4..645f9b4a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -54,7 +54,6 @@ import hudson.Extension; import hudson.ExtensionList; import hudson.FilePath; -import hudson.Launcher; import hudson.model.Descriptor; import hudson.model.Run; import hudson.model.TaskListener; @@ -200,7 +199,7 @@ public String getDisplayName() { @Override public Set> getRequiredContext() { Set> set = new HashSet>(); - Collections.addAll(set, Run.class, FilePath.class, Launcher.class, TaskListener.class); + Collections.addAll(set, Run.class, TaskListener.class); return set; } From e0176bc86eca1e056f982711da863b10f76c476a Mon Sep 17 00:00:00 2001 From: "Artem V. Navrotskiy" Date: Fri, 3 Sep 2021 08:28:03 +0300 Subject: [PATCH 211/262] Replace warning by error This is not breaking change: this code path was unreachable because this step previously required an agent. --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 8e830a5d..ed6adcac 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -341,7 +341,7 @@ private List loadExternalParameterFile(BuildContext context) { parameterList.add(sCurrentLine); } } else { - context.logger.println("[WARNING] workspace is null"); + throw new AbortException("Workspace is null but parameter file is used. Looks like this step was started with \"agent: none\""); } } catch (InterruptedException | IOException e) { context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); From ba60b56eb5495a7aa99fed0530b04bf87270d6fb Mon Sep 17 00:00:00 2001 From: David Schumann Date: Fri, 28 Jan 2022 12:31:02 +0100 Subject: [PATCH 212/262] Update README adding updateBuildStatus() --- README_PipelineConfiguration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_PipelineConfiguration.md b/README_PipelineConfiguration.md index b5a14d28..a915c6d5 100644 --- a/README_PipelineConfiguration.md +++ b/README_PipelineConfiguration.md @@ -99,6 +99,7 @@ The `Handle` object provides the following methods: - `RemoteBuildStatus getBuildStatus()` returns the current remote build status - `Result getBuildResult()` return the result of the remote build - `RemoteBuildStatus updateBuildStatusBlocking()` waits for completion and returns the build result +- `RemoteBuildStatus updateBuildStatus()` update the handles build status. Required for getBuildInfo() etc. to yield updated results. - `boolean isFinished()` true if the remote build finished - `boolean isQueued()` true if the job is queued but not yet running - `String toString()` From 9b07f4fa9a9d2485b66cfb88d824e5b87a152726 Mon Sep 17 00:00:00 2001 From: quilicicf Date: Fri, 3 Jun 2022 11:43:37 +0200 Subject: [PATCH 213/262] Introduce failing test It should be fixed by changing the code only since it represents the expected output --- .../RemoteBuildConfiguration.java | 8 +-- .../RemoteBuildConfigurationTest.java | 61 +++++++++++++++---- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index ed6adcac..5b644988 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -106,7 +106,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep /** * We need to keep this for compatibility - old config deserialization! - * + * * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. */ private transient List auth; @@ -373,7 +373,7 @@ private void removeEmptyElements(Collection collection) { * @param List parameters * @return List of build parameters */ - private List getCleanedParameters(List parameters) { + List getCleanedParameters(List parameters) { List params = new ArrayList(parameters); removeEmptyElements(params); removeCommentsFromParameters(params); @@ -660,7 +660,7 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task * @throws IOException if there is an error triggering the remote job. * @throws InterruptedException if any thread has interrupted the current * thread. - * + * */ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOException, InterruptedException { List cleanedParams = getCleanedParameters(getParameterList(context)); @@ -1276,7 +1276,7 @@ public static DescriptorImpl newInstanceForTests() { * /** 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. */ /* diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index b4a58975..8d3cb8de 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -1,5 +1,8 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import static java.lang.String.join; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toMap; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -13,6 +16,7 @@ import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; +import java.util.AbstractMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,13 +39,13 @@ import hudson.EnvVars; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; +import hudson.model.ListView; import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; import hudson.model.User; -import hudson.model.ListView; +import hudson.security.AuthorizationStrategy.Unsecured; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.SecurityRealm; -import hudson.security.AuthorizationStrategy.Unsecured; import hudson.security.csrf.DefaultCrumbIssuer; import hudson.util.LogTaskListener; import hudson.util.Secret; @@ -64,16 +68,16 @@ private void disableAuth() { private void enableAuth() throws IOException { MockAuthorizationStrategy mockAuth = new MockAuthorizationStrategy(); jenkinsRule.jenkins.setAuthorizationStrategy(mockAuth); - + HudsonPrivateSecurityRealm hudsonPrivateSecurityRealm = new HudsonPrivateSecurityRealm(false, false, null); jenkinsRule.jenkins.setSecurityRealm(hudsonPrivateSecurityRealm); //jenkinsRule.createDummySecurityRealm()); testUser = hudsonPrivateSecurityRealm.createAccount("test", "test"); testUserToken = testUser.getProperty(jenkins.security.ApiTokenProperty.class).getApiToken(); - + mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); } - - + + @Test public void testRemoteBuild() throws Exception { disableAuth(); @@ -99,7 +103,7 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle parms.put("parameterName2", "value2"); this._testRemoteBuild(authenticate, withParam, remoteProject, parms); } - + private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject, Map parms) throws Exception { String remoteUrl = jenkinsRule.getURL().toString(); @@ -142,20 +146,20 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle //Trigger build jenkinsRule.waitUntilNoActivity(); jenkinsRule.buildAndAssertSuccess(project); - + //Check results FreeStyleBuild lastBuild2 = project.getLastBuild(); assertNotNull(lastBuild2); List log = IOUtils.readLines(lastBuild2.getLogInputStream()); assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "anonymous") + ", Building in workspace")); - + FreeStyleBuild lastBuild = remoteProject.getLastBuild(); assertNotNull("lastBuild null", lastBuild); if (withParam){ EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); for (Map.Entry p : parms.entrySet()) { assertEquals(p.getValue(), remoteEnv.get(p.getKey())); - } + } } else { assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); } @@ -517,7 +521,7 @@ public void testRemoteFolderedBuild() throws Exception { this._testRemoteBuild(false, true, remoteProject); } - + @Test public void testRemoteFolderedBuildWithoutParameters() throws Exception { disableAuth(); @@ -526,7 +530,7 @@ public void testRemoteFolderedBuildWithoutParameters() throws Exception { FreeStyleProject remoteProject = remoteJobFolder.createProject(FreeStyleProject.class, "someJobName1"); this._testRemoteBuild(false, false, remoteProject); } - + @Test public void testRemoteBuildWith5KByteString() throws Exception { enableAuth(); @@ -551,4 +555,37 @@ public void testRemoteViewBuild() throws Exception { _testRemoteBuild(false, false, remoteProject); } + @Test @WithoutJenkins + public void testParseStringParameters() { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + final String parameters = join("\n", asList( + "# bla bla", // Comment + "", // Empty line + " ", // Empty line (if trimmed) + "PARAM1=toto", // Simple parameter + "PARAM2=dG90bwo=", // Parameter with = sign (see https://issues.jenkins.io/browse/JENKINS-58818) + "PARAM3=line1", // Multi-line parameter + "line2", // Multi-line parameter + "line3" // Multi-line parameter + )); + config.setParameters(parameters); + + final Map expectedParametersMap = new HashMap<>(); + expectedParametersMap.put("PARAM1", "toto"); + expectedParametersMap.put("PARAM2", "dG90bwo="); // This one should fail because it's broken (see issue 58818) + expectedParametersMap.put("PARAM3", "line1"); // We keep the wrong value in the test because it's unfixable with string parameters + + final List actualParameterList = config.getCleanedParameters( + config.getParameterList(null) + ); + final Map actualParametersMap = actualParameterList.stream() + .map(line -> { + final String[] splitLine = line.split("="); + return new AbstractMap.SimpleEntry<>(splitLine[0], splitLine.length > 1 ? splitLine[1] : ""); + }) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + + assertEquals(expectedParametersMap, actualParametersMap); + } + } From f1eb49cf6294b77dae9361a4176d60d8591f40b7 Mon Sep 17 00:00:00 2001 From: quilicicf Date: Fri, 3 Jun 2022 11:49:58 +0200 Subject: [PATCH 214/262] Improve parameters handling --- .../RemoteBuildConfiguration.java | 256 ++---- .../parameters2/FileParameters.java | 118 +++ .../parameters2/JobParameters.java | 82 ++ .../parameters2/MapParameter.java | 86 ++ .../parameters2/MapParameters.java | 101 +++ .../parameters2/StringParameters.java | 81 ++ .../pipeline/RemoteBuildPipelineStep.java | 50 +- .../utils/HttpHelper.java | 114 +-- .../utils/TokenMacroUtils.java | 32 +- .../RemoteBuildConfiguration/config.jelly | 28 +- .../parameters2/FileParameters/config.jelly | 8 + .../parameters2/MapParameter/config.jelly | 11 + .../parameters2/MapParameters/config.jelly | 14 + .../parameters2/StringParameters/config.jelly | 8 + .../RemoteBuildConfigurationTest.java | 857 +++++++++--------- 15 files changed, 1092 insertions(+), 754 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters/config.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters/config.jelly diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 5b644988..72e7720c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,15 +1,20 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; import static java.lang.Math.min; +import static java.util.Collections.singletonMap; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; -import static org.apache.commons.lang.StringUtils.stripAll; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; -import java.io.BufferedReader; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.sf.json.JSONObject; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNullableByDefault; import java.io.IOException; -import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Serializable; import java.net.CookieHandler; @@ -18,9 +23,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,15 +30,13 @@ import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNullableByDefault; - import org.apache.commons.lang.exception.ExceptionUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters.ParametersDescriptor; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItem; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.QueueItemData; @@ -64,7 +64,6 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; -import hudson.model.Item; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; @@ -74,12 +73,9 @@ import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.tasks.SimpleBuildStep; -import net.sf.json.JSONObject; /** - * * @author Maurice W. - * */ @ParametersAreNullableByDefault public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep, Serializable { @@ -91,6 +87,7 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep * override potential global config */ private final static Auth2 DEFAULT_AUTH = NullAuth.INSTANCE; + private final static JobParameters DEFAULT_PARAMETERS = new MapParameters(); private static final int DEFAULT_HTTP_GET_READ_TIMEOUT = 10000; private static final int DEFAULT_HTTP_POST_READ_TIMEOUT = 30000; @@ -109,11 +106,14 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep * * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. */ + @SuppressWarnings("DeprecatedIsStillUsed") private transient List auth; private String remoteJenkinsName; private String remoteJenkinsUrl; private Auth2 auth2; + + private JobParameters parameters2; private boolean shouldNotFailBuild; private boolean trustAllCertificates; private boolean overrideTrustAllCertificates; @@ -124,9 +124,20 @@ public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep private boolean blockBuildUntilComplete; private String job; private String token; + /** + * We need to keep this for compatibility - old config deserialization! + * + * @deprecated since 3.1.6-SNAPSHOT - use {@link JobParameters} instead. + */ + @SuppressWarnings("DeprecatedIsStillUsed") private String parameters; private boolean enhancedLogging; - private boolean loadParamsFromFile; + /** + * We need to keep this for compatibility - old config deserialization! + * + * @deprecated since 3.1.6-SNAPSHOT - use {@link JobParameters} instead. + */ + @SuppressWarnings("DeprecatedIsStillUsed") private String parameterFile; private int maxConn = 1; private boolean useCrumbCache; @@ -163,6 +174,14 @@ protected Object readResolve() { } } auth = null; + + // migrate parameters/parameterFile to JobParameters + if (parameters2 == null) { + parameters2 = JobParameters.migrateOldParameters(parameters, parameterFile); + } + parameters = null; + parameterFile = null; + if (hostLocks == null) { hostLocks = new HashMap<>(); } @@ -189,7 +208,7 @@ public void setAbortTriggeredJob(boolean abortTriggeredJob) { @DataBoundSetter public void setMaxConn(int maxConn) { - this.maxConn = (maxConn > 5) ? 5 : (maxConn < 1) ? 1 : maxConn; + this.maxConn = (maxConn > 5) ? 5 : Math.max(maxConn, 1); } @DataBoundSetter @@ -209,6 +228,13 @@ public void setAuth2(Auth2 auth) { this.auth = null; } + @DataBoundSetter + public void setParameters2(JobParameters parameters2) { + this.parameters2 = parameters2; + parameters = null; // Disable old parameters + parameterFile = null; // Disable old parameters + } + @DataBoundSetter public void setShouldNotFailBuild(boolean shouldNotFailBuild) { this.shouldNotFailBuild = shouldNotFailBuild; @@ -274,22 +300,6 @@ public void setEnhancedLogging(boolean enhancedLogging) { this.enhancedLogging = enhancedLogging; } - @DataBoundSetter - public void setLoadParamsFromFile(boolean loadParamsFromFile) { - this.loadParamsFromFile = loadParamsFromFile; - } - - @DataBoundSetter - public void setParameterFile(String parameterFile) { - if (loadParamsFromFile && (parameterFile == null || parameterFile.isEmpty())) - throw new IllegalArgumentException("Parameter file path is empty"); - - if (parameterFile == null) - this.parameterFile = ""; - else - this.parameterFile = parameterFile; - } - @DataBoundSetter public void setDisabled(boolean disabled) { this.disabled = disabled; @@ -305,94 +315,9 @@ public void setUseCrumbCache(boolean useCrumbCache) { this.useCrumbCache = useCrumbCache; } - public List getParameterList(BuildContext context) { - String params = getParameters(); - if (!params.isEmpty()) { - String[] parameterArray = params.split("\n"); - parameterArray = stripAll(parameterArray); - return new ArrayList(Arrays.asList(parameterArray)); - } else if (loadParamsFromFile) { - return loadExternalParameterFile(context); - } else { - return new ArrayList(); - } - } - - /** - * Reads a file from the jobs workspace, and loads the list of parameters from - * with in it. It will also call ```getCleanedParameters``` before returning. - * - * @param BuildContext context - * @return List of build parameters - */ - private List loadExternalParameterFile(BuildContext context) { - - BufferedReader br = null; - List parameterList = new ArrayList(); - try { - if (context.workspace != null) { - FilePath filePath = context.workspace.child(getParameterFile()); - String sCurrentLine; - context.logger.println(String.format("Loading parameters from file %s", filePath.getRemote())); - - br = new BufferedReader(new InputStreamReader(filePath.read(), "UTF-8")); - - while ((sCurrentLine = br.readLine()) != null) { - parameterList.add(sCurrentLine); - } - } else { - throw new AbortException("Workspace is null but parameter file is used. Looks like this step was started with \"agent: none\""); - } - } catch (InterruptedException | IOException e) { - context.logger.println(String.format("[WARNING] Failed loading parameters: %s", e.getMessage())); - } finally { - try { - if (br != null) { - br.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - return getCleanedParameters(parameterList); - } - - /** - * Strip out any empty strings from the parameterList - */ - private void removeEmptyElements(Collection collection) { - collection.removeAll(Arrays.asList(null, "")); - collection.removeAll(Arrays.asList(null, " ")); - } - - /** - * Same as "getParameterList", but removes comments and empty strings Notice - * that no type of character encoding is happening at this step. All encoding - * happens in the "buildUrlQueryString" method. - * - * @param List parameters - * @return List of build parameters - */ - List getCleanedParameters(List parameters) { - List params = new ArrayList(parameters); - removeEmptyElements(params); - removeCommentsFromParameters(params); - return params; - } - - /** - * Strip out any comments (lines that start with a #) from the collection that - * is passed in. - */ - private void removeCommentsFromParameters(Collection collection) { - List itemsToRemove = new ArrayList(); - - for (String parameter : collection) { - if (parameter.indexOf("#") == 0) { - itemsToRemove.add(parameter); - } - } - collection.removeAll(itemsToRemove); + public Map getParameterMap(BuildContext context) throws AbortException { + return getParameters2() + .getParametersMap(context); } /** @@ -662,24 +587,24 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task * thread. * */ - public Handle performTriggerAndGetQueueId(BuildContext context) throws IOException, InterruptedException { - List cleanedParams = getCleanedParameters(getParameterList(context)); + public Handle performTriggerAndGetQueueId(@NonNull BuildContext context) throws IOException, InterruptedException { + Map parameters = getParameterMap(context); String jobNameOrUrl = this.getJob(); String securityToken = this.getToken(); try { - cleanedParams = TokenMacroUtils.applyTokenMacroReplacements(cleanedParams, context); + parameters = TokenMacroUtils.applyTokenMacroReplacements(parameters, context); jobNameOrUrl = TokenMacroUtils.applyTokenMacroReplacements(jobNameOrUrl, context); securityToken = TokenMacroUtils.applyTokenMacroReplacements(securityToken, context); } catch (IOException e) { this.failBuild(e, context.logger); } - logConfiguration(context, cleanedParams); + logConfiguration(context, parameters); final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); - final String triggerUrlString = HttpHelper.buildTriggerUrl(jobNameOrUrl, securityToken, null, + String triggerUrlString = HttpHelper.buildTriggerUrl(jobNameOrUrl, securityToken, isRemoteParameterized, context); // token shouldn't be exposed in the console @@ -694,7 +619,7 @@ public Handle performTriggerAndGetQueueId(BuildContext context) throws IOExcepti context.logger.println("Triggering remote job now."); try { - ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, cleanedParams, + ConnectionResponse responseRemoteJob = HttpHelper.tryPost(triggerUrlString, context, parameters, this.getHttpPostReadTimeout(), this.getPollInterval(buildInfo.getStatus()), this.getConnectionRetryLimit(), this.getAuth2(), getLock(triggerUrlString), isUseCrumbCache()); QueueItem queueItem = new QueueItem(responseRemoteJob.getHeader()); @@ -957,24 +882,24 @@ public ConnectionResponse doGet(String urlString, BuildContext context, RemoteBu getLock(urlString)); } - private void logAuthInformation(BuildContext context) throws IOException { + private void logAuthInformation(@NonNull BuildContext context) { Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); Auth2 localAuth = this.getAuth2(); if (localAuth != null && !(localAuth instanceof NullAuth)) { String authString = (context.run == null) ? localAuth.getDescriptor().getDisplayName() - : localAuth.toString((Item) context.run.getParent()); - context.logger.println(String.format(" Using job-level defined " + authString)); + : localAuth.toString(context.run.getParent()); + context.logger.printf(" Using job-level defined " + authString + "%n"); } else if (serverAuth != null && !(serverAuth instanceof NullAuth)) { String authString = (context.run == null) ? serverAuth.getDescriptor().getDisplayName() - : serverAuth.toString((Item) context.run.getParent()); - context.logger.println(String.format(" Using globally defined " + authString)); + : serverAuth.toString(context.run.getParent()); + context.logger.printf(" Using globally defined " + authString + "%n"); } else { context.logger.println(" No credentials configured"); } } - private void logConfiguration(BuildContext context, List effectiveParams) throws IOException { + private void logConfiguration(@Nonnull BuildContext context, Map effectiveParams) throws IOException { String _job = getJob(); String _jobExpanded = getJobExpanded(context); String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; @@ -985,32 +910,26 @@ private void logConfiguration(BuildContext context, List effectiveParams Auth2 _auth = getAuth2(); int _connectionRetryLimit = getConnectionRetryLimit(); boolean _blockBuildUntilComplete = getBlockBuildUntilComplete(); - String _parameterFile = getParameterFile(); - String _parameters = (effectiveParams == null || effectiveParams.size() <= 0) ? "" : effectiveParams.toString(); - boolean _loadParamsFromFile = getLoadParamsFromFile(); context.logger.println( "################################################################################################################"); context.logger.println(" Parameterized Remote Trigger Configuration:"); - context.logger.println(String.format(" - job: %s %s", _job, _jobExpandedLogEntry)); + context.logger.printf(" - job: %s %s%n", _job, _jobExpandedLogEntry); if (!isEmpty(_remoteJenkinsName)) { - context.logger.println(String.format(" - remoteJenkinsName: %s", _remoteJenkinsName)); + context.logger.printf(" - remoteJenkinsName: %s%n", _remoteJenkinsName); } if (!isEmpty(_remoteJenkinsUrl)) { - context.logger.println(String.format(" - remoteJenkinsUrl: %s", _remoteJenkinsUrl)); + context.logger.printf(" - remoteJenkinsUrl: %s%n", _remoteJenkinsUrl); } if (_auth != null && !(_auth instanceof NullAuth)) { - String authString = context.run == null ? _auth.getDescriptor().getDisplayName() - : _auth.toString((Item) context.run.getParent()); - context.logger.println(String.format(" - auth: %s", authString)); - } - context.logger.println(String.format(" - parameters: %s", _parameters)); - if (_loadParamsFromFile) { - context.logger.println(String.format(" - loadParamsFromFile: %s", _loadParamsFromFile)); - context.logger.println(String.format(" - parameterFile: %s", _parameterFile)); + final String authString = context.run == null + ? _auth.getDescriptor().getDisplayName() + : _auth.toString(context.run.getParent()); + context.logger.printf(" - auth: %s%n", authString); } - context.logger.println(String.format(" - blockBuildUntilComplete: %s", _blockBuildUntilComplete)); - context.logger.println(String.format(" - connectionRetryLimit: %s", _connectionRetryLimit)); - context.logger.println(String.format(" - trustAllCertificates: %s", _trustAllCertificates)); + context.logger.printf(" - parameters: %s%n", effectiveParams); + context.logger.printf(" - blockBuildUntilComplete: %s%n", _blockBuildUntilComplete); + context.logger.printf(" - connectionRetryLimit: %s%n", _connectionRetryLimit); + context.logger.printf(" - trustAllCertificates: %s%n", _trustAllCertificates); context.logger.println( "################################################################################################################"); } @@ -1107,20 +1026,12 @@ public String getToken() { return trimToEmpty(token); } - public String getParameters() { - return trimToEmpty(parameters); - } - public boolean getEnhancedLogging() { return enhancedLogging; } - public boolean getLoadParamsFromFile() { - return loadParamsFromFile; - } - - public String getParameterFile() { - return trimToEmpty(parameterFile); + public JobParameters getParameters2() { + return (parameters2 != null) ? parameters2 : DEFAULT_PARAMETERS; } public int getConnectionRetryLimit() { @@ -1131,12 +1042,12 @@ public boolean isDisabled() { return disabled; } - private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, BuildContext context) + private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, @NonNull BuildContext context) throws IOException, InterruptedException { String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - remoteJobUrl += "/api/json?" + HttpHelper.buildUrlQueryString(Arrays.asList( - "tree=actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url")); + Map parameters = singletonMap("tree", "actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url"); + remoteJobUrl += "/api/json?" + HttpHelper.buildUrlQueryString(parameters); JSONObject jsonObject = DropCachePeriodicWork.safeGetJobInfo(remoteJobUrl, isUseJobInfoCache()); if (jsonObject != null) { @@ -1168,7 +1079,7 @@ public boolean isDisabled() { * @throws IOException if it is not possible to identify if the job is * parameterized. */ - private boolean isRemoteJobParameterized(JSONObject remoteJobMetadata) throws IOException { + private boolean isRemoteJobParameterized(final JSONObject remoteJobMetadata) throws IOException { boolean isParameterized = false; if (remoteJobMetadata != null) { if (remoteJobMetadata.getJSONArray("actions").size() >= 1) { @@ -1253,7 +1164,7 @@ public static final class DescriptorImpl extends BuildStepDescriptor { *

    * If you don't want fields to be persisted, use transient. */ - private CopyOnWriteList remoteSites = new CopyOnWriteList(); + private CopyOnWriteList remoteSites = new CopyOnWriteList<>(); /** * In order to load the persisted global configuration, you have to call load() @@ -1287,15 +1198,18 @@ public static DescriptorImpl newInstanceForTests() { * FormValidation.ok(); } */ - public boolean isApplicable(@SuppressWarnings("rawtypes") Class aClass) { + @Override + public boolean isApplicable(Class aClass) { // Indicates that this builder can be used with all kinds of project // types return true; } /** - * This human readable name is used in the configuration screen. + * This human-readable name is used in the configuration screen. */ + @NonNull + @Override public String getDisplayName() { return "Trigger a remote parameterized job"; } @@ -1376,10 +1290,18 @@ public static List getAuth2Descriptors() { return Auth2.all(); } + public static List getParametersDescriptors() { + return JobParameters.all(); + } + public static Auth2Descriptor getDefaultAuth2Descriptor() { return NullAuth.DESCRIPTOR; } + public static ParametersDescriptor getDefaultParametersDescriptor() { + return MapParameters.DESCRIPTOR; + } + } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java new file mode 100644 index 00000000..82857d99 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java @@ -0,0 +1,118 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; + +import javax.annotation.Nonnull; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.Objects; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.AbortException; +import hudson.Extension; +import hudson.FilePath; + +public class FileParameters extends JobParameters { + + private static final long serialVersionUID = 3614172320192170597L; + + @Extension + public static final FileParametersDescriptor DESCRIPTOR = new FileParametersDescriptor(); + + private String filePath; + + @DataBoundConstructor + public FileParameters() { + this.filePath = null; + } + + public FileParameters(String filePath) { + this.filePath = filePath; + } + + @DataBoundSetter + public void setFilePath(final String filePath) { + this.filePath = filePath; + } + + public String getFilePath() { + return filePath; + } + + @Override + public String toString() { + return "(" + getClass().getSimpleName() + ") " + filePath; + } + + @Override + public FileParametersDescriptor getDescriptor() { + return DESCRIPTOR; + } + + @Override + public Map getParametersMap(final BuildContext context) throws AbortException { + final String parametersAsString = readParametersFile(context); + return JobParameters.parseStringParameters(parametersAsString); + } + + private String readParametersFile(final BuildContext context) throws AbortException { + if (context.workspace == null) { + throw new AbortException("Workspace is null but parameter file is used. Looks like this step was started with \"agent: none\""); + } + + BufferedReader reader = null; + try { + final FilePath absoluteFilePath = context.workspace.child(getFilePath()); + context.logger.printf("Loading parameters from file %s%n", absoluteFilePath.getRemote()); + + reader = new BufferedReader(new InputStreamReader(absoluteFilePath.read(), UTF_8)); + return reader.lines().collect(joining("\n")); + + } catch (final InterruptedException | IOException e) { + context.logger.printf("[WARNING] Failed loading parameters: %s%n", e.getMessage()); + return ""; + + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (final IOException ex) { + ex.printStackTrace(); + } + } + } + + @Symbol("FileParameters") + public static class FileParametersDescriptor extends ParametersDescriptor { + @Nonnull + @Override + public String getDisplayName() { + return "File parameters"; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final FileParameters that = (FileParameters) o; + return Objects.equals(filePath, that.filePath); + } + + @Override + public int hashCode() { + return Objects.hash(filePath); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java new file mode 100644 index 00000000..93d57ef9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java @@ -0,0 +1,82 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import static java.util.stream.Collectors.toMap; + +import java.io.Serializable; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Predicate; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; + +import hudson.AbortException; +import hudson.DescriptorExtensionList; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import jenkins.model.Jenkins; + +public abstract class JobParameters extends AbstractDescribableImpl implements Serializable, Cloneable { + + private static final DescriptorExtensionList ALL = + DescriptorExtensionList.createDescriptorList(Jenkins.getInstance(), JobParameters.class); + + public static DescriptorExtensionList all() { + return ALL; + } + + public static JobParameters migrateOldParameters(final String parameters, final String parameterFile) { + if (parameterFile != null) { + return new FileParameters(parameterFile); + } + + if (parameters != null) { + return new StringParameters(parameters); + } + + return new MapParameters(); + } + + public static Map parseStringParameters(final String parametersAsString) { + return Arrays.stream(parametersAsString.split("\\n")) + .filter(not(JobParameters::isBlankLine)) + .filter(not(JobParameters::isCommentedLine)) + .filter(JobParameters::containsEqualSign) + .map(JobParameters::splitParameterLine) + .collect(toMap(Entry::getKey, Entry::getValue)); + } + + private static Predicate not(Predicate t) { + return t.negate(); + } + + private static boolean isBlankLine(String line) { + return line.trim().isEmpty(); + } + + private static boolean isCommentedLine(String line) { + return line.trim().startsWith("#"); + } + + private static boolean containsEqualSign(String line) { + return line.contains("="); + } + + private static Entry splitParameterLine(String line) { + final int firstIndexOfEqualSign = line.indexOf("="); + return new AbstractMap.SimpleEntry<>( + line.substring(0, firstIndexOfEqualSign), + line.substring(firstIndexOfEqualSign + 1) + ); + } + + public static abstract class ParametersDescriptor extends Descriptor { } + + public abstract Map getParametersMap(final BuildContext context) throws AbortException; + + @Override + public JobParameters clone() throws CloneNotSupportedException { + return (JobParameters) super.clone(); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java new file mode 100644 index 00000000..da8a5bde --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java @@ -0,0 +1,86 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import javax.annotation.Nonnull; +import java.io.Serializable; +import java.util.Objects; + +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; + +public class MapParameter extends AbstractDescribableImpl implements Cloneable, Serializable { + + @Extension + public static final MapParameterDescriptor DESCRIPTOR = new MapParameterDescriptor(); + + private String name; + private String value; + + @DataBoundConstructor + public MapParameter() { + this("", ""); + } + + public MapParameter(String name, String value) { + this.name = name; + this.value = value; + } + + @DataBoundSetter + public void setName(String name) { + this.name = name; + } + + @DataBoundSetter + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + @Override + public MapParameter clone() throws CloneNotSupportedException { + return (MapParameter) super.clone(); + } + + @Override + public Descriptor getDescriptor() { + return DESCRIPTOR; + } + + @Symbol("MapParameter") + public static class MapParameterDescriptor extends Descriptor { + @Nonnull + @Override + public String getDisplayName() { + return "Map parameter"; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MapParameter that = (MapParameter) o; + return Objects.equals(name, that.name) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java new file mode 100644 index 00000000..ba4a005c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java @@ -0,0 +1,101 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import static java.util.stream.Collectors.toMap; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; + +public class MapParameters extends JobParameters { + + private static final long serialVersionUID = 3614172320192170597L; + + @Extension + public static final MapParametersDescriptor DESCRIPTOR = new MapParametersDescriptor(); + + private final List parameters = new ArrayList<>(); + + @DataBoundConstructor + public MapParameters() {} + + public MapParameters(@NonNull Map parametersMap) { + setParametersMap(parametersMap); + } + + @DataBoundSetter + public void setParameters(final List parameters) { + this.parameters.clear(); + if (parameters != null) { + this.parameters.addAll(parameters); + } + } + + public void setParametersMap(final Map parametersMap) { + this.parameters.clear(); + if (parametersMap != null) { + parametersMap + .entrySet() + .stream() + .map(entry -> new MapParameter(entry.getKey(), entry.getValue())) + .forEach(parameters::add); + } + } + + public List getParameters() { + return parameters; + } + + @Override + public String toString() { + return "(" + getClass().getSimpleName() + ") " + parameters; + } + + @Override + public MapParametersDescriptor getDescriptor() { + return DESCRIPTOR; + } + + @Override + public Map getParametersMap(final BuildContext context) { + return parameters + .stream() + .collect(toMap(MapParameter::getName, MapParameter::getValue)); + } + + @Symbol("MapParameters") + public static class MapParametersDescriptor extends ParametersDescriptor { + @Nonnull + @Override + public String getDisplayName() { + return "Map parameters"; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MapParameters that = (MapParameters) o; + return Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(parameters); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java new file mode 100644 index 00000000..99d64f22 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java @@ -0,0 +1,81 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; + +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; + +public class StringParameters extends JobParameters { + + private static final long serialVersionUID = 3614172320192170597L; + + @Extension + public static final StringParametersDescriptor DESCRIPTOR = new StringParametersDescriptor(); + + private String parameters; + + @DataBoundConstructor + public StringParameters() { + this.parameters = null; + } + + public StringParameters(String parameters) { + this.parameters = parameters; + } + + @DataBoundSetter + public void setParameters(final String parameters) { + this.parameters = parameters; + } + + public String getParameters() { + return parameters; + } + + @Override + public String toString() { + return "(" + getClass().getSimpleName() + ") " + parameters; + } + + @Override + public StringParametersDescriptor getDescriptor() { + return DESCRIPTOR; + } + + @Override + public Map getParametersMap(final BuildContext context) { + return JobParameters.parseStringParameters(parameters); + } + + @Symbol("StringParameters") + public static class StringParametersDescriptor extends ParametersDescriptor { + @Nonnull + @Override + public String getDisplayName() { + return "String parameters"; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StringParameters that = (StringParameters) o; + return Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(parameters); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 645f9b4a..a2d44049 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -22,21 +22,22 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.annotation.Nonnull; - import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; @@ -144,8 +145,8 @@ public void setToken(String token) { } @DataBoundSetter - public void setParameters(String parameters) { - remoteBuildConfig.setParameters(parameters); + public void setParameters2(JobParameters parameters2) { + remoteBuildConfig.setParameters2(parameters2); } @DataBoundSetter @@ -153,16 +154,6 @@ public void setEnhancedLogging(boolean enhancedLogging) { remoteBuildConfig.setEnhancedLogging(enhancedLogging); } - @DataBoundSetter - public void setLoadParamsFromFile(boolean loadParamsFromFile) { - remoteBuildConfig.setLoadParamsFromFile(loadParamsFromFile); - } - - @DataBoundSetter - public void setParameterFile(String parameterFile) { - remoteBuildConfig.setParameterFile(parameterFile); - } - @DataBoundSetter public void setUseJobInfoCache(boolean useJobInfoCache) { remoteBuildConfig.setUseJobInfoCache(useJobInfoCache); @@ -172,7 +163,7 @@ public void setUseJobInfoCache(boolean useJobInfoCache) { public void setUseCrumbCache(boolean useCrumbCache) { remoteBuildConfig.setUseCrumbCache(useCrumbCache); } - + @DataBoundSetter public void setDisabled(boolean disabled) { remoteBuildConfig.setDisabled(disabled); @@ -198,7 +189,7 @@ public String getDisplayName() { @Override public Set> getRequiredContext() { - Set> set = new HashSet>(); + Set> set = new HashSet<>(); Collections.addAll(set, Run.class, TaskListener.class); return set; } @@ -251,9 +242,17 @@ public static List getAuth2Descriptors() { return Auth2.all(); } + public static List getParametersDescriptors() { + return JobParameters.all(); + } + public static Auth2Descriptor getDefaultAuth2Descriptor() { return NullAuth.DESCRIPTOR; } + + public static JobParameters.ParametersDescriptor getDefaultParametersDescriptor() { + return MapParameters.DESCRIPTOR; + } } public static class Execution extends SynchronousNonBlockingStepExecution { @@ -283,7 +282,7 @@ protected Handle run() throws Exception { handle = remoteBuildConfig.performTriggerAndGetQueueId(context); if (remoteBuildConfig.getBlockBuildUntilComplete()) { remoteBuildConfig.performWaitForBuild(context, handle); - } + } } } catch (InterruptedException e) { @@ -342,22 +341,14 @@ public String getToken() { return remoteBuildConfig.getToken(); } - public String getParameters() { - return remoteBuildConfig.getParameters(); + public JobParameters getParameters2() { + return remoteBuildConfig.getParameters2(); } public boolean getEnhancedLogging() { return remoteBuildConfig.getEnhancedLogging(); } - public boolean getLoadParamsFromFile() { - return remoteBuildConfig.getLoadParamsFromFile(); - } - - public String getParameterFile() { - return remoteBuildConfig.getParameterFile(); - } - public int getConnectionRetryLimit() { return remoteBuildConfig.getConnectionRetryLimit(); } @@ -381,10 +372,9 @@ public int getMaxConn() { public Auth2 getAuth() { return remoteBuildConfig.getAuth2(); } - + public boolean isDisabled() { return remoteBuildConfig.isDisabled(); } - } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index a6a43580..b018661d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -1,19 +1,37 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; +import static java.util.Collections.emptyMap; import static org.apache.commons.io.IOUtils.closeQuietly; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.apache.commons.lang.StringUtils.trimToNull; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; +import net.sf.json.util.JSONUtils; + +import javax.annotation.Nonnull; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import javax.net.ssl.*; -import java.net.*; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; @@ -21,7 +39,6 @@ import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; @@ -30,10 +47,8 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; -import javax.annotation.Nonnull; - -import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.JenkinsCrumb; @@ -47,10 +62,6 @@ import hudson.AbortException; import hudson.ProxyConfiguration; -import net.sf.json.JSONException; -import net.sf.json.JSONObject; -import net.sf.json.JSONSerializer; -import net.sf.json.util.JSONUtils; public class HttpHelper { @@ -83,42 +94,15 @@ private static String addToQueryString(String queryString, String item) { * the parameters needed to trigger the remote job. * @return query-parameter-formated URL-encoded string. */ - public static String buildUrlQueryString(Collection parameters) { - - // List to hold the encoded parameters - List encodedParameters = new ArrayList(); - - if (parameters != null) { - for (String parameter : parameters) { - - // Step #1 - break apart the parameter-pairs (because we don't want to encode - // the "=" character) - String[] splitParameters = parameter.split("="); - - // List to hold each individually encoded parameter item - List encodedItems = new ArrayList(); - for (String item : splitParameters) { - try { - // Step #2 - encode each individual parameter item add the encoded item to its - // corresponding list - - encodedItems.add(encodeValue(item)); - - } catch (Exception e) { - // do nothing - // because we are "hard-coding" the encoding type, there is a 0% chance that - // this will fail. - logger.warning(e.toString()); - } - - } - - // Step #3 - reunite the previously separated parameter items and add them to - // the corresponding list - encodedParameters.add(StringUtils.join(encodedItems, "=")); - } - } - return StringUtils.join(encodedParameters, "&"); + public static String buildUrlQueryString(@NonNull final Map parameters) { + return parameters.entrySet() + .stream() + .map(entry -> String.format( + "%s=%s", + encodeValue(entry.getKey()), + encodeValue(entry.getValue()) + )) + .collect(Collectors.joining("&")); } /** @@ -129,12 +113,8 @@ public static String buildUrlQueryString(Collection parameters) { * Boolean indicating if the remote job is parameterized or not * @return A string which represents a portion of the build URL */ - private static String getBuildTypeUrl(boolean isRemoteJobParameterized, Collection params) { - boolean isParameterized = false; - - if (isRemoteJobParameterized || (params != null && params.size() > 0)) { - isParameterized = true; - } + private static String getBuildTypeUrl(boolean isRemoteJobParameterized, @NonNull Map params) { + boolean isParameterized = isRemoteJobParameterized || params.size() > 0; if (isParameterized) { return paramerizedBuildUrl; @@ -358,8 +338,6 @@ private static String getUrlWithoutParameters(String url) { * Name of the remote job * @param securityToken * Security token used to trigger remote job - * @param params - * Parameters for the remote job * @param isRemoteJobParameterized * Is the remote job parameterized * @param context @@ -368,7 +346,7 @@ private static String getUrlWithoutParameters(String url) { * @throws IOException * throw when it can't pass data checking */ - public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, Collection params, + public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, boolean isRemoteJobParameterized, BuildContext context) throws IOException { String triggerUrlString; @@ -383,12 +361,12 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, } triggerUrlString = context.effectiveRemoteServer.getAddress(); triggerUrlString += buildTokenRootUrl; - triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, params); + triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, emptyMap()); query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); // TODO: does it work with full URL? } else { triggerUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); - triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, params); + triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, emptyMap()); } // don't try to include a security token in the URL if none is provided @@ -396,13 +374,6 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, query = addToQueryString(query, "token=" + encodeValue(securityToken)); } - // turn our Collection into a query string - String buildParams = buildUrlQueryString(params); - - if (!buildParams.isEmpty()) { - query = addToQueryString(query, buildParams); - } - // by adding "delay=0", this will (theoretically) force this job to the top of // the remote queue query = addToQueryString(query, "delay=0"); @@ -446,7 +417,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, - Collection postParams, int readTimeout, int numberOfAttempts, int pollInterval, int retryLimit, + Map postParams, int readTimeout, int numberOfAttempts, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { @@ -454,7 +425,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT Map> responseHeader = null; int responseCode = 0; - byte[] postDataBytes = new byte[] {}; + byte[] postDataBytes = new byte[]{}; String parmsString = ""; if (HTTP_POST.equalsIgnoreCase(requestType) && postParams != null && postParams.size() > 0) { parmsString = buildUrlQueryString(postParams); @@ -526,8 +497,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } else { try { responseObject = (JSONObject) JSONSerializer.toJSON(response); - } - catch(JSONException e) { + } catch (JSONException e) { // despite JSONUtils.mayBeJSON believing that this might be JSON, it looks like it wasn't return new ConnectionResponse(responseHeader, response, responseCode); } @@ -584,7 +554,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, - Collection params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, + Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { if (lock == null) { context.logger.println("calling remote without locking..."); @@ -616,12 +586,12 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } } - public static ConnectionResponse tryPost(String urlString, BuildContext context, Collection params, + public static ConnectionResponse tryPost(String urlString, BuildContext context, Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { return tryCall(urlString, HTTP_POST, context, params, readTimeout, pollInterval, retryLimit, - overrideAuth, null, lock,isCrubmCacheEnabled); + overrideAuth, null, lock, isCrubmCacheEnabled); } public static ConnectionResponse tryGet(String urlString, BuildContext context, int readTimeout, @@ -640,7 +610,7 @@ public static String tryGetRawResp(String urlString, BuildContext context, int r return resp.toString(); } - public static ConnectionResponse post(String urlString, BuildContext context, Collection params, + public static ConnectionResponse post(String urlString, BuildContext context, Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { return tryPost(urlString, context, params, readTimeout, pollInterval, retryLimit, overrideAuth, diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java index 224e7f80..5344d16e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java @@ -1,43 +1,39 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.jenkinsci.plugins.tokenmacro.TokenMacro; -public class TokenMacroUtils -{ +public class TokenMacroUtils { - public static String applyTokenMacroReplacements(String input, BasicBuildContext context) throws IOException - { + public static String applyTokenMacroReplacements(String input, BasicBuildContext context) throws IOException { try { if (isUseTokenMacro(context)) { return TokenMacro.expandAll(context.run, context.workspace, context.listener, input); } - } - catch (MacroEvaluationException e) { + } catch (MacroEvaluationException e) { throw new IOException(e); - } - catch (InterruptedException e) { + } catch (InterruptedException e) { throw new IOException(e); } return input; } - public static List applyTokenMacroReplacements(List inputs, BasicBuildContext context) throws IOException - { - List outputs = new ArrayList(); - for (String input : inputs) { - outputs.add(applyTokenMacroReplacements(input, context)); + public static Map applyTokenMacroReplacements(Map input, BasicBuildContext context) + throws IOException { + + Map output = new LinkedHashMap<>(); + for (Map.Entry entry : input.entrySet()) { + output.put(entry.getKey(), applyTokenMacroReplacements(entry.getValue(), context)); } - return outputs; + return output; } - public static boolean isUseTokenMacro(BasicBuildContext context) - { + public static boolean isUseTokenMacro(BasicBuildContext context) { return context != null && context.run != null && context.workspace != null && context.listener != null; } diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly index 7945f8d7..73278550 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/config.jelly @@ -1,8 +1,8 @@ - + - + @@ -10,7 +10,7 @@ - + @@ -22,13 +22,13 @@ - + - + @@ -43,10 +43,8 @@ - - - - + + @@ -55,7 +53,7 @@ - + @@ -65,14 +63,8 @@ - - - - - - - - + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters/config.jelly new file mode 100644 index 00000000..c28d75a4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters/config.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter/config.jelly new file mode 100644 index 00000000..602220f6 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter/config.jelly @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters/config.jelly new file mode 100644 index 00000000..13959434 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters/config.jelly @@ -0,0 +1,14 @@ + + + + + + +

    + +
    + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters/config.jelly new file mode 100644 index 00000000..434d1fcc --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters/config.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 8d3cb8de..85ae521f 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -4,9 +4,12 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.toMap; import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; +import static org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL_UNIX; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; @@ -25,6 +28,8 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.RemoteBuildPipelineStep; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.junit.Assert; @@ -53,117 +58,112 @@ public class RemoteBuildConfigurationTest { - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); - - private User testUser; - private String testUserToken; + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); - private void disableAuth() { - jenkinsRule.jenkins.setAuthorizationStrategy(Unsecured.UNSECURED); - jenkinsRule.jenkins.setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); - jenkinsRule.jenkins.setCrumbIssuer(null); - } + private User testUser; + private String testUserToken; - private void enableAuth() throws IOException { - MockAuthorizationStrategy mockAuth = new MockAuthorizationStrategy(); - jenkinsRule.jenkins.setAuthorizationStrategy(mockAuth); + private void disableAuth() { + jenkinsRule.jenkins.setAuthorizationStrategy(Unsecured.UNSECURED); + jenkinsRule.jenkins.setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); + jenkinsRule.jenkins.setCrumbIssuer(null); + } - HudsonPrivateSecurityRealm hudsonPrivateSecurityRealm = new HudsonPrivateSecurityRealm(false, false, null); - jenkinsRule.jenkins.setSecurityRealm(hudsonPrivateSecurityRealm); //jenkinsRule.createDummySecurityRealm()); - testUser = hudsonPrivateSecurityRealm.createAccount("test", "test"); - testUserToken = testUser.getProperty(jenkins.security.ApiTokenProperty.class).getApiToken(); + private void enableAuth() throws IOException { + MockAuthorizationStrategy mockAuth = new MockAuthorizationStrategy(); + jenkinsRule.jenkins.setAuthorizationStrategy(mockAuth); - mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); - } + HudsonPrivateSecurityRealm hudsonPrivateSecurityRealm = new HudsonPrivateSecurityRealm(false, false, null); + jenkinsRule.jenkins.setSecurityRealm(hudsonPrivateSecurityRealm); //jenkinsRule.createDummySecurityRealm()); + testUser = hudsonPrivateSecurityRealm.createAccount("test", "test"); + testUserToken = testUser.getProperty(jenkins.security.ApiTokenProperty.class).getApiToken(); + mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); + } - @Test - public void testRemoteBuild() throws Exception { - disableAuth(); - _testRemoteBuild(false); - } + @Test + public void testRemoteBuild() throws Exception { + disableAuth(); + _testRemoteBuild(false); + } - @Test - public void testRemoteBuildWithAuthentication() throws Exception { - enableAuth(); - _testRemoteBuild(true); - } + @Test + public void testRemoteBuildWithAuthentication() throws Exception { + enableAuth(); + _testRemoteBuild(true); + } - @Test - public void testRemoteBuildWithCrumb() throws Exception { - disableAuth(); - jenkinsRule.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); - _testRemoteBuild(false); - } + @Test + public void testRemoteBuildWithCrumb() throws Exception { + disableAuth(); + jenkinsRule.jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); + _testRemoteBuild(false); + } private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject) throws Exception { Map parms = new HashMap<>(); parms.put("parameterName1", "value1"); parms.put("parameterName2", "value2"); - this._testRemoteBuild(authenticate, withParam, remoteProject, parms); - } - - private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject, Map parms) throws Exception { - - String remoteUrl = jenkinsRule.getURL().toString(); - RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); - remoteJenkinsServer.setDisplayName("JENKINS"); - remoteJenkinsServer.setAddress(remoteUrl); - RemoteBuildConfiguration.DescriptorImpl descriptor = - jenkinsRule.jenkins.getDescriptorByType(RemoteBuildConfiguration.DescriptorImpl.class); - descriptor.setRemoteSites(remoteJenkinsServer); - - FreeStyleProject project = jenkinsRule.createFreeStyleProject(); - RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); - configuration.setJob(remoteProject.getFullName()); - configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); - configuration.setPreventRemoteBuildQueue(false); - configuration.setBlockBuildUntilComplete(true); - configuration.setPollInterval(1); - configuration.setHttpGetReadTimeout(1000); - configuration.setHttpPostReadTimeout(1000); - configuration.setUseCrumbCache(false); - configuration.setUseJobInfoCache(false); - configuration.setEnhancedLogging(true); - configuration.setTrustAllCertificates(true); - if (withParam){ - String parmString = ""; - for (Map.Entry p : parms.entrySet()) { - parmString += p.getKey() + "=" + p.getValue() + NL_UNIX; - } - configuration.setParameters(parmString); - } - if(authenticate) { - TokenAuth tokenAuth = new TokenAuth(); - tokenAuth.setUserName(testUser.getId()); - tokenAuth.setApiToken(Secret.fromString(testUserToken)); - configuration.setAuth2(tokenAuth); - } - - project.getBuildersList().add(configuration); - - //Trigger build - jenkinsRule.waitUntilNoActivity(); - jenkinsRule.buildAndAssertSuccess(project); - - //Check results - FreeStyleBuild lastBuild2 = project.getLastBuild(); - assertNotNull(lastBuild2); - List log = IOUtils.readLines(lastBuild2.getLogInputStream()); - assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "anonymous") + ", Building in workspace")); - - FreeStyleBuild lastBuild = remoteProject.getLastBuild(); - assertNotNull("lastBuild null", lastBuild); - if (withParam){ - EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); - for (Map.Entry p : parms.entrySet()) { - assertEquals(p.getValue(), remoteEnv.get(p.getKey())); - } - } else { - assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); - } - } + this._testRemoteBuild(authenticate, withParam, remoteProject, parms); + } + + private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyleProject remoteProject, Map params) throws Exception { + + String remoteUrl = jenkinsRule.getURL().toString(); + RemoteJenkinsServer remoteJenkinsServer = new RemoteJenkinsServer(); + remoteJenkinsServer.setDisplayName("JENKINS"); + remoteJenkinsServer.setAddress(remoteUrl); + RemoteBuildConfiguration.DescriptorImpl descriptor = + jenkinsRule.jenkins.getDescriptorByType(RemoteBuildConfiguration.DescriptorImpl.class); + descriptor.setRemoteSites(remoteJenkinsServer); + + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); + configuration.setJob(remoteProject.getFullName()); + configuration.setRemoteJenkinsName(remoteJenkinsServer.getDisplayName()); + configuration.setPreventRemoteBuildQueue(false); + configuration.setBlockBuildUntilComplete(true); + configuration.setPollInterval(1); + configuration.setHttpGetReadTimeout(1000); + configuration.setHttpPostReadTimeout(1000); + configuration.setUseCrumbCache(false); + configuration.setUseJobInfoCache(false); + configuration.setEnhancedLogging(true); + configuration.setTrustAllCertificates(true); + if (withParam) { + configuration.setParameters2(new MapParameters(params)); + } + if (authenticate) { + TokenAuth tokenAuth = new TokenAuth(); + tokenAuth.setUserName(testUser.getId()); + tokenAuth.setApiToken(Secret.fromString(testUserToken)); + configuration.setAuth2(tokenAuth); + } + + project.getBuildersList().add(configuration); + + //Trigger build + jenkinsRule.waitUntilNoActivity(); + jenkinsRule.buildAndAssertSuccess(project); + + //Check results + FreeStyleBuild lastBuild2 = project.getLastBuild(); + assertNotNull(lastBuild2); + List log = IOUtils.readLines(lastBuild2.getLogInputStream()); + assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "anonymous") + ", Building in workspace")); + + FreeStyleBuild lastBuild = remoteProject.getLastBuild(); + assertNotNull("lastBuild null", lastBuild); + if (withParam) { + EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); + for (Map.Entry p : params.entrySet()) { + assertEquals(p.getValue(), remoteEnv.get(p.getKey())); + } + } else { + assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); + } + } private void _testRemoteBuild(boolean authenticate) throws Exception { FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); @@ -173,341 +173,311 @@ private void _testRemoteBuild(boolean authenticate) throws Exception { _testRemoteBuild(authenticate, true, remoteProject); } - @Test @WithoutJenkins - public void testDefaults() throws IOException { - - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("job"); - - assertEquals(false, config.getBlockBuildUntilComplete()); //False in Job - assertEquals(false, config.getEnhancedLogging()); - assertEquals("job", config.getJob()); - assertEquals(false, config.getLoadParamsFromFile()); - assertEquals(false, config.getOverrideAuth()); - assertEquals("", config.getParameterFile()); - assertEquals("", config.getParameters()); - assertEquals(10000, config.getHttpGetReadTimeout()); - assertEquals(30000, config.getHttpPostReadTimeout()); - assertEquals(10, config.getPollInterval(RemoteBuildStatus.RUNNING)); - assertEquals(false, config.getPreventRemoteBuildQueue()); - assertEquals(null, config.getRemoteJenkinsName()); - assertEquals(false, config.getShouldNotFailBuild()); - assertEquals(false, config.getOverrideTrustAllCertificates()); - assertEquals(false, config.getTrustAllCertificates()); - assertEquals("", config.getToken()); - } - - @Test @WithoutJenkins - public void testDefaultsPipelineStep() throws IOException { - - RemoteBuildPipelineStep config = new RemoteBuildPipelineStep("job"); - - assertEquals(true, config.getBlockBuildUntilComplete()); //True in Pipeline Step - assertEquals(false, config.getEnhancedLogging()); - assertEquals("job", config.getJob()); - assertEquals(false, config.getLoadParamsFromFile()); - assertTrue(config.getAuth() instanceof NullAuth); - assertEquals("", config.getParameterFile()); - assertEquals("", config.getParameters()); - assertEquals(10000, config.getHttpGetReadTimeout()); - assertEquals(30000, config.getHttpPostReadTimeout()); - assertEquals(10, config.getPollInterval()); - assertEquals(false, config.getPreventRemoteBuildQueue()); - assertEquals(null, config.getRemoteJenkinsName()); - assertEquals(false, config.getShouldNotFailBuild()); - assertEquals(false, config.getOverrideTrustAllCertificates()); - assertEquals(false, config.getTrustAllCertificates()); - assertEquals("", config.getToken()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withoutServer() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - assertEquals("MyJob", config.getJob()); - try { - config.evaluateEffectiveRemoteHost(null); - fail("findRemoteHost() should throw an AbortException since server not specified"); - } catch(AbortException e) { - assertEquals("Configuration of the remote Jenkins host is missing.", e.getMessage()); - } - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config.setRemoteJenkinsUrl("http://test:8080"); - assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); - - config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("A/B/C/D/MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); - - config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("A/B/C/D/MyJob", config.getJob()); - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withJobUrl() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("http://test:8080/job/folder/job/MyJob"); - assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" - assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { - //URL specified for "job" shall override specified remote host - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("http://testA:8080/job/folder/job/MyJobA"); - config.setRemoteJenkinsUrl("http://testB:8080"); - assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { - //URL specified for "job" shall override global setting - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("http://testA:8080/job/folder/job/MyJobA"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://testB:8080"); - - config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" - assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_withoutJob() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { - try { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("xxx"); - Field field = config.getClass().getDeclaredField("job"); - field.setAccessible(true); - field.set(config, ""); - config.evaluateEffectiveRemoteHost(null); - fail("findRemoteHost() should throw an AbortException since job not specified"); - } catch(AbortException e) { - assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); - } - } - - @Test @WithoutJenkins - public void testRemoteUrlOverridesRemoteName() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); - - config.setRemoteJenkinsName("remoteJenkinsName"); - assertEquals("http://globallyConfigured:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - - //Now override remote host URL - config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); - assertEquals("MyJob", config.getJob()); - assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - /** - * Testing if it is possible to set the TrustAllCertificates-parameter with OverrideTrustAllCertificates set to true - * - * @throws IOException - */ - @Test @WithoutJenkins - public void testRemoteOverridesTrustAllCertificates() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config.setTrustAllCertificates(false); - config.setOverrideTrustAllCertificates(true); - - config = mockGlobalRemoteHost(config, - "remoteJenkinsName", - "http://globallyConfigured:8080", - true); - - config.setRemoteJenkinsName("remoteJenkinsName"); - assertTrue(config.getTrustAllCertificates()); - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_jobNameMissing() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - try { - config.evaluateEffectiveRemoteHost(null); - } - catch (AbortException e) { - assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); - } - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_globalConfigMissing() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); - - config.setRemoteJenkinsName("notConfiguredRemoteHost"); - try { - config.evaluateEffectiveRemoteHost(null); - } - catch (AbortException e) { - assertEquals("Could get remote host with ID 'notConfiguredRemoteHost' configured in Jenkins global configuration. Please check your global configuration.", e.getMessage()); - } - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); - - config.setRemoteJenkinsName("notConfiguredRemoteHost"); - config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); - assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("http://localJobUrl:8080/job/MyJob"); - config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); - - config.setRemoteJenkinsName("notConfiguredRemoteHost"); - assertEquals("http://localJobUrl:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_localOverrideHostURL() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config.setRemoteJenkinsUrl("http://hostname:8080"); - assertEquals("http://hostname:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); - } - - @Test @WithoutJenkins - public void testEvaluateEffectiveRemoteHost_localOverrideHostURLWrong() throws IOException { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); - config.setJob("MyJob"); - config.setRemoteJenkinsUrl("hostname:8080"); - try { - config.evaluateEffectiveRemoteHost(null); - fail("Expected AbortException"); - } - catch (AbortException e) { - assertEquals("The 'Override remote host URL' parameter value (remoteJenkinsUrl: 'hostname:8080') is no valid URL", e.getMessage()); - } - } - - private RemoteBuildConfiguration mockGlobalRemoteHost(RemoteBuildConfiguration config, String remoteName, String remoteUrl) throws MalformedURLException { - RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); - jenkinsServer.setDisplayName(remoteName); - jenkinsServer.setAddress(remoteUrl); - - RemoteBuildConfiguration spy = spy(config); - DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); - descriptor.setRemoteSites(jenkinsServer); - doReturn(descriptor).when(spy).getDescriptor(); - - return spy; - } - - private RemoteBuildConfiguration mockGlobalRemoteHost( - RemoteBuildConfiguration config, String remoteName, String remoteUrl, boolean trustAllCertificates - ) throws MalformedURLException { - RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); - jenkinsServer.setDisplayName(remoteName); - jenkinsServer.setAddress(remoteUrl); - - RemoteBuildConfiguration spy = spy(config); - DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); - descriptor.setRemoteSites(jenkinsServer); - doReturn(descriptor).when(spy).getDescriptor(); - spy.setTrustAllCertificates(trustAllCertificates); - - return spy; - } - - @Test @WithoutJenkins - public void testRemoveTrailingSlashes() { - assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx")); - assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx/")); - assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx//////")); - assertEquals("xxx/yy", RemoteBuildConfiguration.removeTrailingSlashes("xxx/yy//")); - assertEquals("xxx", RemoteBuildConfiguration.removeTrailingSlashes("xxx/ ")); - } - - @Test @WithoutJenkins - public void testRemoveQueryParameters() { - assertEquals("xxx", RemoteBuildConfiguration.removeQueryParameters("xxx")); - assertEquals("http://test:8080/MyJob", RemoteBuildConfiguration.removeQueryParameters("http://test:8080/MyJob?xy=abc")); - assertEquals("xxx", RemoteBuildConfiguration.removeQueryParameters("xxx?zzz")); - } - - @Test @WithoutJenkins - public void testRemoveHashParameters() { - assertEquals("xxx", RemoteBuildConfiguration.removeHashParameters("xxx")); - assertEquals("http://test:8080/MyJob", RemoteBuildConfiguration.removeHashParameters("http://test:8080/MyJob#asdsad")); - assertEquals("xxx", RemoteBuildConfiguration.removeHashParameters("xxx#zzz")); - } - - @Test @WithoutJenkins - public void testGenerateEffectiveRemoteBuildURL() throws Exception { - URL remoteBuildURL = new URL("http://test:8080/job/Abc/3/"); - - assertEquals(new URL("https://foobar:8443/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar:8443")); - assertEquals(new URL("http://foobar:8888/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar:8888")); - assertEquals(new URL("https://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar")); - assertEquals(new URL("http://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar")); - } - - @Test @WithoutJenkins - public void testGenerateJobUrl() throws MalformedURLException, AbortException { - RemoteJenkinsServer remoteServer = new RemoteJenkinsServer(); - remoteServer.setAddress("https://server:8080/jenkins"); - - assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); - assertEquals("https://server:8080/jenkins/job/Folder/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "Folder/JobName")); - assertEquals("https://server:8080/jenkins/job/More/job/than/job/one/job/folder", RemoteBuildConfiguration.generateJobUrl(remoteServer, "More/than/one/folder")); - try { - RemoteBuildConfiguration.generateJobUrl(remoteServer, ""); - Assert.fail("Expected IllegalArgumentException"); - } catch(IllegalArgumentException e) {} - try { - RemoteBuildConfiguration.generateJobUrl(remoteServer, null); - Assert.fail("Expected IllegalArgumentException"); - } catch(IllegalArgumentException e) {} - try { - RemoteBuildConfiguration.generateJobUrl(null, "JobName"); - Assert.fail("Expected NullPointerException"); - } catch(NullPointerException e) {} - - //Test trailing slash - remoteServer.setAddress("https://server:8080/jenkins/"); - assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); - - try { - RemoteJenkinsServer missingUrl = new RemoteJenkinsServer(); - RemoteBuildConfiguration.generateJobUrl(missingUrl, "JobName"); - Assert.fail("Expected AbortException"); - } catch(AbortException e) {} - - } + @Test @WithoutJenkins + public void testDefaults() throws IOException { + + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("job"); + + assertFalse(config.getBlockBuildUntilComplete()); //False in Job + assertFalse(config.getEnhancedLogging()); + assertEquals("job", config.getJob()); + assertFalse(config.getOverrideAuth()); + assertTrue(config.getParameters2() instanceof MapParameters); + assertEquals(10000, config.getHttpGetReadTimeout()); + assertEquals(30000, config.getHttpPostReadTimeout()); + assertEquals(10, config.getPollInterval(RemoteBuildStatus.RUNNING)); + assertFalse(config.getPreventRemoteBuildQueue()); + assertNull(config.getRemoteJenkinsName()); + assertFalse(config.getShouldNotFailBuild()); + assertFalse(config.getOverrideTrustAllCertificates()); + assertFalse(config.getTrustAllCertificates()); + assertEquals("", config.getToken()); + } + + @Test @WithoutJenkins + public void testDefaultsPipelineStep() throws IOException { + + RemoteBuildPipelineStep config = new RemoteBuildPipelineStep("job"); + + assertTrue(config.getBlockBuildUntilComplete()); //True in Pipeline Step + assertFalse(config.getEnhancedLogging()); + assertEquals("job", config.getJob()); + assertTrue(config.getAuth() instanceof NullAuth); + assertTrue(config.getParameters2() instanceof MapParameters); + assertEquals(10000, config.getHttpGetReadTimeout()); + assertEquals(30000, config.getHttpPostReadTimeout()); + assertEquals(10, config.getPollInterval()); + assertFalse(config.getPreventRemoteBuildQueue()); + assertNull(config.getRemoteJenkinsName()); + assertFalse(config.getShouldNotFailBuild()); + assertFalse(config.getOverrideTrustAllCertificates()); + assertFalse(config.getTrustAllCertificates()); + assertEquals("", config.getToken()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withoutServer() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + assertEquals("MyJob", config.getJob()); + try { + config.evaluateEffectiveRemoteHost(null); + fail("findRemoteHost() should throw an AbortException since server not specified"); + } catch (AbortException e) { + assertEquals("Configuration of the remote Jenkins host is missing.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobNameAndRemoteUrl() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("http://test:8080"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobNameAndRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withMultiFolderJobNameAndRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("A/B/C/D/MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://test:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("A/B/C/D/MyJob", config.getJob()); + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrl() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://test:8080/job/folder/job/MyJob"); + assertEquals("http://test:8080/job/folder/job/MyJob", config.getJob()); //The value configured for "job" + assertEquals("http://test:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrlAndRemoteUrl() throws IOException { + //URL specified for "job" shall override specified remote host + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://testA:8080/job/folder/job/MyJobA"); + config.setRemoteJenkinsUrl("http://testB:8080"); + assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testJobUrlHandling_withJobUrlAndRemoteName() throws IOException { + //URL specified for "job" shall override global setting + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://testA:8080/job/folder/job/MyJobA"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://testB:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("http://testA:8080/job/folder/job/MyJobA", config.getJob()); //The value configured for "job" + assertEquals("http://testA:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_withoutJob() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + try { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("xxx"); + Field field = config.getClass().getDeclaredField("job"); + field.setAccessible(true); + field.set(config, ""); + config.evaluateEffectiveRemoteHost(null); + fail("findRemoteHost() should throw an AbortException since job not specified"); + } catch (AbortException e) { + assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testRemoteUrlOverridesRemoteName() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertEquals("http://globallyConfigured:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + + //Now override remote host URL + config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); + assertEquals("MyJob", config.getJob()); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + /** + * Testing if it is possible to set the TrustAllCertificates-parameter with OverrideTrustAllCertificates set to true + * + * @throws IOException + */ + @Test @WithoutJenkins + public void testRemoteOverridesTrustAllCertificates() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setTrustAllCertificates(false); + config.setOverrideTrustAllCertificates(true); + + config = mockGlobalRemoteHost(config, + "remoteJenkinsName", + "http://globallyConfigured:8080", + true); + + config.setRemoteJenkinsName("remoteJenkinsName"); + assertTrue(config.getTrustAllCertificates()); + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_jobNameMissing() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + try { + config.evaluateEffectiveRemoteHost(null); + } catch (final AbortException e) { + assertEquals("Parameter 'Remote Job Name or URL' ('job' variable in Pipeline) not specified.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_globalConfigMissing() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + try { + config.evaluateEffectiveRemoteHost(null); + } catch (AbortException e) { + assertEquals("Could get remote host with ID 'notConfiguredRemoteHost' configured in Jenkins global configuration. Please check your global configuration.", e.getMessage()); + } + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideHostURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + config.setRemoteJenkinsUrl("http://locallyOverridden:8080"); + assertEquals("http://locallyOverridden:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_globalConfigMissing_localOverrideJobURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("http://localJobUrl:8080/job/MyJob"); + config = mockGlobalRemoteHost(config, "remoteJenkinsName", "http://globallyConfigured:8080"); + + config.setRemoteJenkinsName("notConfiguredRemoteHost"); + assertEquals("http://localJobUrl:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_localOverrideHostURL() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("http://hostname:8080"); + assertEquals("http://hostname:8080", config.evaluateEffectiveRemoteHost(null).getAddress()); + } + + @Test @WithoutJenkins + public void testEvaluateEffectiveRemoteHost_localOverrideHostURLWrong() throws IOException { + RemoteBuildConfiguration config = new RemoteBuildConfiguration(); + config.setJob("MyJob"); + config.setRemoteJenkinsUrl("hostname:8080"); + try { + config.evaluateEffectiveRemoteHost(null); + fail("Expected AbortException"); + } catch (AbortException e) { + assertEquals("The 'Override remote host URL' parameter value (remoteJenkinsUrl: 'hostname:8080') is no valid URL", e.getMessage()); + } + } + + private RemoteBuildConfiguration mockGlobalRemoteHost(RemoteBuildConfiguration config, String remoteName, String remoteUrl) throws MalformedURLException { + RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); + jenkinsServer.setDisplayName(remoteName); + jenkinsServer.setAddress(remoteUrl); + + RemoteBuildConfiguration spy = spy(config); + DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); + descriptor.setRemoteSites(jenkinsServer); + doReturn(descriptor).when(spy).getDescriptor(); + + return spy; + } + + private RemoteBuildConfiguration mockGlobalRemoteHost( + RemoteBuildConfiguration config, String remoteName, String remoteUrl, boolean trustAllCertificates + ) throws MalformedURLException { + RemoteJenkinsServer jenkinsServer = new RemoteJenkinsServer(); + jenkinsServer.setDisplayName(remoteName); + jenkinsServer.setAddress(remoteUrl); + + RemoteBuildConfiguration spy = spy(config); + DescriptorImpl descriptor = DescriptorImpl.newInstanceForTests(); + descriptor.setRemoteSites(jenkinsServer); + doReturn(descriptor).when(spy).getDescriptor(); + spy.setTrustAllCertificates(trustAllCertificates); + + return spy; + } + + @Test @WithoutJenkins + public void testGenerateEffectiveRemoteBuildURL() throws Exception { + URL remoteBuildURL = new URL("http://test:8080/job/Abc/3/"); + + assertEquals(new URL("https://foobar:8443/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar:8443")); + assertEquals(new URL("http://foobar:8888/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar:8888")); + assertEquals(new URL("https://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "https://foobar")); + assertEquals(new URL("http://foobar/job/Abc/3/"), RemoteBuildConfiguration.generateEffectiveRemoteBuildURL(remoteBuildURL, "http://foobar")); + } + + @Test @WithoutJenkins + public void testGenerateJobUrl() throws MalformedURLException, AbortException { + RemoteJenkinsServer remoteServer = new RemoteJenkinsServer(); + remoteServer.setAddress("https://server:8080/jenkins"); + + assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); + assertEquals("https://server:8080/jenkins/job/Folder/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "Folder/JobName")); + assertEquals("https://server:8080/jenkins/job/More/job/than/job/one/job/folder", RemoteBuildConfiguration.generateJobUrl(remoteServer, "More/than/one/folder")); + try { + RemoteBuildConfiguration.generateJobUrl(remoteServer, ""); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + RemoteBuildConfiguration.generateJobUrl(remoteServer, null); + Assert.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { } + try { + RemoteBuildConfiguration.generateJobUrl(null, "JobName"); + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { } + + //Test trailing slash + remoteServer.setAddress("https://server:8080/jenkins/"); + assertEquals("https://server:8080/jenkins/job/JobName", RemoteBuildConfiguration.generateJobUrl(remoteServer, "JobName")); + + try { + RemoteJenkinsServer missingUrl = new RemoteJenkinsServer(); + RemoteBuildConfiguration.generateJobUrl(missingUrl, "JobName"); + Assert.fail("Expected AbortException"); + } catch (AbortException e) { } + + } @Test public void testRemoteFolderedBuild() throws Exception { @@ -538,10 +508,10 @@ public void testRemoteBuildWith5KByteString() throws Exception { remoteProject.addProperty( new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), new StringParameterDefinition("parameterName2", "default2"))); - Map parms = new HashMap<>(); - parms.put("parameterName1", TestConst.garbled5KString1); - parms.put("parameterName2", TestConst.garbled5KString2); - _testRemoteBuild(true, true, remoteProject, parms); + Map params = new HashMap<>(); + params.put("parameterName1", TestConst.garbled5KString1); + params.put("parameterName2", TestConst.garbled5KString2); + this._testRemoteBuild(true, true, remoteProject, params); } @Test @@ -557,7 +527,6 @@ public void testRemoteViewBuild() throws Exception { @Test @WithoutJenkins public void testParseStringParameters() { - RemoteBuildConfiguration config = new RemoteBuildConfiguration(); final String parameters = join("\n", asList( "# bla bla", // Comment "", // Empty line @@ -568,23 +537,13 @@ public void testParseStringParameters() { "line2", // Multi-line parameter "line3" // Multi-line parameter )); - config.setParameters(parameters); + final Map actualParametersMap = JobParameters.parseStringParameters(parameters); final Map expectedParametersMap = new HashMap<>(); expectedParametersMap.put("PARAM1", "toto"); expectedParametersMap.put("PARAM2", "dG90bwo="); // This one should fail because it's broken (see issue 58818) expectedParametersMap.put("PARAM3", "line1"); // We keep the wrong value in the test because it's unfixable with string parameters - final List actualParameterList = config.getCleanedParameters( - config.getParameterList(null) - ); - final Map actualParametersMap = actualParameterList.stream() - .map(line -> { - final String[] splitLine = line.split("="); - return new AbstractMap.SimpleEntry<>(splitLine[0], splitLine.length > 1 ? splitLine[1] : ""); - }) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - assertEquals(expectedParametersMap, actualParametersMap); } From f428209e441ad424a43b8e99ca1fe5a714f1d0b8 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 12 Jun 2022 00:54:57 +0800 Subject: [PATCH 215/262] update the change log --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0217909..cf22bb0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.1.6 (Jun 12th, 2022) + +### Improvement + +* Improve parameters handling. ([f1eb49cf](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7)) +* Allow use `triggerRemoteJob` step with `agent none` ([fef4451](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/fef4451ad0c743d47f3282e75dfb4c9d1ffbb3c3)) + # 3.1.5.1 (Oct 6th, 2020) ### Improvement From efc6d7924d30de405f77f616479e083e90d2dda4 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Sun, 12 Jun 2022 01:07:31 +0800 Subject: [PATCH 216/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2752bf5e..b7ef4a59 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6-SNAPSHOT + 3.1.7-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. From 27f1ee546de867c82f626cc9cb722fd7008eb717 Mon Sep 17 00:00:00 2001 From: quilicicf Date: Mon, 13 Jun 2022 09:17:06 +0200 Subject: [PATCH 217/262] Fix migration to parameters2 The migration was checking for `null` when actually parameters end up being empty when unset. This means that only the `parameterFile` case was working since it's checked first with a fast return. --- .../parameters2/JobParameters.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java index 93d57ef9..92d6f1e4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java @@ -1,5 +1,6 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; +import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.stream.Collectors.toMap; import java.io.Serializable; @@ -27,11 +28,11 @@ public static DescriptorExtensionList all() } public static JobParameters migrateOldParameters(final String parameters, final String parameterFile) { - if (parameterFile != null) { + if (!isNullOrEmpty(parameterFile)) { return new FileParameters(parameterFile); } - if (parameters != null) { + if (!isNullOrEmpty(parameters)) { return new StringParameters(parameters); } From 5033a881308f18d416c340c969b36ec2902bb6d8 Mon Sep 17 00:00:00 2001 From: quilicicf Date: Mon, 13 Jun 2022 11:17:09 +0200 Subject: [PATCH 218/262] Fix pipeline step execution and add documentation --- .../parameters2/FileParameters.java | 2 +- .../parameters2/MapParameters.java | 6 ++- .../parameters2/StringParameters.java | 2 +- .../pipeline/RemoteBuildPipelineStep.java | 46 +++++++++++++++++-- .../help-parameters.html | 10 ---- .../help-parameters2.html | 19 ++++++++ .../RemoteBuildPipelineStep/config.jelly | 17 ++----- .../help-parameters.html | 21 +++++++-- .../RemoteBuildConfigurationTest.java | 2 +- 9 files changed, 90 insertions(+), 35 deletions(-) delete mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters.html create mode 100644 src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters2.html diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java index 82857d99..30c3c099 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java @@ -23,7 +23,7 @@ public class FileParameters extends JobParameters { private static final long serialVersionUID = 3614172320192170597L; - @Extension + @Extension(ordinal = 0) public static final FileParametersDescriptor DESCRIPTOR = new FileParametersDescriptor(); private String filePath; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java index ba4a005c..fb79e6db 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java @@ -8,8 +8,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; +import org.jboss.marshalling.util.IntKeyMap; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; @@ -21,13 +23,13 @@ public class MapParameters extends JobParameters { private static final long serialVersionUID = 3614172320192170597L; - @Extension + @Extension(ordinal = 2) public static final MapParametersDescriptor DESCRIPTOR = new MapParametersDescriptor(); private final List parameters = new ArrayList<>(); @DataBoundConstructor - public MapParameters() {} + public MapParameters() { } public MapParameters(@NonNull Map parametersMap) { setParametersMap(parametersMap); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java index 99d64f22..3d4954b8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java @@ -15,7 +15,7 @@ public class StringParameters extends JobParameters { private static final long serialVersionUID = 3614172320192170597L; - @Extension + @Extension(ordinal = 1) public static final StringParametersDescriptor DESCRIPTOR = new StringParametersDescriptor(); private String parameters; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index a2d44049..2fe0e774 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -22,10 +22,14 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline; +import static java.util.stream.Collectors.toMap; + import javax.annotation.Nonnull; +import java.nio.file.Path; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; @@ -35,8 +39,10 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.FileParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.StringParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; @@ -52,6 +58,7 @@ import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import hudson.AbortException; import hudson.Extension; import hudson.ExtensionList; import hudson.FilePath; @@ -70,7 +77,7 @@ public RemoteBuildPipelineStep(String job) { remoteBuildConfig = new RemoteBuildConfiguration(); remoteBuildConfig.setJob(job); remoteBuildConfig.setShouldNotFailBuild(false); // We need to get notified. Failure feedback is collected async - // then. + // then. remoteBuildConfig.setBlockBuildUntilComplete(true); // default for Pipeline Step } @@ -145,8 +152,39 @@ public void setToken(String token) { } @DataBoundSetter - public void setParameters2(JobParameters parameters2) { - remoteBuildConfig.setParameters2(parameters2); + public void setParameters(Object parameters) throws AbortException { + if (parameters instanceof JobParameters) { + remoteBuildConfig.setParameters2((JobParameters) parameters); + } else if (parameters instanceof String) { + final String parametersAsString = (String) parameters; + if (parametersAsString.contains("=") || parametersAsString.contains("\n")) { + remoteBuildConfig.setParameters2(new StringParameters(parametersAsString)); + } else { + remoteBuildConfig.setParameters2(new FileParameters(parametersAsString)); + } + } else if (parameters instanceof Map) { + @SuppressWarnings("unchecked") final Map parametersAsMap = + ((Map) parameters).entrySet() + .stream() + .collect(toMap( + (entry) -> entry.getKey().toString(), + (entry) -> entry.getValue().toString() + )); + remoteBuildConfig.setParameters2(new MapParameters(parametersAsMap)); + } else { + throw new AbortException("Cannot read remote job parameters."); + } + + } + + /** + * @deprecated Still there to allow old configuration (3.1.5 and below) to work. + * Use {@link RemoteBuildPipelineStep#setParameters(Object)} instead now. + */ + @Deprecated + @DataBoundSetter + public void setParameterFile(String parameterFile) { + remoteBuildConfig.setParameters2(new FileParameters(parameterFile)); } @DataBoundSetter @@ -341,7 +379,7 @@ public String getToken() { return remoteBuildConfig.getToken(); } - public JobParameters getParameters2() { + public JobParameters getParameters() { return remoteBuildConfig.getParameters2(); } diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters.html deleted file mode 100644 index 0a7af8b5..00000000 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters.html +++ /dev/null @@ -1,10 +0,0 @@ -
    -
    - Job Parameters -
    - Parameters which will be used when triggering the remote job. -
    - If no parameters are needed, then just leave this blank. -
    - Any line start with a pound-sign (#) will be treated as a comment. -
    \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters2.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters2.html new file mode 100644 index 00000000..81e758f8 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-parameters2.html @@ -0,0 +1,19 @@ +
    +
    + Job Parameters +
    + Parameters which will be used when triggering the remote job. +
    + If no parameters are needed, then just leave this blank. +
      +
    • Map parameters
      + This is the recommended type. It allows you to provide parameters with key/value and supports multi-line parameters. +
    • +
    • String parameters (legacy)
      + This type allows to describe parameters within a big string like in versions 3.1.5 and lower. It does not support multi-line parameters. +
    • +
    • File parameters (legacy)
      + This type allows to describe parameters within a file like in versions 3.1.5 and lower. It does not support multi-line parameters. +
    • +
    +
    diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly index 4eb695a0..2ab24529 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -19,11 +19,11 @@ - + - + @@ -52,10 +52,8 @@ - - - - + + @@ -78,12 +76,5 @@
    - - - - - -
    - diff --git a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html index 365d597c..8295f445 100644 --- a/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html @@ -3,8 +3,23 @@ Job Parameters
    Parameters which will be used when triggering the remote job. -
    +
    If no parameters are needed, then just leave this blank. -
    - Any line start with a pound-sign (#) will be treated as a comment. +
    + In case of a String, the plugin uses String/File parameters depending on the parameter's content. + +
      +
    • Map parameters
      +

      Map<String, Object>

      +

      This is the recommended type. It allows you to provide parameters with key/value and supports multi-line parameters.

      +
    • +
    • String parameters (legacy)
      +

      String

      +

      This type allows to describe parameters within a big string like in versions 3.1.5 and lower. It does not support multi-line parameters.

      +
    • +
    • File parameters (legacy)
      +

      String

      + This type allows to describe parameters within a file like in versions 3.1.5 and lower. It does not support multi-line parameters. +
    • +
    diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 85ae521f..81e069b9 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -204,7 +204,7 @@ public void testDefaultsPipelineStep() throws IOException { assertFalse(config.getEnhancedLogging()); assertEquals("job", config.getJob()); assertTrue(config.getAuth() instanceof NullAuth); - assertTrue(config.getParameters2() instanceof MapParameters); + assertTrue(config.getParameters() instanceof MapParameters); assertEquals(10000, config.getHttpGetReadTimeout()); assertEquals(30000, config.getHttpPostReadTimeout()); assertEquals(10, config.getPollInterval()); From 0637b21efc1b86c156e4a0f660fa833c42177d15 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 14 Jun 2022 00:11:30 +0800 Subject: [PATCH 219/262] fix hpi:run error (https://javadoc.jenkins-ci.org/jenkins/model/Jenkins.html#getInstanceOrNull()) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b7ef4a59..24d33b55 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ - 1.642.3 + 1.653 8 3.1.4-SNAPSHOT From b48fe2bc4db4a8cb7d6abb647ca69ba626cac56c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 14 Jun 2022 00:14:27 +0800 Subject: [PATCH 220/262] update change log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf22bb0d..2c71dc25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.6.1 (Jun 14th, 2022) + +### Improvement + +* Complete the PR [f1eb49cf] in 3.1.6. (https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7). ([5033a88](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/5033a881308f18d416c340c969b36ec2902bb6d8)) + # 3.1.6 (Jun 12th, 2022) ### Improvement From 63888112faf7e6b06c8d9ceb2d425128e3b5f1ed Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 14 Jun 2022 00:18:05 +0800 Subject: [PATCH 221/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.6.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 24d33b55..d895c871 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.7-SNAPSHOT + 3.1.6.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.6.1 From 622b2ffe73a05f205f7cb95f8ae3138a19ca56f3 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 14 Jun 2022 00:18:11 +0800 Subject: [PATCH 222/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d895c871..24d33b55 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6.1 + 3.1.7-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.6.1 + HEAD From 45bce33f1adf7e23b5794b22de6c39f9be17b7d6 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 14 Jun 2022 17:03:30 +0800 Subject: [PATCH 223/262] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c71dc25..9df5faf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Improvement -* Complete the PR [f1eb49cf] in 3.1.6. (https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7). ([5033a88](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/5033a881308f18d416c340c969b36ec2902bb6d8)) +* Complete the PR [f1eb49cf](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7) in 3.1.6. . ([5033a88](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/5033a881308f18d416c340c969b36ec2902bb6d8)) # 3.1.6 (Jun 12th, 2022) From 24bbd0051003d2689cf0e912a4ab1b993bd19380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 01:40:52 +0000 Subject: [PATCH 224/262] Bump script-security from 1.34 to 1.75 Bumps [script-security](https://github.com/jenkinsci/script-security-plugin) from 1.34 to 1.75. - [Release notes](https://github.com/jenkinsci/script-security-plugin/releases) - [Changelog](https://github.com/jenkinsci/script-security-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/script-security-plugin/compare/script-security-1.34...script-security-1.75) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins:script-security dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 24d33b55..8b0523bd 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ org.jenkins-ci.plugins script-security - 1.34 + 1.75 true From e33104c2524fed7fa4914031d2482a3f7c927f0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 02:31:06 +0000 Subject: [PATCH 225/262] Bump credentials from 2.1.16 to 2.6.1.1 Bumps [credentials](https://github.com/jenkinsci/credentials-plugin) from 2.1.16 to 2.6.1.1. - [Release notes](https://github.com/jenkinsci/credentials-plugin/releases) - [Changelog](https://github.com/jenkinsci/credentials-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/credentials-plugin/compare/credentials-2.1.16...credentials-2.6.1.1) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins:credentials dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8b0523bd..5bb146b7 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ org.jenkins-ci.plugins credentials - 2.1.16 + 2.6.1.1 org.jenkins-ci.plugins From 999a5e57f0ca131cb65c902561348cac2d05cfa4 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 13 Jul 2022 09:27:50 +0800 Subject: [PATCH 226/262] Update pom.xml breaking changs for the parameters since 3.1.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5bb146b7..e85f3be4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ 1.653 8 - 3.1.4-SNAPSHOT + 3.1.6 Parameterized-Remote-Trigger From 404ae51fb61d8d2d6971217a06db86016b43b50e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Wed, 13 Jul 2022 09:30:42 +0800 Subject: [PATCH 227/262] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df5faf4..d345a1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Complete the PR [f1eb49cf](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7) in 3.1.6. . ([5033a88](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/5033a881308f18d416c340c969b36ec2902bb6d8)) +#### Breaking changes for the parameters (multiple lines, read from files) + # 3.1.6 (Jun 12th, 2022) ### Improvement @@ -11,6 +13,8 @@ * Improve parameters handling. ([f1eb49cf](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/f1eb49cf6294b77dae9361a4176d60d8591f40b7)) * Allow use `triggerRemoteJob` step with `agent none` ([fef4451](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/commit/fef4451ad0c743d47f3282e75dfb4c9d1ffbb3c3)) +#### Breaking changes for the parameters (multiple lines, read from files) + # 3.1.5.1 (Oct 6th, 2020) ### Improvement From adda1384b514237b59eda7377815ce04ee9685d4 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 18 Jul 2022 23:56:04 +0800 Subject: [PATCH 228/262] update CHANGELOG.md for breaking changes made since 3.1.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d345a1b9..7d32b76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.6.2 (July 18th, 2022) + +### Improvement + +* Mark breaking changes for 3.1.6.x + # 3.1.6.1 (Jun 14th, 2022) ### Improvement From d527a624011682f381a2d1783667c67ac48bc77d Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 19 Jul 2022 00:03:56 +0800 Subject: [PATCH 229/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.6.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e85f3be4..eacf0f95 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.7-SNAPSHOT + 3.1.6.2 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.6.2 From 923c752582662a32e1b24a017e1084205509d815 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 19 Jul 2022 00:04:02 +0800 Subject: [PATCH 230/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index eacf0f95..80c829e8 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6.2 + 3.1.6.3-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.6.2 + HEAD From 7a7efbd216f3d0b5dc361077d504b88248addebb Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 22 Jul 2022 03:43:46 +0800 Subject: [PATCH 231/262] fix pipeline generation & unable to parse pipeline correctly --- .../pipeline/RemoteBuildPipelineStep.java | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 2fe0e774..40d2d229 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -24,14 +24,16 @@ import static java.util.stream.Collectors.toMap; -import javax.annotation.Nonnull; -import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import javax.annotation.Nonnull; + import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; @@ -41,12 +43,14 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.FileParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.JobParameters; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameter; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.MapParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2.StringParameters; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; @@ -57,6 +61,8 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import hudson.AbortException; import hudson.Extension; @@ -72,6 +78,8 @@ public class RemoteBuildPipelineStep extends Step { private RemoteBuildConfiguration remoteBuildConfig; + private static final Logger logger = LoggerFactory.getLogger(RemoteBuildPipelineStep.class); + @DataBoundConstructor public RemoteBuildPipelineStep(String job) { remoteBuildConfig = new RemoteBuildConfiguration(); @@ -154,32 +162,87 @@ public void setToken(String token) { @DataBoundSetter public void setParameters(Object parameters) throws AbortException { if (parameters instanceof JobParameters) { + logger.trace("job parameter detected"); remoteBuildConfig.setParameters2((JobParameters) parameters); } else if (parameters instanceof String) { final String parametersAsString = (String) parameters; if (parametersAsString.contains("=") || parametersAsString.contains("\n")) { + logger.trace("string var"); remoteBuildConfig.setParameters2(new StringParameters(parametersAsString)); } else { + logger.trace("string file"); remoteBuildConfig.setParameters2(new FileParameters(parametersAsString)); } } else if (parameters instanceof Map) { - @SuppressWarnings("unchecked") final Map parametersAsMap = - ((Map) parameters).entrySet() - .stream() - .collect(toMap( - (entry) -> entry.getKey().toString(), - (entry) -> entry.getValue().toString() - )); + logger.trace("map"); + + @SuppressWarnings("unchecked") + final Map parametersAsMap = ((Map) parameters).entrySet().stream() + .collect(toMap((entry) -> entry.getKey().toString(), (entry) -> entry.getValue().toString())); + logger.trace("parsed map"); remoteBuildConfig.setParameters2(new MapParameters(parametersAsMap)); } else { - throw new AbortException("Cannot read remote job parameters."); - } + logger.trace(parameters.getClass().toString() + ", force casting to UninstantiatedDescribable"); + + if (!(parameters instanceof UninstantiatedDescribable)) { + throw new AbortException("Cannot parse pipeline parameters."); + } + + UninstantiatedDescribable uninstantiatedParms = (UninstantiatedDescribable) parameters; + + Map args = uninstantiatedParms.getArguments(); + if (args.entrySet().size() < 1) { + throw new AbortException("Cannot parse pipeline parameters, no parameters detected."); + } + + String keys = ""; + for (Entry entry : args.entrySet()) { + keys += entry.getKey() + ","; + + if (entry.getValue() instanceof List) { + if (entry.getKey().equalsIgnoreCase("parameters")) { + MapParameters mps = new MapParameters(); + List lmp = new ArrayList(); + + for (Object obj : ((List) entry.getValue())) { + UninstantiatedDescribable ao = (UninstantiatedDescribable) obj; + MapParameter mpTmp = new MapParameter(); + + for (Object subParms : ao.getArguments().entrySet()) { + + Entry castedParm = (Entry) subParms; + + if (castedParm.getKey() == "name") { + mpTmp.setName(castedParm.getValue()); + } else if (castedParm.getKey() == "value") { + mpTmp.setValue(castedParm.getValue()); + } else { + throw new AbortException( + "Cannot parse pipeline parameters, unknown sub key: " + castedParm.getKey()); + } + } + lmp.add(mpTmp); + } + mps.setParameters(lmp); + logger.trace("map data: " + mps.toString()); + remoteBuildConfig.setParameters2((JobParameters) mps); + return; + } + } else if (entry.getKey().toLowerCase().equalsIgnoreCase("filepath")) { + logger.trace(entry.getValue().toString()); + remoteBuildConfig.setParameters2((JobParameters) new FileParameters(entry.getValue().toString())); + return; + } + } + throw new AbortException("Cannot parse pipeline parameters, unknown key: " + keys); + } } /** * @deprecated Still there to allow old configuration (3.1.5 and below) to work. - * Use {@link RemoteBuildPipelineStep#setParameters(Object)} instead now. + * Use {@link RemoteBuildPipelineStep#setParameters(Object)} instead + * now. */ @Deprecated @DataBoundSetter @@ -415,4 +478,15 @@ public boolean isDisabled() { return remoteBuildConfig.isDisabled(); } + /** + * @deprecated Still there to allow old configuration (3.1.5 and below) to work. + * Use {@link RemoteBuildPipelineStep#getParameters(Object)} instead + * now. Without this getter, pipeline script generator throws a + * exception, but we need to keep the backward compatibility of + * `parameterFile`, so we add this getter back with null return + * value. + */ + public String getParameterFile() { + return null; + } } From aaa133ee2c4f0985a58c0956054d7e9c59e4280c Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 22 Jul 2022 03:47:30 +0800 Subject: [PATCH 232/262] update readme --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d32b76b..3e927dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.1.6.3 (July 22th, 2022) + +### Bug fixes + +* Fix pipeline generation error +* Fix pipeline unable to parse + # 3.1.6.2 (July 18th, 2022) ### Improvement From 72ed59ee8e6a6bd6323087336f864ad74495e22e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 22 Jul 2022 03:59:31 +0800 Subject: [PATCH 233/262] fix java doc warning --- .../pipeline/RemoteBuildPipelineStep.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 40d2d229..df2e2c42 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -243,6 +243,8 @@ public void setParameters(Object parameters) throws AbortException { * @deprecated Still there to allow old configuration (3.1.5 and below) to work. * Use {@link RemoteBuildPipelineStep#setParameters(Object)} instead * now. + * @param parameterFile + * The parameter file. */ @Deprecated @DataBoundSetter @@ -480,11 +482,12 @@ public boolean isDisabled() { /** * @deprecated Still there to allow old configuration (3.1.5 and below) to work. - * Use {@link RemoteBuildPipelineStep#getParameters(Object)} instead + * Use {@link RemoteBuildPipelineStep#getParameters()} instead * now. Without this getter, pipeline script generator throws a * exception, but we need to keep the backward compatibility of * `parameterFile`, so we add this getter back with null return * value. + * @return Path of the parameter file. */ public String getParameterFile() { return null; From e7b53ca8d995a0737c163b15dfd2099f88797277 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 22 Jul 2022 04:14:52 +0800 Subject: [PATCH 234/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.1.6.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 80c829e8..d5952f90 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6.3-SNAPSHOT + 3.1.6.3 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.1.6.3 From 33b77e1d4afee2e49257c0211fe3fb24166d27e4 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Fri, 22 Jul 2022 04:14:58 +0800 Subject: [PATCH 235/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d5952f90..91f964cc 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6.3 + 3.1.6.4-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -52,7 +52,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.1.6.3 + HEAD From c4cce98ec57058b2daebae7ca02ead68d2248a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n?= Date: Mon, 28 Nov 2022 19:34:17 +0100 Subject: [PATCH 236/262] fetch remaining log after remote job finishes --- .../ParameterizedRemoteTrigger/RemoteBuildConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 72e7720c..b8126cde 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -710,6 +710,7 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx handle.setBuildInfo(buildInfo); } if (this.getEnhancedLogging()) { + consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); context.logger .println("--------------------------------------------------------------------------------"); } From ad1fd1e2b3eb6467fa87adef09eefb1fd2d95cb8 Mon Sep 17 00:00:00 2001 From: gongy Date: Fri, 11 Aug 2023 10:43:47 +0800 Subject: [PATCH 237/262] Modernize the plugin baseline 1. Upgrade to Jenkins 2.346.3 baseline. 2. Fix the compilation issue which the jboss package is not used. 3. Fix the tests that failed in new Jenkins version. 4. Fix SpotBugs issues. 5. Fix stream not closed issue in tests --- pom.xml | 25 +++++++++++------ .../RemoteBuildConfiguration.java | 12 ++++---- .../parameters2/MapParameters.java | 2 -- .../pipeline/Handle.java | 7 ++++- .../RemoteBuildConfigurationTest.java | 28 +++++++++++-------- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 91f964cc..aad5997c 100644 --- a/pom.xml +++ b/pom.xml @@ -3,12 +3,14 @@ org.jenkins-ci.plugins plugin - 3.50 + 4.50 + - 1.653 - 8 + 2.346.3 + 1.8 + 1.8 3.1.6 @@ -42,7 +44,7 @@ maven-hpi-plugin 3.0.4-SNAPSHOT - + --> @@ -69,27 +71,34 @@ + + + + io.jenkins.tools.bom + bom-2.346.x + 1742.vb_70478c1b_25f + import + pom + + + org.jenkins-ci.plugins credentials - 2.6.1.1 org.jenkins-ci.plugins token-macro - 2.3 org.jenkins-ci.plugins script-security - 1.75 true org.jenkins-ci.plugins.workflow workflow-step-api - 2.13 true diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index b8126cde..8b90ad1d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -23,9 +23,11 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Semaphore; import java.util.logging.Level; import java.util.logging.Logger; @@ -845,7 +847,7 @@ protected static URL generateEffectiveRemoteBuildURL(URL remoteBuildURL, String private String printOffsetConsoleOutput(BuildContext context, String offset, RemoteBuildInfo buildInfo) throws IOException, InterruptedException { - if (offset.equals("-1")) { + if (offset == null || offset.equals("-1")) { return "-1"; } String buildUrlString = String.format("%slogText/progressiveText?start=%s", buildInfo.getBuildURL(), offset); @@ -1271,10 +1273,10 @@ public ListBoxModel doFillRemoteJenkinsNameItems() { ListBoxModel model = new ListBoxModel(); model.add(""); - for (RemoteJenkinsServer site : getRemoteSites()) { - model.add(site.getDisplayName()); - } - + Arrays.stream(getRemoteSites()) + .filter(Objects::nonNull) + .map(RemoteJenkinsServer::getDisplayName) + .forEach(model::add); return model; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java index fb79e6db..1e46731b 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java @@ -8,10 +8,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; -import org.jboss.marshalling.util.IntKeyMap; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.kohsuke.stapler.DataBoundConstructor; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index be0a68f8..bdb91b1c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -8,12 +8,14 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNullableByDefault; +import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; @@ -366,7 +368,10 @@ public Object readJsonFileFromBuildArchive(String filename) throws IOException, private String getParameterFromJobMetadata(JSONObject remoteJobMetadata, String string) { try { - return trimToNull(remoteJobMetadata.getString("name")); + return Optional.ofNullable(remoteJobMetadata) + .map(meta->meta.getString("name")) + .map(StringUtils::trimToNull) + .orElse(null); } catch (JSONException e) { return null; diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index 81e069b9..60536286 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.spy; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Map; +import jenkins.security.ApiTokenProperty; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration.DescriptorImpl; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; @@ -77,7 +79,7 @@ private void enableAuth() throws IOException { HudsonPrivateSecurityRealm hudsonPrivateSecurityRealm = new HudsonPrivateSecurityRealm(false, false, null); jenkinsRule.jenkins.setSecurityRealm(hudsonPrivateSecurityRealm); //jenkinsRule.createDummySecurityRealm()); testUser = hudsonPrivateSecurityRealm.createAccount("test", "test"); - testUserToken = testUser.getProperty(jenkins.security.ApiTokenProperty.class).getApiToken(); + testUserToken = testUser.getProperty(ApiTokenProperty.class).generateNewToken("test").plainValue; mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); } @@ -150,19 +152,23 @@ private void _testRemoteBuild(boolean authenticate, boolean withParam, FreeStyle //Check results FreeStyleBuild lastBuild2 = project.getLastBuild(); assertNotNull(lastBuild2); - List log = IOUtils.readLines(lastBuild2.getLogInputStream()); - assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "anonymous") + ", Building in workspace")); - FreeStyleBuild lastBuild = remoteProject.getLastBuild(); - assertNotNull("lastBuild null", lastBuild); - if (withParam) { - EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); - for (Map.Entry p : params.entrySet()) { - assertEquals(p.getValue(), remoteEnv.get(p.getKey())); + try(InputStream logStream=lastBuild2.getLogInputStream()){ + List log = IOUtils.readLines(logStream); + assertTrue(log.toString(), log.toString().contains("Started by user " + (authenticate ? "test" : "unknown or anonymous") + ", Running as SYSTEM, Building in workspace")); + + FreeStyleBuild lastBuild = remoteProject.getLastBuild(); + assertNotNull("lastBuild null", lastBuild); + if (withParam) { + EnvVars remoteEnv = lastBuild.getEnvironment(new LogTaskListener(null, null)); + for (Map.Entry p : params.entrySet()) { + assertEquals(p.getValue(), remoteEnv.get(p.getKey())); + } + } else { + assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); } - } else { - assertNotEquals("lastBuild should be executed no matter the result which depends on the remote job configuration.", null, lastBuild.getNumber()); } + } private void _testRemoteBuild(boolean authenticate) throws Exception { From a940cae1b3bcf3fa630577a9d4327d2776a98cb3 Mon Sep 17 00:00:00 2001 From: gongy Date: Tue, 15 Aug 2023 11:58:59 +0800 Subject: [PATCH 238/262] Implement W3C traceparent header if opentelemeter plugin is available 1. Add OtelUtils to check if opentelemeter is available. 2. Generate W3C traceparent header if there's a valid span. 3. Add test which uses mocker server to verify the traceparent header --- pom.xml | 12 ++ .../RemoteBuildConfiguration.java | 5 +- .../pipeline/RemoteBuildPipelineStep.java | 3 +- .../utils/HttpHelper.java | 7 + .../utils/OtelUtils.java | 75 +++++++ .../OpenTelemeterTest.java | 191 ++++++++++++++++++ 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java create mode 100644 src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java diff --git a/pom.xml b/pom.xml index aad5997c..360efbfa 100644 --- a/pom.xml +++ b/pom.xml @@ -101,12 +101,24 @@ workflow-step-api true + + io.jenkins.plugins + opentelemetry + true + 2.10.0 + org.mockito mockito-core 2.18.3 test + + org.mock-server + mockserver-junit-rule + 5.14.0 + test + diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 8b90ad1d..eb882018 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -50,6 +50,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.HttpHelper; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.OtelUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.RestUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.TokenMacroUtils; import org.kohsuke.accmod.Restricted; @@ -567,7 +568,7 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task Handle handle = null; BuildContext context = null; RemoteJenkinsServer effectiveRemoteServer = null; - try { + try (AutoCloseable ignored = OtelUtils.isOpenTelemetryAvailable() ? OtelUtils.activeSpanIfAvailable(build) : OtelUtils.noop()) { effectiveRemoteServer = evaluateEffectiveRemoteHost(new BasicBuildContext(build, workspace, listener)); context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); handle = performTriggerAndGetQueueId(context); @@ -575,6 +576,8 @@ public void perform(Run build, FilePath workspace, Launcher launcher, Task } catch (InterruptedException e) { this.abortRemoteTask(effectiveRemoteServer, handle, context); throw e; + } catch (Exception e) { + throw new RuntimeException(e); } } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index df2e2c42..014e7c89 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -50,6 +50,7 @@ import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.AffectedField; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.FormValidationUtils.RemoteURLCombinationsResult; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.OtelUtils; import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable; import org.jenkinsci.plugins.workflow.steps.Step; import org.jenkinsci.plugins.workflow.steps.StepContext; @@ -380,7 +381,7 @@ protected Handle run() throws Exception { BuildContext context = new BuildContext(build, workspace, listener, listener.getLogger(), effectiveRemoteServer); Handle handle = null; - try { + try (AutoCloseable ignored = OtelUtils.isOpenTelemetryAvailable() ? OtelUtils.activeSpanIfAvailable(stepContext) : OtelUtils.noop()) { if (!remoteBuildConfig.isStepDisabled(listener.getLogger())) { handle = remoteBuildConfig.performTriggerAndGetQueueId(context); if (remoteBuildConfig.getBlockBuildUntilComplete()) { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index b018661d..2493a841 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -49,6 +49,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.JenkinsCrumb; @@ -439,6 +440,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT conn.setDoInput(true); conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept-Language", "UTF-8"); + if (requestType.equals(HTTP_POST) && OtelUtils.isOpenTelemetryAvailable() ) { + String traceParentHeader = OtelUtils.getTraceParent(); + if (StringUtils.isNotBlank(OtelUtils.getTraceParent())){ + conn.setRequestProperty("traceparent", traceParentHeader); + } + } conn.setRequestMethod(requestType); conn.setReadTimeout(readTimeout); addCrumbToConnection(conn, context, overrideAuth, isCrubmCacheEnabled); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java new file mode 100644 index 00000000..ad19020c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java @@ -0,0 +1,75 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import hudson.Plugin; +import hudson.PluginWrapper; +import hudson.model.Run; +import io.jenkins.plugins.opentelemetry.job.OtelTraceService; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class OtelUtils { + + private static String TRACE_PARENT_VERSION = "00"; + private static String TRACE_PARENT_TRACE_FLAG = "01"; + + public static String getTraceParent() { + return Optional.ofNullable(Span.fromContextOrNull(Context.current())) + .map(OtelUtils::genTraceParent) + .orElse(null); + + } + + + public static AutoCloseable activeSpanIfAvailable(StepContext stepContext) { + try { + FlowNode flowNode = stepContext.get(FlowNode.class); + Run run = stepContext.get(Run.class); + return Optional.ofNullable(Jenkins.get().getExtensionList(OtelTraceService.class)) + .filter(list -> list.size()>0) + .map(list -> list.get(0)) + .map(otelTraceServices -> otelTraceServices.getSpan(run, flowNode)) + .map(Span::makeCurrent) + .map(AutoCloseable.class::cast) + .orElseGet(OtelUtils::noop); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static AutoCloseable activeSpanIfAvailable(Run run) { + return Optional.ofNullable(Jenkins.get().getExtensionList(OtelTraceService.class)) + .filter(list -> list.size()>0) + .map(list -> list.get(0)) + .map(otelTraceServices -> otelTraceServices.getSpan(run)) + .map(Span::makeCurrent) + .map(AutoCloseable.class::cast) + .orElseGet(OtelUtils::noop); + } + + @NotNull + public static AutoCloseable noop() { + return () -> { + }; + } + + @NotNull + public static boolean isOpenTelemetryAvailable() { + return Optional.ofNullable(Jenkins.get().getPlugin("opentelemetry")) + .map(Plugin::getWrapper) + .map(PluginWrapper::isActive) + .orElse(false); + } + + @NotNull + private static String genTraceParent(Span span) { + return TRACE_PARENT_VERSION + "-" + span.getSpanContext().getTraceId() + "-" + span.getSpanContext().getSpanId() + "-" + TRACE_PARENT_TRACE_FLAG; + } + + +} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java new file mode 100644 index 00000000..fd989d24 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java @@ -0,0 +1,191 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import hudson.model.FreeStyleProject; +import hudson.security.AuthorizationStrategy; +import hudson.security.SecurityRealm; +import io.jenkins.plugins.opentelemetry.OpenTelemetryConfiguration; +import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; +import org.jetbrains.annotations.NotNull; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockserver.client.MockServerClient; +import org.mockserver.junit.MockServerRule; +import org.mockserver.mock.Expectation; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.JsonBody.json; +import static org.mockserver.model.Parameter.param; + + +public class OpenTelemeterTest { + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + + @Rule + public MockServerRule mockServerRule = new MockServerRule(this); + + private MockServerClient mockServerClient; + + private void disableAuth() { + jenkinsRule.jenkins.setAuthorizationStrategy(AuthorizationStrategy.Unsecured.UNSECURED); + jenkinsRule.jenkins.setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); + jenkinsRule.jenkins.setCrumbIssuer(null); + } + + @Test + public void testRemoteBuild() throws Exception { + disableAuth(); + initOpenTelemetry(); + String[] allExpectation = setupRemoteJenkinsMock(); + FreeStyleProject project = createProjectTriggerFrom(); + //Trigger build + jenkinsRule.waitUntilNoActivity(); + jenkinsRule.buildAndAssertSuccess(project); + mockServerClient.verify(allExpectation); + } + + @NotNull + private FreeStyleProject createProjectTriggerFrom() throws IOException { + FreeStyleProject project = jenkinsRule.createFreeStyleProject(); + RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); + configuration.setJob(createJobUrl()); + configuration.setPreventRemoteBuildQueue(false); + configuration.setBlockBuildUntilComplete(true); + configuration.setPollInterval(1); + configuration.setHttpGetReadTimeout(1000); + configuration.setHttpPostReadTimeout(1000); + configuration.setUseCrumbCache(false); + configuration.setUseJobInfoCache(false); + configuration.setEnhancedLogging(true); + configuration.setTrustAllCertificates(true); + project.getBuildersList().add(configuration); + return project; + } + + @NotNull + private String[] setupRemoteJenkinsMock() { + Expectation[] metaExp = mockServerClient.when( + request() + .withMethod("GET") + .withPath("/job/remote1/api/json") + .withQueryStringParameters( + param("tree", "actions[parameterDefinitions],property[parameterDefinitions],name,fullName,displayName,fullDisplayName,url") + ) + ).respond( + response() + .withBody(json(ImmutableMap.of( + "_class", "org.jenkinsci.plugins.workflow.job.WorkflowJob", + "actions", ImmutableList.of(ImmutableMap.of("_class", "someactionclass")), + "displayName", "remote1", + "fullDisplayName", "remote1", + "fullName", "remote1", + "name", "remote1", + "url", createJobUrl(), + "property", ImmutableList.of(ImmutableMap.of("_class", "somepropertyclass")) + ))) + ); + String jobQueue = "http://localhost:" + mockServerClient.getPort() + "/queue/item/311/"; + Expectation[] jobBuildExp = mockServerClient.when( + request() + .withMethod("POST") + .withPath("/job/remote1/build") + .withQueryStringParameters( + param("delay", "0") + ) + .withHeader("traceparent", "00-[0-9A-F]{32}-[0-9A-F]{16}-01") //https://www.w3.org/TR/trace-context/#traceparent-header-field-values + + ).respond( + response() + .withHeader("location", jobQueue) + ); + + Map mockQueue = ImmutableMap.of( + "_class", "hudson.model.Queue$LeftItem", + "blocked", false, + "buildable", false, + "id", 311, + "executable", ImmutableMap.of( + "_class", "org.jenkinsci.plugins.workflow.job.WorkflowRun", + "number", 34, + "url", "https://jenkins-himalia.aws-devops.itsma-ng.net/job/test1/34/" + ) + + ); + Expectation[] queueExp = mockServerClient.when( + request() + .withMethod("GET") + .withPath("/queue/item/311/api/json/") + + + ).respond( + response() + .withBody(json(mockQueue)) + ); + + + Expectation[] jobResultExp = mockServerClient.when( + request() + .withMethod("GET") + .withPath("/job/test1/34/api/json/") + .withQueryStringParameter("tree", "result,building") + + + ).respond( + response() + .withBody(json(ImmutableMap.of("_class", "org.jenkinsci.plugins.workflow.job.WorkflowRun", + "building", false, + "result", "SUCCESS"))) + ); + + + Expectation[] progressiveTextExp = mockServerClient.when( + request() + .withMethod("GET") + .withPath("/job/test1/34/logText/progressiveText") + + + ).respond( + response() + .withBody("job output") + ); + String[] allExp = Stream.of(metaExp, jobBuildExp, queueExp, jobResultExp, progressiveTextExp) + .flatMap(Arrays::stream) + .map(Expectation::getId) + .toArray(String[]::new); + return allExp; + } + + @NotNull + private String createJobUrl() { + return "http://localhost:" + mockServerClient.getPort() + "/job/remote1"; + } + + private void initOpenTelemetry() { + OpenTelemetrySdkProvider openTelemetrySdkProviders = jenkinsRule.getInstance().getExtensionList(OpenTelemetrySdkProvider.class).get(0); + String mockOtelUrl = "http://localhost:" + mockServerClient.getPort() + "/otel/"; + OpenTelemetryConfiguration config = new OpenTelemetryConfiguration( + Optional.of(mockOtelUrl), + Optional.empty(), + Optional.empty(), + Optional.of(1000), + Optional.of(1000), + Optional.of("jenkins"), + Optional.of("jenkins"), + Optional.empty(), + ImmutableMap.of("otel.exporter.otlp.protocol", "http/protobuf") + ); + + openTelemetrySdkProviders.initialize(config); + } +} From 2b88557190b6862ea2cea37b01374e5942cb9f6f Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 5 Sep 2023 01:10:29 +0800 Subject: [PATCH 239/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 91f964cc..5a06c34d 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Parameterized-Remote-Trigger - 3.1.6.4-SNAPSHOT + 3.2.0-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. From 9b3e307f162f43f76ea9b67f0448ced33e311e16 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 5 Sep 2023 01:17:56 +0800 Subject: [PATCH 240/262] Update change log for 3.1.6.4 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e927dbb..0993d270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 3.1.6.4 (Sep 5th, 2023) + +### Bug fixes + +* When enhancedLogging is set and remote log is trimmed. It adds a last poll for logs after the remote build finished. + # 3.1.6.3 (July 22th, 2022) ### Bug fixes From cb53520a5d34f940afb062880a3d85b85057dafe Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 5 Sep 2023 01:50:21 +0800 Subject: [PATCH 241/262] Update change log for 3.2.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0993d270..6fcb9900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.2.0 (Sep 5th, 2023) + +### Improvement + +* Support opentelemeter. +* Upgrade to Jenkins 2.346.3 baseline. + # 3.1.6.4 (Sep 5th, 2023) ### Bug fixes From 3c11d1a9559126bc706ed7d5b4b29a84502213ca Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 5 Sep 2023 02:05:25 +0800 Subject: [PATCH 242/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.2.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index cd745222..60710707 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ Parameterized-Remote-Trigger - 3.2.0-SNAPSHOT + 3.2.0 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -54,7 +54,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + Parameterized-Remote-Trigger-3.2.0 From 57036daf731d28646011fe60db098f9c1c7e0911 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 5 Sep 2023 02:05:31 +0800 Subject: [PATCH 243/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 60710707..d9c2c554 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ Parameterized-Remote-Trigger - 3.2.0 + 3.2.1-SNAPSHOT hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -54,7 +54,7 @@ scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git https://github.com/jenkinsci/parameterized-remote-trigger-plugin - Parameterized-Remote-Trigger-3.2.0 + HEAD From 15e9b11494c32ac75a21a080d0594c0eff0f664a Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 22 Sep 2023 16:40:44 -0700 Subject: [PATCH 244/262] Refresh plugin for September 2023 --- .mvn/extensions.xml | 7 +++ .mvn/maven.config | 2 + Jenkinsfile | 5 ++- pom.xml | 44 +++++++++++-------- .../utils/OtelUtils.java | 8 ++-- .../OpenTelemeterTest.java | 8 ++-- 6 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 .mvn/extensions.xml create mode 100644 .mvn/maven.config diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 00000000..1f363640 --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.7 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 00000000..2a0299c4 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,2 @@ +-Pconsume-incrementals +-Pmight-produce-incrementals diff --git a/Jenkinsfile b/Jenkinsfile index 7c0976c2..35927195 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,4 +5,7 @@ // - latest Pipeline plugins, 'Timestamper' plugin // - recommended to use this Jenkinsfile with 'Multibranch Pipeline' plugin -buildPlugin() \ No newline at end of file +buildPlugin(useContainerAgent: true, configurations: [ + [platform: 'linux', jdk: 21], + [platform: 'windows', jdk: 17], +]) diff --git a/pom.xml b/pom.xml index d9c2c554..776b0ed2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,22 +1,24 @@ + 4.0.0 org.jenkins-ci.plugins plugin - 4.50 + 4.73 - - 2.346.3 - 1.8 - 1.8 - - 3.1.6 - + + 3.2.1 + -SNAPSHOT + 2.387.3 + jenkinsci/parameterized-remote-trigger-plugin + + 3.1.6 + Parameterized-Remote-Trigger - 3.2.1-SNAPSHOT + ${revision}${changelist} hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -51,10 +53,10 @@ - scm:git:git://github.com/jenkinsci/parameterized-remote-trigger-tlugin.git - scm:git:git@github.com:jenkinsci/parameterized-remote-trigger-plugin.git - https://github.com/jenkinsci/parameterized-remote-trigger-plugin - HEAD + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + https://github.com/${gitHubRepo} + ${scmTag} @@ -75,8 +77,8 @@ io.jenkins.tools.bom - bom-2.346.x - 1742.vb_70478c1b_25f + bom-2.387.x + 2446.v2e9fd3b_d8c81 import pom @@ -104,20 +106,26 @@ io.jenkins.plugins opentelemetry + 2.16.0 true - 2.10.0 org.mockito mockito-core - 2.18.3 test org.mock-server mockserver-junit-rule - 5.14.0 + 5.15.0 test + + + + javax.servlet + javax.servlet-api + + diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java index ad19020c..bb240aa2 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/OtelUtils.java @@ -9,7 +9,7 @@ import jenkins.model.Jenkins; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jetbrains.annotations.NotNull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Optional; @@ -52,13 +52,13 @@ public static AutoCloseable activeSpanIfAvailable(Run run) { .orElseGet(OtelUtils::noop); } - @NotNull + @NonNull public static AutoCloseable noop() { return () -> { }; } - @NotNull + @NonNull public static boolean isOpenTelemetryAvailable() { return Optional.ofNullable(Jenkins.get().getPlugin("opentelemetry")) .map(Plugin::getWrapper) @@ -66,7 +66,7 @@ public static boolean isOpenTelemetryAvailable() { .orElse(false); } - @NotNull + @NonNull private static String genTraceParent(Span span) { return TRACE_PARENT_VERSION + "-" + span.getSpanContext().getTraceId() + "-" + span.getSpanContext().getSpanId() + "-" + TRACE_PARENT_TRACE_FLAG; } diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java index fd989d24..df5023e4 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java @@ -7,7 +7,7 @@ import hudson.security.SecurityRealm; import io.jenkins.plugins.opentelemetry.OpenTelemetryConfiguration; import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; -import org.jetbrains.annotations.NotNull; +import edu.umd.cs.findbugs.annotations.NonNull; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -55,7 +55,7 @@ public void testRemoteBuild() throws Exception { mockServerClient.verify(allExpectation); } - @NotNull + @NonNull private FreeStyleProject createProjectTriggerFrom() throws IOException { FreeStyleProject project = jenkinsRule.createFreeStyleProject(); RemoteBuildConfiguration configuration = new RemoteBuildConfiguration(); @@ -73,7 +73,7 @@ private FreeStyleProject createProjectTriggerFrom() throws IOException { return project; } - @NotNull + @NonNull private String[] setupRemoteJenkinsMock() { Expectation[] metaExp = mockServerClient.when( request() @@ -166,7 +166,7 @@ private String[] setupRemoteJenkinsMock() { return allExp; } - @NotNull + @NonNull private String createJobUrl() { return "http://localhost:" + mockServerClient.getPort() + "/job/remote1"; } From b2ec6c7f0b0dd17ab9c1ef6d5c1c08b6fa039e9f Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Tue, 7 May 2024 19:09:26 -0600 Subject: [PATCH 245/262] Replace JSR-305 annotations with spotbugs annotations Annotations for Nonnull, CheckForNull, and several others were proposed for Java as part of dormant Java specification request JSR-305. The proposal never became a part of standard Java. Jenkins plugins should switch from using JSR-305 annotations to use Spotbugs annotations that provide the same semantics. The [mailing list discussion](https://groups.google.com/g/jenkinsci-dev/c/uE1wwtVi1W0/m/gLxdEJmlBQAJ) from James Nord describes the affected annotations and why they should be replaced with annotations that are actively maintained. The ["Improve a plugin" tutorial](https://www.jenkins.io/doc/developer/tutorial-improve/replace-jsr-305-annotations/) provides instructions to perform this change. An [OpenRewrite recipe](https://docs.openrewrite.org/recipes/jenkins/javaxannotationstospotbugs) is also available and is even better than the tutorial. Confirmed that automated tests pass on Linux with Java 21. --- .../BasicBuildContext.java | 6 +-- .../BuildContext.java | 20 +++++----- .../ConnectionResponse.java | 21 +++++----- .../RemoteBuildConfiguration.java | 23 +++++------ .../RemoteJenkinsServer.java | 2 +- .../parameters2/FileParameters.java | 5 ++- .../parameters2/MapParameter.java | 5 ++- .../parameters2/MapParameters.java | 3 +- .../parameters2/StringParameters.java | 5 ++- .../pipeline/Handle.java | 39 +++++++++---------- .../pipeline/RemoteBuildPipelineStep.java | 4 +- .../remoteJob/QueueItem.java | 16 ++++---- .../remoteJob/QueueItemData.java | 26 ++++++------- .../remoteJob/RemoteBuildInfo.java | 26 ++++++------- .../RemoteBuildInfoExporterAction.java | 6 +-- .../utils/Base64Utils.java | 8 ++-- .../utils/HttpHelper.java | 3 +- 17 files changed, 106 insertions(+), 112 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java index 58fff64d..e2964cb3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java @@ -1,8 +1,7 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNullableByDefault; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.FilePath; import hudson.model.Run; @@ -14,7 +13,6 @@ *
    * The reason for wrapping is simplicity. */ -@ParametersAreNullableByDefault public class BasicBuildContext { @Nullable @CheckForNull diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java index 9be6270c..c34aeddf 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -4,9 +4,8 @@ import java.io.PrintStream; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNullableByDefault; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.pipeline.Handle; @@ -23,39 +22,38 @@ * want to provide a {@link PrintStream} for logging. Therefore the first three objects can be null, the {@link PrintStream} * must not be null. */ -@ParametersAreNullableByDefault public class BuildContext extends BasicBuildContext { - @Nonnull + @NonNull public final PrintStream logger; - @Nonnull + @NonNull public RemoteJenkinsServer effectiveRemoteServer; /** * The current Item (job, pipeline,...) where the plugin is used from. */ - @Nonnull + @NonNull public final String currentItem; - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) { + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @NonNull PrintStream logger, @NonNull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) { super(run, workspace, listener); this.logger = logger; this.effectiveRemoteServer = effectiveRemoteServer; this.currentItem = getCurrentItem(run, currentItem); } - public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer) { + public BuildContext(@Nullable Run run, @Nullable FilePath workspace, @Nullable TaskListener listener, @NonNull PrintStream logger, @NonNull RemoteJenkinsServer effectiveRemoteServer) { this(run, workspace, listener, logger, effectiveRemoteServer, null); } - public BuildContext(@Nonnull PrintStream logger, @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) + public BuildContext(@NonNull PrintStream logger, @NonNull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) { this(null, null, null, logger, effectiveRemoteServer, currentItem); } - @Nonnull + @NonNull private String getCurrentItem(Run run, String currentItem) { String runItem = null; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java index 38443d47..7d2575c6 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -1,22 +1,23 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import edu.umd.cs.findbugs.annotations.NonNull; + import net.sf.json.JSONObject; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.stream.Collectors; - +import java.util.stream.Collectors; + /** * Http response containing header, body (JSON format) and response code. * */ public class ConnectionResponse { - @Nonnull + @NonNull private final Map> header = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); @Nullable @CheckForNull @@ -25,11 +26,11 @@ public class ConnectionResponse @Nullable @CheckForNull private final String rawBody; - @Nonnull + @NonNull private final int responseCode; - public ConnectionResponse(@Nonnull Map> header, @Nullable JSONObject body, @Nonnull int responseCode) + public ConnectionResponse(@NonNull Map> header, @Nullable JSONObject body, @NonNull int responseCode) { loadHeader(header); this.body = body; @@ -37,7 +38,7 @@ public ConnectionResponse(@Nonnull Map> header, @Nullable J this.responseCode = responseCode; } - public ConnectionResponse(@Nonnull Map> header, @Nullable String rawBody, @Nonnull int responseCode) + public ConnectionResponse(@NonNull Map> header, @Nullable String rawBody, @NonNull int responseCode) { loadHeader(header); this.body = null; @@ -45,7 +46,7 @@ public ConnectionResponse(@Nonnull Map> header, @Nullable S this.responseCode = responseCode; } - public ConnectionResponse(@Nonnull Map> header, @Nonnull int responseCode) + public ConnectionResponse(@NonNull Map> header, @NonNull int responseCode) { loadHeader(header); this.body = null; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index eb882018..24cc254e 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -10,10 +10,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import net.sf.json.JSONObject; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNullableByDefault; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; import java.io.PrintStream; import java.io.Serializable; @@ -80,7 +78,6 @@ /** * @author Maurice W. */ -@ParametersAreNullableByDefault public class RemoteBuildConfiguration extends Builder implements SimpleBuildStep, Serializable { private static final long serialVersionUID = -4059001060991775146L; @@ -335,7 +332,7 @@ public Map getParameterMap(BuildContext context) throws AbortExc * @throws MalformedURLException if remoteJenkinsName no valid URL * or job an URL but nor valid. */ - @Nonnull + @NonNull public RemoteJenkinsServer evaluateEffectiveRemoteHost(BasicBuildContext context) throws IOException { RemoteJenkinsServer globallyConfiguredServer = findRemoteHost(this.remoteJenkinsName); RemoteJenkinsServer server = globallyConfiguredServer; @@ -752,8 +749,8 @@ public void performWaitForBuild(BuildContext context, Handle handle) throws IOEx * @throws InterruptedException if any thread has interrupted the current * thread. */ - @Nonnull - private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildContext context) + @NonNull + private QueueItemData getQueueItemData(@NonNull String queueId, @NonNull BuildContext context) throws IOException, InterruptedException { if (context.effectiveRemoteServer.getAddress() == null) { @@ -788,8 +785,8 @@ private QueueItemData getQueueItemData(@Nonnull String queueId, @Nonnull BuildCo return queueItem; } - @Nonnull - public RemoteBuildInfo updateBuildInfo(@Nonnull RemoteBuildInfo buildInfo, @Nonnull BuildContext context) + @NonNull + public RemoteBuildInfo updateBuildInfo(@NonNull RemoteBuildInfo buildInfo, @NonNull BuildContext context) throws IOException, InterruptedException { if (buildInfo.isNotTriggered()) @@ -905,7 +902,7 @@ private void logAuthInformation(@NonNull BuildContext context) { } } - private void logConfiguration(@Nonnull BuildContext context, Map effectiveParams) throws IOException { + private void logConfiguration(@NonNull BuildContext context, Map effectiveParams) throws IOException { String _job = getJob(); String _jobExpanded = getJobExpanded(context); String _jobExpandedLogEntry = (_job.equals(_jobExpanded)) ? "" : "(" + _jobExpanded + ")"; @@ -1048,7 +1045,7 @@ public boolean isDisabled() { return disabled; } - private @Nonnull JSONObject getRemoteJobMetadata(String jobNameOrUrl, @NonNull BuildContext context) + private @NonNull JSONObject getRemoteJobMetadata(String jobNameOrUrl, @NonNull BuildContext context) throws IOException, InterruptedException { String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); @@ -1271,7 +1268,7 @@ public FormValidation doCheckRemoteJenkinsName(@QueryParameter("remoteJenkinsNam } @Restricted(NoExternalUse.class) - @Nonnull + @NonNull public ListBoxModel doFillRemoteJenkinsNameItems() { ListBoxModel model = new ListBoxModel(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 661990f3..b9cff988 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -10,7 +10,7 @@ import java.security.SecureRandom; import java.util.List; -import javax.annotation.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckForNull; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java index 30c3c099..c414d460 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java @@ -3,8 +3,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; -import javax.annotation.Nonnull; import java.io.BufferedReader; + +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.io.InputStreamReader; import java.util.Map; @@ -92,7 +93,7 @@ private String readParametersFile(final BuildContext context) throws AbortExcept @Symbol("FileParameters") public static class FileParametersDescriptor extends ParametersDescriptor { - @Nonnull + @NonNull @Override public String getDisplayName() { return "File parameters"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java index da8a5bde..0fdf9dd4 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; + import java.io.Serializable; import java.util.Objects; @@ -60,7 +61,7 @@ public Descriptor getDescriptor() { @Symbol("MapParameter") public static class MapParameterDescriptor extends Descriptor { - @Nonnull + @NonNull @Override public String getDisplayName() { return "Map parameter"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java index 1e46731b..67c7e430 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java @@ -4,7 +4,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; -import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -75,7 +74,7 @@ public Map getParametersMap(final BuildContext context) { @Symbol("MapParameters") public static class MapParametersDescriptor extends ParametersDescriptor { - @Nonnull + @NonNull @Override public String getDisplayName() { return "Map parameters"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java index 3d4954b8..0e4f850d 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; + import java.util.Map; import java.util.Objects; @@ -55,7 +56,7 @@ public Map getParametersMap(final BuildContext context) { @Symbol("StringParameters") public static class StringParametersDescriptor extends ParametersDescriptor { - @Nonnull + @NonNull @Override public String getDisplayName() { return "String parameters"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java index bdb91b1c..d017dcba 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -4,16 +4,16 @@ import static org.apache.commons.lang.StringUtils.trimToNull; import java.io.IOException; + import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.util.Optional; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNullableByDefault; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; @@ -33,15 +33,14 @@ * environment variables (like in a Job). This prevents issues e.g. when triggering * remote jobs in a parallel pipeline step. */ -@ParametersAreNullableByDefault public class Handle implements Serializable { private static final long serialVersionUID = 4418782245518194292L; - @Nonnull + @NonNull private final RemoteBuildConfiguration remoteBuildConfiguration; - @Nonnull + @NonNull private RemoteBuildInfo buildInfo; @Nullable @@ -58,10 +57,10 @@ public class Handle implements Serializable { /** * The current local Item (Job, Pipeline,...) where this plugin is currently used. */ - @Nonnull + @NonNull private final String currentItem; - @Nonnull + @NonNull private final RemoteJenkinsServer effectiveRemoteServer; /* @@ -71,12 +70,12 @@ public class Handle implements Serializable { * already finished. * TODO: Once we found a way to log to the pipeline log directly we can switch */ - @Nonnull + @NonNull private String lastLog; - public Handle(@Nonnull RemoteBuildConfiguration remoteBuildConfiguration, @Nonnull RemoteBuildInfo buildInfo, @Nonnull String currentItem, - @Nonnull RemoteJenkinsServer effectiveRemoteServer, @Nonnull JSONObject remoteJobMetadata) + public Handle(@NonNull RemoteBuildConfiguration remoteBuildConfiguration, @NonNull RemoteBuildInfo buildInfo, @NonNull String currentItem, + @NonNull RemoteJenkinsServer effectiveRemoteServer, @NonNull JSONObject remoteJobMetadata) { this.remoteBuildConfiguration = remoteBuildConfiguration; this.buildInfo = buildInfo; @@ -183,7 +182,7 @@ public URL getBuildUrl() { * * @return the build number, or 0 if it could not be identified (yet). */ - @Nonnull + @NonNull @Whitelisted public int getBuildNumber() { return buildInfo.getBuildNumber(); @@ -194,7 +193,7 @@ public int getBuildNumber() { * * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildInfo} the build info */ - @Nonnull + @NonNull @Whitelisted public RemoteBuildInfo getBuildInfo() { return buildInfo; @@ -205,7 +204,7 @@ public RemoteBuildInfo getBuildInfo() { * * @return {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status */ - @Nonnull + @NonNull @Whitelisted public RemoteBuildStatus getBuildStatus() { return buildInfo.getStatus(); @@ -223,7 +222,7 @@ public RemoteBuildStatus getBuildStatus() { * @throws InterruptedException * if any thread has interrupted the current thread. */ - @Nonnull + @NonNull @Whitelisted public RemoteBuildStatus updateBuildStatus() throws IOException, InterruptedException { return updateBuildStatus(false); @@ -241,13 +240,13 @@ public RemoteBuildStatus updateBuildStatus() throws IOException, InterruptedExce * @throws InterruptedException * if any thread has interrupted the current thread. */ - @Nonnull + @NonNull @Whitelisted public RemoteBuildStatus updateBuildStatusBlocking() throws IOException, InterruptedException { return updateBuildStatus(true); } - @Nonnull + @NonNull private RemoteBuildStatus updateBuildStatus(boolean blockUntilFinished) throws IOException, InterruptedException { //Return if buildStatus exists and is final (does not change anymore) if(buildInfo.isFinished()) return buildInfo.getStatus(); @@ -275,7 +274,7 @@ public void setBuildInfo(RemoteBuildInfo buildInfo) * * @return {@link hudson.model.Result} the build result */ - @Nonnull + @NonNull @Whitelisted public Result getBuildResult() { return buildInfo.getResult(); @@ -288,7 +287,7 @@ public Result getBuildResult() { * * @return The latest log entries from the last called method. */ - @Nonnull + @NonNull @Whitelisted public String lastLog() { String log = lastLog.trim(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java index 014e7c89..2faf660f 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -32,7 +32,7 @@ import java.util.Map.Entry; import java.util.Set; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BasicBuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; @@ -299,7 +299,7 @@ public Set> getRequiredContext() { } @Restricted(NoExternalUse.class) - @Nonnull + @NonNull public ListBoxModel doFillRemoteJenkinsNameItems() { RemoteBuildConfiguration.DescriptorImpl descriptor = Descriptor.findByDescribableClassName( ExtensionList.lookup(RemoteBuildConfiguration.DescriptorImpl.class), diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java index 3026de2c..9887783a 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java @@ -3,10 +3,10 @@ import java.util.List; import java.util.Map; -import javax.annotation.Nonnull; - -import hudson.AbortException; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.AbortException; + /** * Represents an item on the queue. Contains information about the location * of the job in the queue. @@ -16,14 +16,14 @@ public class QueueItem { final static private String key = "Location"; - @Nonnull + @NonNull private final String location; - @Nonnull + @NonNull private final String id; - public QueueItem(@Nonnull Map> header) throws AbortException + public QueueItem(@NonNull Map> header) throws AbortException { if (!header.containsKey(key)) throw new AbortException(String.format("Error triggering the remote job. The header of the response has an unexpected format: %n%s", header)); @@ -36,12 +36,12 @@ public QueueItem(@Nonnull Map> header) throws AbortException } } - @Nonnull + @NonNull public String getLocation() { return location; } - @Nonnull + @NonNull public String getId() { return id; } diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java index b0ba0d4b..69535bb3 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -3,28 +3,28 @@ import java.net.MalformedURLException; import java.net.URL; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; - -import net.sf.json.JSONException; -import net.sf.json.JSONObject; - +import net.sf.json.JSONException; + +import net.sf.json.JSONObject; + /** * Contains information about the remote job while is waiting on the queue. * */ public class QueueItemData { - @Nonnull + @NonNull private QueueItemStatus status; @Nullable private String why; - @Nonnull + @NonNull private int buildNumber; @Nullable @@ -71,7 +71,7 @@ public boolean isCancelled() return status == QueueItemStatus.CANCELLED; } - @Nonnull + @NonNull public QueueItemStatus getStatus() { return status; } @@ -81,7 +81,7 @@ public String getWhy() { return why; } - @Nonnull + @NonNull public int getBuildNumber() { return buildNumber; @@ -103,7 +103,7 @@ public URL getBuildURL() * @throws MalformedURLException * if there is an error creating the build URL. */ - public void update(@Nonnull BuildContext context, @Nonnull JSONObject queueResponse) throws MalformedURLException + public void update(@NonNull BuildContext context, @NonNull JSONObject queueResponse) throws MalformedURLException { if (queueResponse.getBoolean("blocked")) status = QueueItemStatus.BLOCKED; if (queueResponse.getBoolean("buildable")) status = QueueItemStatus.BUILDABLE; @@ -134,7 +134,7 @@ public void update(@Nonnull BuildContext context, @Nonnull JSONObject queueRespo } } - private boolean getOptionalBoolean(@Nonnull JSONObject queueResponse, @Nonnull String attribute) + private boolean getOptionalBoolean(@NonNull JSONObject queueResponse, @NonNull String attribute) { if (queueResponse.containsKey(attribute)) return queueResponse.getBoolean(attribute); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java index e0d7def3..4e1dea52 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -3,13 +3,13 @@ import java.io.Serializable; import java.net.URL; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.AbortException; -import hudson.model.Result; - +import hudson.model.Result; + /** * This class contains information about the remote build. * @@ -39,16 +39,16 @@ public class RemoteBuildInfo implements Serializable @CheckForNull private String queueId; - @Nonnull + @NonNull private int buildNumber; @CheckForNull private URL buildURL; - @Nonnull + @NonNull private RemoteBuildStatus status; - @Nonnull + @NonNull private Result result; @@ -63,7 +63,7 @@ public String getQueueId() { return queueId; } - @Nonnull + @NonNull public int getBuildNumber() { return buildNumber; @@ -75,13 +75,13 @@ public URL getBuildURL() return buildURL; } - @Nonnull + @NonNull public RemoteBuildStatus getStatus() { return status; } - @Nonnull + @NonNull public Result getResult() { return result; @@ -92,7 +92,7 @@ public void setQueueId(String queueId) { this.status = RemoteBuildStatus.QUEUED; } - public void setBuildData(@Nonnull int buildNumber, @Nullable URL buildURL) throws AbortException + public void setBuildData(@NonNull int buildNumber, @Nullable URL buildURL) throws AbortException { if (buildURL == null) { throw new AbortException(String.format("Unexpected remote build status: %s", toString())); @@ -125,7 +125,7 @@ public void setBuildResult(String result) this.result = Result.fromString(result); } - @Nonnull + @NonNull @Override public String toString() { diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java index 75431d3b..5858afd8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java @@ -1,13 +1,13 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; +import edu.umd.cs.findbugs.annotations.NonNull; + import java.net.URL; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import javax.annotation.Nonnull; - import hudson.EnvVars; import hudson.model.AbstractBuild; import hudson.model.EnvironmentContributingAction; @@ -33,7 +33,7 @@ public RemoteBuildInfoExporterAction(Run parentBuild, BuildReference build addBuildReferenceSafe(buildRef); } - public static RemoteBuildInfoExporterAction addBuildInfoExporterAction(@Nonnull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { + public static RemoteBuildInfoExporterAction addBuildInfoExporterAction(@NonNull Run parentBuild, String triggeredProjectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { BuildReference reference = new BuildReference(triggeredProjectName, buildNumber, jobURL, buildInfo); RemoteBuildInfoExporterAction action; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java index 396659f7..ec53f2f1 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java @@ -3,9 +3,9 @@ import static org.apache.commons.lang.StringUtils.isEmpty; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.UnsupportedEncodingException; import org.apache.commons.codec.binary.Base64; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; @@ -41,7 +41,7 @@ public static String encode(String input) throws UnsupportedEncodingException * if there is a failure while replacing token macros, or * if there is a failure while encoding user:password. */ - @Nonnull + @NonNull public static String generateAuthorizationHeaderValue(String authType, String user, String password, BuildContext context, boolean applyMacro) throws IOException { @@ -56,7 +56,7 @@ public static String generateAuthorizationHeaderValue(String authType, String us return authTypeKey + " " + encodedTuple; } - @Nonnull + @NonNull private static String getAuthType(String authType) { if ("Basic".equalsIgnoreCase(authType)) return "Basic"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index 2493a841..d4ea8ad0 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -13,7 +13,6 @@ import net.sf.json.JSONSerializer; import net.sf.json.util.JSONUtils; -import javax.annotation.Nonnull; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; @@ -204,7 +203,7 @@ private static String readInputStream(HttpURLConnection connection) throws IOExc * @throws IOException * if the request failed. */ - @Nonnull + @NonNull private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) throws IOException { String address = context.effectiveRemoteServer.getAddress(); From 61b2cf53c06641a4912c3a3cbd3ea817ba366864 Mon Sep 17 00:00:00 2001 From: Vishal Wagh <169045855+vwagh-dev@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:42:38 +0530 Subject: [PATCH 246/262] Use java.net.HttpURLConnection.HTTP_OK than depend on jetty --- .../ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java index 0787cad6..d7ea4529 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemTest.java @@ -1,13 +1,13 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; import hudson.AbortException; -import org.eclipse.jetty.server.Response; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import java.net.HttpURLConnection; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -66,7 +66,7 @@ public static Collection data() { @Test public void test() { // ConnectionResponse creates case-insensitive map of header - ConnectionResponse connectionResponse = new ConnectionResponse(header, Response.SC_OK); + ConnectionResponse connectionResponse = new ConnectionResponse(header, HttpURLConnection.HTTP_OK); try { QueueItem queueItem = new QueueItem(connectionResponse.getHeader()); From c4a8b138418d3cc0a8bca4074666fb6c51b7e588 Mon Sep 17 00:00:00 2001 From: mlefebvre Date: Tue, 10 Dec 2024 11:43:21 -0800 Subject: [PATCH 247/262] Fix typo in ExceedRetryLimitException --- .../exceptions/ExceedRetryLimitException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java index 37da8801..ca4734f5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java @@ -11,7 +11,7 @@ public class ExceedRetryLimitException extends IOException { @Override public String getMessage() { - return "Max number of connection retries have been exeeded."; + return "Max number of connection retries have been exceeded."; } } From 3472f327958f1606cceb052c0238989dfbdb1d27 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Tue, 7 Jan 2025 10:31:44 -0700 Subject: [PATCH 248/262] Fix Jenkinsfile comments Remove the jdk8 reference, since the plugin no longer supports JDK 8. Provide link to Pipeline library documentation for details. --- Jenkinsfile | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 35927195..140e2bad 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,10 +1,7 @@ -// Builds a module using https://github.com/jenkins-infra/pipeline-library -// Requirements: -// - agents with label 'linux' and 'windows' -// - tools with label 'jdk8' and 'mvn' -// - latest Pipeline plugins, 'Timestamper' plugin -// - recommended to use this Jenkinsfile with 'Multibranch Pipeline' plugin - +/* + See the documentation for more options: + https://github.com/jenkins-infra/pipeline-library/ +*/ buildPlugin(useContainerAgent: true, configurations: [ [platform: 'linux', jdk: 21], [platform: 'windows', jdk: 17], From 89cc05272949d9c62e16f61ccd10f5967e8b6010 Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Wed, 8 Jan 2025 13:03:46 +0100 Subject: [PATCH 249/262] Applied recipe UpgradeToRecommendCoreVersion --- pom.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 776b0ed2..ac3d0de8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,14 +4,16 @@ org.jenkins-ci.plugins plugin - 4.73 + 4.88 3.2.1 -SNAPSHOT - 2.387.3 + + 2.452 + ${jenkins.baseline}.4 jenkinsci/parameterized-remote-trigger-plugin 3.1.6 @@ -77,8 +79,8 @@ io.jenkins.tools.bom - bom-2.387.x - 2446.v2e9fd3b_d8c81 + bom-${jenkins.baseline}.x + 3875.v1df09947cde6 import pom From fc5ad911937aff271a172a7ae31050dcf6f29dc7 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Jan 2025 23:01:25 +0800 Subject: [PATCH 250/262] update change log for 3.2.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fcb9900..b6981c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 3.2.1 (Jan 13th, 2025) + +### Improvement + +* Upgrade toolset. +* Update class dependency. +* Some document refinements. + # 3.2.0 (Sep 5th, 2023) ### Improvement From 86efc89742f00a3c889f5a23db5456f2a37593b0 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Jan 2025 23:07:01 +0800 Subject: [PATCH 251/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.2.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ac3d0de8..dcb26633 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ Parameterized-Remote-Trigger - ${revision}${changelist} + 3.2.1 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -58,7 +58,7 @@ scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} - ${scmTag} + Parameterized-Remote-Trigger-3.2.1 From d804092a063434720568f56acdf6e36cd58a7b7e Mon Sep 17 00:00:00 2001 From: cashlalala Date: Mon, 13 Jan 2025 23:07:20 +0800 Subject: [PATCH 252/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index dcb26633..46780fd3 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ - 3.2.1 + 3.2.2 -SNAPSHOT 2.452 @@ -20,7 +20,7 @@ Parameterized-Remote-Trigger - 3.2.1 + ${revision}${changelist} hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -58,7 +58,7 @@ scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} - Parameterized-Remote-Trigger-3.2.1 + ${scmTag} From 9ca0925e2f04eee396f526ef49652c2764578095 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Tue, 4 Mar 2025 07:57:50 -0800 Subject: [PATCH 253/262] Migrate from EE 8 to EE 9 --- pom.xml | 8 ++++---- .../plugins/ParameterizedRemoteTrigger/Auth.java | 2 +- .../RemoteBuildConfiguration.java | 4 ++-- .../ParameterizedRemoteTrigger/auth2/CredentialsAuth.java | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 46780fd3..19965f9c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 4.88 + 5.8 @@ -12,8 +12,8 @@ 3.2.2 -SNAPSHOT - 2.452 - ${jenkins.baseline}.4 + 2.479 + ${jenkins.baseline}.1 jenkinsci/parameterized-remote-trigger-plugin 3.1.6 @@ -80,7 +80,7 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 3875.v1df09947cde6 + 3944.v1a_e4f8b_452db_ import pom diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index c497ec2e..b2524925 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -136,7 +136,7 @@ public String getDisplayName() { public static ListBoxModel doFillCredsItems() { StandardUsernameListBoxModel model = new StandardUsernameListBoxModel(); - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + Item item = Stapler.getCurrentRequest2().findAncestorObject(Item.class); List listOfAllCredentails = CredentialsProvider.lookupCredentials( StandardUsernameCredentials.class, item, ACL.SYSTEM, Collections. emptyList()); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index 24cc254e..daf012d8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -56,7 +56,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import hudson.AbortException; import hudson.Extension; @@ -1218,7 +1218,7 @@ public String getDisplayName() { } @Override - public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { + public boolean configure(StaplerRequest2 req, JSONObject formData) throws FormException { remoteSites.replaceBy(req.bindJSONToList(RemoteJenkinsServer.class, formData.get("remoteSites"))); save(); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java index 1b7820be..18645a82 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -142,7 +142,7 @@ public String getDisplayName() { public static ListBoxModel doFillCredentialsItems() { StandardUsernameListBoxModel model = new StandardUsernameListBoxModel(); - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + Item item = Stapler.getCurrentRequest2().findAncestorObject(Item.class); List listOfAllCredentails = CredentialsProvider.lookupCredentials( StandardUsernameCredentials.class, item, ACL.SYSTEM, Collections. emptyList()); From 4a515172e87a9db00c47b17a5eab4e5739f27687 Mon Sep 17 00:00:00 2001 From: RubenYoungOn Date: Wed, 6 Aug 2025 14:14:13 +0200 Subject: [PATCH 254/262] JENKINS-75957: Correct bearer token header --- .../ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java index 32e7489b..6e1060ee 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -38,7 +38,7 @@ public Secret getToken() { @Override public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { - connection.setRequestProperty("Authorization", "Bearer: " + getToken().getPlainText()); + connection.setRequestProperty("Authorization", "Bearer " + getToken().getPlainText()); } @Override From e866f2ff447b6b2bd407d3d1d769f48586543609 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 7 Aug 2025 19:34:52 -0400 Subject: [PATCH 255/262] `doCheckAddress` broken for `http` protocol, and has poor feedback --- .../RemoteJenkinsServer.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index b9cff988..04ff8e66 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -5,6 +5,7 @@ import java.io.Serializable; import javax.net.ssl.*; import java.net.URL; +import java.net.URLConnection; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -219,11 +220,13 @@ public FormValidation doCheckAddress(@QueryParameter String address, @QueryParam // check that the host is reachable try { - HttpsURLConnection conn = (HttpsURLConnection) host.openConnection(); - try { - makeConnectionTrustAllCertificates(conn, trustAllCertificates); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - return FormValidation.error(e, "A key management error occurred."); + URLConnection conn = host.openConnection(); + if (conn instanceof HttpsURLConnection sslConn) { + try { + makeConnectionTrustAllCertificates(sslConn, trustAllCertificates); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + return FormValidation.error(e, "A key management error occurred."); + } } conn.setConnectTimeout(5000); conn.connect(); @@ -234,10 +237,10 @@ public FormValidation doCheckAddress(@QueryParameter String address, @QueryParam ); } } catch (Exception e) { - return FormValidation.warning("Address looks good, but a connection could not be established."); + return FormValidation.warning(e, "Address looks good, but a connection could not be established."); } - return FormValidation.ok(); + return FormValidation.ok(host + " is reachable."); } public static List getAuth2Descriptors() { From 7623687b8f1a12438cb177002c62d1018653a676 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 8 Aug 2025 08:14:45 -0400 Subject: [PATCH 256/262] No need to request a crumb when using API token authn --- .../auth2/Auth2.java | 7 ++ .../auth2/TokenAuth.java | 5 ++ .../utils/HttpHelper.java | 87 +++++++------------ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java index 4ce62926..46d60c28 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -42,6 +42,13 @@ public static abstract class Auth2Descriptor extends Descriptor */ public abstract void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException; + /** + * Whether a Jenkins crumb is required on {@code POST} requests. + */ + public boolean requiresCrumb() { + return true; + } + public abstract String toString(); /** diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java index 1b265af3..3f53420c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -55,6 +55,11 @@ public void setAuthorizationHeader(URLConnection connection, BuildContext contex connection.setRequestProperty("Authorization", authHeaderValue); } + @Override + public boolean requiresCrumb() { + return false; + } + @Override public String toString() { return "'" + getDescriptor().getDisplayName() + "' as user '" + getUserName() + "'"; diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index d4ea8ad0..94c5a4dc 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -38,7 +38,6 @@ import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; -import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @@ -204,7 +203,7 @@ private static String readInputStream(HttpURLConnection connection) throws IOExc * if the request failed. */ @NonNull - private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, boolean isCacheEnabled) + private static JenkinsCrumb getCrumb(BuildContext context, Auth2 auth, boolean isCacheEnabled) throws IOException { String address = context.effectiveRemoteServer.getAddress(); if (address == null) { @@ -224,7 +223,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b context.logger.println("reuse cached crumb: " + globalHost); return jenkinsCrumb; } - HttpURLConnection connection = (HttpURLConnection) getAuthorizedConnection(context, crumbProviderUrl, overrideAuth); + HttpURLConnection connection = (HttpURLConnection) getAuthorizedConnection(context, crumbProviderUrl, auth); int responseCode = connection.getResponseCode(); if (responseCode == 401) { throw new UnauthorizedException(crumbProviderUrl); @@ -258,11 +257,11 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b * @param context * @throws IOException */ - private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth, + private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 auth, boolean isCacheEnabled) throws IOException { String method = connection.getRequestMethod(); - if (method != null && method.equalsIgnoreCase("POST")) { - JenkinsCrumb crumb = getCrumb(context, overrideAuth, isCacheEnabled); + if (method != null && method.equalsIgnoreCase("POST") && auth.requiresCrumb()) { + JenkinsCrumb crumb = getCrumb(context, auth, isCacheEnabled); if (crumb.isEnabledOnRemote()) { connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); } @@ -277,24 +276,16 @@ private static void addCrumbToConnection(HttpURLConnection connection, BuildCont * ATTENTION: TRUSTING ALL CERTIFICATES IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO! * @param context The build context * @param url The url to the remote build - * @param overrideAuth + * @param auth * @return An authorized connection with or without a NaiveTrustManager * @throws IOException */ - private static URLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth) + private static URLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 auth) throws IOException { URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) : url.openConnection(); - Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); - - if (overrideAuth != null && !(overrideAuth instanceof NullAuth)) { - // Override Authorization Header if configured locally - overrideAuth.setAuthorizationHeader(connection, context); - } else if (serverAuth != null) { - // Set Authorization Header configured globally for remoteServer - serverAuth.setAuthorizationHeader(connection, context); - } + auth.setAuthorizationHeader(connection, context); if (connection instanceof HttpsURLConnection) { HttpsURLConnection conn = (HttpsURLConnection) connection; @@ -405,7 +396,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, * interval between each retry in second * @param retryLimit * the retry uplimit - * @param overrideAuth + * @param auth * auth used to overwrite the default auth * @param rawRespRef * the raw http response @@ -418,7 +409,7 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken, */ private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, Map postParams, int readTimeout, int numberOfAttempts, int pollInterval, int retryLimit, - Auth2 overrideAuth, StringBuilder rawRespRef, boolean isCrubmCacheEnabled) + Auth2 auth, StringBuilder rawRespRef, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { JSONObject responseObject = null; @@ -433,7 +424,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) getAuthorizedConnection(context, url, overrideAuth); + HttpURLConnection conn = (HttpURLConnection) getAuthorizedConnection(context, url, auth); try { conn.setDoInput(true); @@ -447,7 +438,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } conn.setRequestMethod(requestType); conn.setReadTimeout(readTimeout); - addCrumbToConnection(conn, context, overrideAuth, isCrubmCacheEnabled); + addCrumbToConnection(conn, context, auth, isCrubmCacheEnabled); // wait up to 5 seconds for the connection to be open conn.setConnectTimeout(5000); if (HTTP_POST.equalsIgnoreCase(requestType)) { @@ -538,7 +529,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT context.logger.println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit); numberOfAttempts++; return sendHTTPCall(urlString, requestType, context, postParams, readTimeout, - numberOfAttempts, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); + numberOfAttempts, pollInterval, retryLimit, auth, rawRespRef, isCrubmCacheEnabled); } else { context.logger.println(String.format( @@ -560,12 +551,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, - Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, StringBuilder rawRespRef, + Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 auth, StringBuilder rawRespRef, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { if (lock == null) { context.logger.println("calling remote without locking..."); return sendHTTPCall(urlString, method, context, null, readTimeout, - 1, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); + 1, pollInterval, retryLimit, auth, rawRespRef, isCrubmCacheEnabled); } Boolean isAcquired = null; try { @@ -582,7 +573,7 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, readTimeout, - 1, pollInterval, retryLimit, overrideAuth, rawRespRef, isCrubmCacheEnabled); + 1, pollInterval, retryLimit, auth, rawRespRef, isCrubmCacheEnabled); return cr; } finally { @@ -592,46 +583,34 @@ private static ConnectionResponse tryCall(String urlString, String method, Build } } + private static Auth2 effectiveAuth(BuildContext context, Auth2 overrideAuth) { + if (overrideAuth != null && !(overrideAuth instanceof NullAuth)) { + // use Authorization Header if configured locally + return overrideAuth; + } else { + Auth2 serverAuth = context.effectiveRemoteServer.getAuth2(); + if (serverAuth != null) { + // use Authorization Header configured globally for remoteServer + return serverAuth; + } else { + return NullAuth.INSTANCE; + } + } + } + public static ConnectionResponse tryPost(String urlString, BuildContext context, Map params, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock, boolean isCrubmCacheEnabled) throws IOException, InterruptedException { return tryCall(urlString, HTTP_POST, context, params, readTimeout, pollInterval, retryLimit, - overrideAuth, null, lock, isCrubmCacheEnabled); + effectiveAuth(context, overrideAuth), null, lock, isCrubmCacheEnabled); } public static ConnectionResponse tryGet(String urlString, BuildContext context, int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) throws IOException, InterruptedException { return tryCall(urlString, HTTP_GET, context, null, readTimeout, pollInterval, retryLimit, - overrideAuth, null, lock, false); - } - - public static String tryGetRawResp(String urlString, BuildContext context, int readTimeout, - int pollInterval, int retryLimit, Auth2 overrideAuth, Semaphore lock) - throws IOException, InterruptedException { - StringBuilder resp = new StringBuilder(); - tryCall(urlString, HTTP_GET, context, null, readTimeout, pollInterval, retryLimit, - overrideAuth, resp, lock, false); - return resp.toString(); - } - - public static ConnectionResponse post(String urlString, BuildContext context, Map params, - int readTimeout, int pollInterval, int retryLimit, Auth2 overrideAuth, boolean isCrubmCacheEnabled) - throws IOException, InterruptedException { - return tryPost(urlString, context, params, readTimeout, pollInterval, retryLimit, overrideAuth, - null, isCrubmCacheEnabled); - } - - public static ConnectionResponse get(String urlString, BuildContext context, int readTimeout, - int pollInterval, int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { - return tryGet(urlString, context, readTimeout, pollInterval, retryLimit, overrideAuth, null); - } - - public static String getRawResp(String urlString, String requestType, BuildContext context, - Collection postParams, int readTimeout, int numberOfAttempts, int pollInterval, - int retryLimit, Auth2 overrideAuth) throws IOException, InterruptedException { - return tryGetRawResp(urlString, context, readTimeout, pollInterval, retryLimit, overrideAuth, null); + effectiveAuth(context, overrideAuth), null, lock, false); } } From bb77cae54b050ea2206c0c0567504466fd742fba Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 11 Aug 2025 10:48:10 -0400 Subject: [PATCH 257/262] Include error stream when reporting unexpected HTTP status codes --- .../utils/HttpHelper.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java index d4ea8ad0..b1b33b45 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -48,6 +48,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; @@ -194,6 +195,12 @@ private static String readInputStream(HttpURLConnection connection) throws IOExc } } + private static String readErrorStream(HttpURLConnection connection) throws IOException { + try (InputStream is = connection.getErrorStream()) { + return is != null ? IOUtils.toString(is, StandardCharsets.UTF_8) : ""; + } + } + /** * Tries to obtain a Jenkins Crumb from the remote Jenkins server. * @@ -240,8 +247,8 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b JenkinsCrumb crumb = new JenkinsCrumb(split[0], split[1]); return DropCachePeriodicWork.safePutCrumb(globalHost, crumb, isCacheEnabled); } else { - throw new RuntimeException(String.format("Unexpected response. Response code: %s. Response message: %s", - responseCode, connection.getResponseMessage())); + throw new RuntimeException(String.format("Unexpected response. Response code: %s (%s)%n%s", + responseCode, connection.getResponseMessage(), readErrorStream(connection))); } } catch (FileNotFoundException e) { context.logger.println("CSRF protection is disabled on the remote server."); @@ -525,9 +532,9 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT // If we have connectionRetryLimit set to > 0 then retry that many times. if (numberOfAttempts <= retryLimit) { context.logger.println(String.format( - "Connection to remote server failed [%s], waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s", + "Connection to remote server failed [%s], waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s%n%s", (responseCode == 0 ? e.getMessage() : responseCode), pollInterval, - getUrlWithoutParameters(urlString), parmsString)); + getUrlWithoutParameters(urlString), parmsString, readErrorStream(conn))); // Sleep for 'pollInterval' seconds. // Sleep takes milliseconds so need to convert this.pollInterval to milliseconds @@ -542,9 +549,9 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT } else { context.logger.println(String.format( - "Connection to remote server failed [%s], number of retries exceeded. URL: %s, parameters: %s", + "Connection to remote server failed [%s], number of retries exceeded. URL: %s, parameters: %s%n%s", (responseCode == 0 ? e.getMessage() : responseCode), - getUrlWithoutParameters(urlString), parmsString)); + getUrlWithoutParameters(urlString), parmsString, readErrorStream(conn))); // reached the maximum number of retries, time to fail throw new ExceedRetryLimitException(); From 4a26a4925b790c73abae2f914aa308b0c73daa57 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 11 Aug 2025 11:07:23 -0400 Subject: [PATCH 258/262] Warn about URLs ending with slash --- .../ParameterizedRemoteTrigger/RemoteJenkinsServer.java | 4 ++++ .../ParameterizedRemoteTrigger/utils/FormValidationUtils.java | 1 + 2 files changed, 5 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 04ff8e66..9c3a829c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -210,6 +210,10 @@ public FormValidation doCheckAddress(@QueryParameter String address, @QueryParam return FormValidation.warning("The remote address can not be empty, or it must be overridden on the job configuration."); } + if (address.endsWith("/")) { + return FormValidation.warning("The remote address is not expected to end with a slash (/)."); + } + // check if we have a valid, well-formed URL try { host = new URL(address); diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java index 6aa46c6b..05505438 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -53,6 +53,7 @@ public static RemoteURLCombinationsResult checkRemoteURLCombinations(String remo final String TEXT_WARNING_JOB_VARIABLE = "You are using a variable in the 'Remote Job Name or URL' ('job') field. You have to make sure the value at runtime results in the full job URL"; final String TEXT_ERROR_NO_URL_AT_ALL = "You have to configure either 'Select a remote host' ('remoteJenkinsName'), 'Override remote host URL' ('remoteJenkinsUrl') or specify a full job URL 'Remote Job Name or URL' ('job')"; + // TODO warn about URLs ending with slash if(isEmpty(jobNameOrUrl)) { return new RemoteURLCombinationsResult( FormValidation.error("'Remote Job Name or URL' ('job') not specified"), From f24d7646efa93ea1e247fafb5e0cef2198f1ec02 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 12 Aug 2025 01:26:15 +0800 Subject: [PATCH 259/262] update the change logs --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6981c2c..57ea655f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 3.2.2 (Aug 12th, 2025) + +### Improvements +* Warn about remote Jenkins URLs ending with a slash. (#98) +* No need to request a crumb when using API token authentication. (#96) + +### Bug fixes +* Include error stream when reporting unexpected HTTP status codes. (#97) +* doCheckAddress fixed for http protocol and improved feedback. (#95) +* Correct bearer token header. (#94) + # 3.2.1 (Jan 13th, 2025) ### Improvement From c0d8e9495db796de0ee0e90c611d7e3752810804 Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 12 Aug 2025 01:40:47 +0800 Subject: [PATCH 260/262] [maven-release-plugin] prepare release Parameterized-Remote-Trigger-3.2.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 19965f9c..96ae86e4 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ Parameterized-Remote-Trigger - ${revision}${changelist} + 3.2.2 hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -58,7 +58,7 @@ scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} - ${scmTag} + Parameterized-Remote-Trigger-3.2.2 From c4f866c239a47b1cbbb0cf5af6e10945c026baaf Mon Sep 17 00:00:00 2001 From: cashlalala Date: Tue, 12 Aug 2025 01:41:06 +0800 Subject: [PATCH 261/262] [maven-release-plugin] prepare for next development iteration --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 96ae86e4..b7e8b6d1 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ - 3.2.2 + 3.2.3 -SNAPSHOT 2.479 @@ -20,7 +20,7 @@ Parameterized-Remote-Trigger - 3.2.2 + ${revision}${changelist} hpi Parameterized Remote Trigger Plugin This plugin gives you the ability to trigger parameterized builds on a remote Jenkins server as part of your build. @@ -58,7 +58,7 @@ scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} - Parameterized-Remote-Trigger-3.2.2 + ${scmTag} From 5b8032cdbc13201c41e12e38cb7edb77be207e62 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Fri, 28 Nov 2025 03:31:13 +0000 Subject: [PATCH 262/262] Test with Java 25 and Java 21 Java 25 released September 16, 2025. The Jenkins project wants to support Java 25 soon. Compile and test on ci.jenkins.io with Java 25 and Java 21. Intentionally continues to generate Java 17 byte code as configured by the plugin parent pom. Does not compile or test with Java 17 on ci.jenkins.io any longer because we have found no issues in the past that were specific to the Java 17 compiler. The plan is to drop support for Java 17 in the not too distant future so that the Jenkins project is only supporting two major Java versions at a time, Java 21 and Java 25. Testing done: * Confirmed that automated tests pass with Java 25 --- Jenkinsfile | 4 ++-- pom.xml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 140e2bad..7df47391 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,6 @@ https://github.com/jenkins-infra/pipeline-library/ */ buildPlugin(useContainerAgent: true, configurations: [ - [platform: 'linux', jdk: 21], - [platform: 'windows', jdk: 17], + [platform: 'linux', jdk: 25], + [platform: 'windows', jdk: 21], ]) diff --git a/pom.xml b/pom.xml index b7e8b6d1..57abec8e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 5.8 + 5.28 @@ -13,7 +13,7 @@ -SNAPSHOT 2.479 - ${jenkins.baseline}.1 + ${jenkins.baseline}.3 jenkinsci/parameterized-remote-trigger-plugin 3.1.6 @@ -80,7 +80,7 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 3944.v1a_e4f8b_452db_ + 5054.v620b_5d2b_d5e6 import pom