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/.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/CHANGELOG.md b/CHANGELOG.md index 82cd1b36..57ea655f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,305 @@ -#2.1.4 (May 12th, 2015) -###New Feature/Enhancement: +# 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 + +* Upgrade toolset. +* Update class dependency. +* Some document refinements. + +# 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 + +* 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 + +* Fix pipeline generation error +* Fix pipeline unable to parse + +# 3.1.6.2 (July 18th, 2022) + +### Improvement + +* Mark breaking changes for 3.1.6.x + +# 3.1.6.1 (Jun 14th, 2022) + +### Improvement + +* 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 + +* 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 + +* 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 + +* [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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* None + +### Improvement + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +### 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: +- 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)) -###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)) +### 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)) - Hand-full of other bugs ([pull request](https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/5/commits)): @@ -12,25 +308,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 @@ -39,34 +335,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 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..7df47391 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,8 @@ +/* + See the documentation for more options: + https://github.com/jenkins-infra/pipeline-library/ +*/ +buildPlugin(useContainerAgent: true, configurations: [ + [platform: 'linux', jdk: 25], + [platform: 'windows', jdk: 21], +]) diff --git a/README.md b/README.md index 479ca10a..0e1a1a0e 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,9 @@ 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) - - -Job setup options - -![select from drop-down](https://raw.github.com/morficus/Parameterized-Remote-Trigger-Plugin/master/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) - - -####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..a915c6d5 --- /dev/null +++ b/README_PipelineConfiguration.md @@ -0,0 +1,190 @@ +# 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: '')``` +- **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.
+ ```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 +- `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 +- `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()` +- `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 +``` + +- 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`. + + +
+ +## 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 + handle.updateBuildStatus() +} +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.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. + + +
+ +# 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 3b67fa1d..57abec8e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,74 +1,134 @@ - - 4.0.0 - - org.jenkins-ci.plugins - plugin - 1.509 - - - Parameterized-Remote-Trigger - 2.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. - http://wiki.jenkins-ci.org/display/JENKINS/Parameterized+Remote+Trigger+Plugin - - - - MIT license - All source code is under the MIT license. - - - - - - morficus - Maurice Williams - - - - - - - org.jenkins-ci.tools - maven-hpi-plugin - 1.95 - - - - - - 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 - 1.9.4 - - - org.jenkins-ci.plugins - token-macro - 1.9 - - - - + + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 5.28 + + + + + 3.2.3 + -SNAPSHOT + + 2.479 + ${jenkins.baseline}.3 + jenkinsci/parameterized-remote-trigger-plugin + + 3.1.6 + + + Parameterized-Remote-Trigger + ${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. + https://github.com/jenkinsci/parameterized-remote-trigger-plugin + + + + MIT license + All source code is under the MIT license. + + + + + + cashlalala + KaiHsiang Chang + + + + + + + + + + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + https://github.com/${gitHubRepo} + ${scmTag} + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + + io.jenkins.tools.bom + bom-${jenkins.baseline}.x + 5054.v620b_5d2b_d5e6 + import + pom + + + + + + org.jenkins-ci.plugins + credentials + + + org.jenkins-ci.plugins + token-macro + + + org.jenkins-ci.plugins + script-security + true + + + org.jenkins-ci.plugins.workflow + workflow-step-api + true + + + io.jenkins.plugins + opentelemetry + 2.16.0 + true + + + org.mockito + mockito-core + test + + + org.mock-server + mockserver-junit-rule + 5.15.0 + test + + + + javax.servlet + javax.servlet-api + + + + + + diff --git a/screenshots/1-system-settings.png b/screenshots/1-system-settings.png index 015e2b88..b03fbe1c 100644 Binary files a/screenshots/1-system-settings.png and b/screenshots/1-system-settings.png differ diff --git a/screenshots/2-build-configuration-1.png b/screenshots/2-build-configuration-1.png index 1b0484f6..072dc54d 100644 Binary files a/screenshots/2-build-configuration-1.png and b/screenshots/2-build-configuration-1.png differ diff --git a/screenshots/3-build-configuration-2.png b/screenshots/3-build-configuration-2.png index 732a9fba..30e4ff11 100644 Binary files a/screenshots/3-build-configuration-2.png and b/screenshots/3-build-configuration-2.png differ diff --git a/screenshots/3-build-configuration-2b.png b/screenshots/3-build-configuration-2b.png new file mode 100644 index 00000000..c52200a2 Binary files /dev/null and b/screenshots/3-build-configuration-2b.png differ diff --git a/screenshots/pipelineSyntaxGenerator.png b/screenshots/pipelineSyntaxGenerator.png new file mode 100644 index 00000000..12c15654 Binary files /dev/null and b/screenshots/pipelineSyntaxGenerator.png differ diff --git a/screenshots/pipelineSyntaxGenerator2.png b/screenshots/pipelineSyntaxGenerator2.png new file mode 100644 index 00000000..e301b4b4 Binary files /dev/null and b/screenshots/pipelineSyntaxGenerator2.png differ diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java index cecd4866..b2524925 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/Auth.java @@ -1,109 +1,98 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import net.sf.json.JSONObject; - -import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer.DescriptorImpl; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.CredentialsAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NullAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.TokenAuth; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.exceptions.CredentialsNotFoundException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.StaplerRequest; + +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.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Item; import hudson.security.ACL; -import hudson.util.Secret; import hudson.util.ListBoxModel; +import hudson.util.Secret; -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; +/** + * We need to keep this for compatibility - old config deserialization! + * @deprecated since 2.3.0-SNAPSHOT - use {@link Auth2} instead. + */ +public class Auth extends AbstractDescribableImpl implements Serializable { -public class Auth extends AbstractDescribableImpl { + private static final long serialVersionUID = 5110932168554914718L; + + 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 +100,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 +122,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 @@ -143,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()); @@ -160,6 +153,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().getPlainText(), 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 NullAuth.INSTANCE; + return authToAuth2(oldAuth.get(0)); + } + + public static Auth2 authToAuth2(Auth oldAuth) { + String authType = oldAuth.getAuthType(); + if (Auth.NONE.equals(authType)) { + return NoneAuth.INSTANCE; + } else if (Auth.API_TOKEN.equals(authType)) { + TokenAuth newAuth = new TokenAuth(); + newAuth.setUserName(oldAuth.getUsername()); + newAuth.setApiToken(Secret.fromString(oldAuth.getApiToken())); + return newAuth; + } else if (Auth.CREDENTIALS_PLUGIN.equals(authType)) { + CredentialsAuth newAuth = new CredentialsAuth(); + newAuth.setCredentials(oldAuth.getCreds()); + return newAuth; + } else { + return NullAuth.INSTANCE; + } } } 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..e2964cb3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BasicBuildContext.java @@ -0,0 +1,33 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +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. + */ +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 new file mode 100644 index 00000000..c34aeddf --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildContext.java @@ -0,0 +1,79 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import static org.apache.commons.lang.StringUtils.trimToNull; + +import java.io.PrintStream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +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. + */ +public class BuildContext extends BasicBuildContext +{ + @NonNull + public final PrintStream logger; + + @NonNull + 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, @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) { + this(run, workspace, listener, logger, effectiveRemoteServer, null); + } + + public BuildContext(@NonNull PrintStream logger, @NonNull RemoteJenkinsServer effectiveRemoteServer, @Nullable String currentItem) + { + this(null, null, null, logger, effectiveRemoteServer, currentItem); + } + + @NonNull + 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/BuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildInfoExporterAction.java deleted file mode 100644 index b000c271..00000000 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/BuildInfoExporterAction.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.jenkinsci.plugins.ParameterizedRemoteTrigger; - -import hudson.EnvVars; -import hudson.model.EnvironmentContributingAction; -import hudson.model.Result; -import hudson.model.AbstractBuild; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -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_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_"; - public static final String BUILD_RUN_COUNT_PREFIX = "TRIGGERED_BUILD_RUN_COUNT_"; - public static final String RUN = "_RUN_"; - - private List builds; - - public BuildInfoExporterAction(AbstractBuild parentBuild, BuildReference buildRef) { - super(); - - this.builds = new ArrayList(); - this.builds.add(buildRef); - } - - static BuildInfoExporterAction addBuildInfoExporterAction(AbstractBuild parentBuild, String triggeredProject, int buildNumber, Result buildResult) { - BuildReference reference = new BuildReference(triggeredProject, buildNumber, buildResult); - - BuildInfoExporterAction action = parentBuild.getAction(BuildInfoExporterAction.class); - if (action == null) { - action = new BuildInfoExporterAction(parentBuild, reference); - parentBuild.getActions().add(action); - } else { - action.addBuildReference(reference); - } - return action; - } - - public void addBuildReference(BuildReference buildRef) { - this.builds.add(buildRef); - } - - public static class BuildReference { - public final String projectName; - public final int buildNumber; - public final Result buildResult; - - public BuildReference(String projectName, int buildNumber, Result buildResult) { - this.projectName = projectName; - this.buildNumber = buildNumber; - this.buildResult = buildResult; - } - } - - public String getIconFileName() { - // TODO Auto-generated method stub - return null; - } - - public String getDisplayName() { - // TODO Auto-generated method stub - return null; - } - - public String getUrlName() { - // TODO Auto-generated method stub - return null; - } - - public void buildEnvVars(AbstractBuild build, EnvVars env) { - for (String project : getProjectsWithBuilds()) { - String sanatizedBuildName = project.replaceAll("[^a-zA-Z0-9]+", "_"); - 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())); - for (BuildReference br : refs) { - if (br.buildNumber != 0) { - String tiggeredBuildRunResultKey = BUILD_RESULT_VARIABLE_PREFIX + sanatizedBuildName + RUN + Integer.toString(br.buildNumber); - env.put(tiggeredBuildRunResultKey, br.buildResult.toString()); - } - } - BuildReference lastBuild = null; - for (int i = (refs.size()); i > 0; i--) { - if (refs.get(i - 1).buildNumber != 0) { - lastBuild = refs.get(i - 1); - break; - } - } - if (lastBuild != null) { - env.put(BUILD_NUMBER_VARIABLE_PREFIX + sanatizedBuildName, Integer.toString(lastBuild.buildNumber)); - env.put(BUILD_RESULT_VARIABLE_PREFIX + sanatizedBuildName, lastBuild.buildResult.toString()); - } - } - } - - private List getBuildRefs(String project) { - List refs = new ArrayList(); - for (BuildReference br : builds) { - if (br.projectName.equals(project)) refs.add(br); - } - return refs; - } - - /** - * Gets a string for all of the build numbers - * - * @param refs List of build references to process. - * @param separator - * @return String containing all the build numbers from refs, never null but - * can be empty - */ - private String getBuildNumbersString(List refs, String separator) { - StringBuilder buf = new StringBuilder(); - boolean first = true; - - for (BuildReference s : refs) { - if (s.buildNumber != 0) { - if (first) { - first = false; - } else { - buf.append(separator); - } - buf.append(s.buildNumber); - } - } - return buf.toString(); - } - - /** - * Gets the unique set of project names that have a linked build. - * - * @return Set of project names that have at least one build linked. - */ - private Set getProjectsWithBuilds() { - Set projects = new HashSet(); - - for (BuildReference br : this.builds) { - if (br.buildNumber != 0) { - projects.add(br.projectName); - } - } - return projects; - } -} \ No newline at end of file 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..7d2575c6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/ConnectionResponse.java @@ -0,0 +1,81 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import net.sf.json.JSONObject; + +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; + +/** + * Http response containing header, body (JSON format) and response code. + * + */ +public class ConnectionResponse +{ + @NonNull + private final Map> header = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + @Nullable @CheckForNull + private final JSONObject body; + + @Nullable @CheckForNull + private final String rawBody; + + @NonNull + private final int responseCode; + + + public ConnectionResponse(@NonNull Map> header, @Nullable JSONObject body, @NonNull int responseCode) + { + loadHeader(header); + this.body = body; + this.rawBody = null; + this.responseCode = responseCode; + } + + public ConnectionResponse(@NonNull Map> header, @Nullable String rawBody, @NonNull int responseCode) + { + loadHeader(header); + this.body = null; + this.rawBody = rawBody; + this.responseCode = responseCode; + } + + public ConnectionResponse(@NonNull Map> header, @NonNull int responseCode) + { + 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; + } + + 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/JenkinsCrumb.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java new file mode 100644 index 00000000..ff773e9c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/JenkinsCrumb.java @@ -0,0 +1,63 @@ +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; + 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 + * 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) + { + 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 fa62789e..daf012d8 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -1,1292 +1,1310 @@ 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 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.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.StringTools.NL; + +import edu.umd.cs.findbugs.annotations.NonNull; 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 java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; +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; import java.net.URL; -import java.net.URLEncoder; -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.Objects; +import java.util.concurrent.Semaphore; +import java.util.logging.Level; +import java.util.logging.Logger; + +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; +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; +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; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest2; -import org.apache.commons.codec.binary.Base64; +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.Result; +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.tasks.SimpleBuildStep; /** - * * @author Maurice W. - * */ -public class RemoteBuildConfiguration extends Builder { - - private final String token; - private final String remoteJenkinsName; - private final String job; - - 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; - - // "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"; - - private final boolean overrideAuth; - private CopyOnWriteList auth = new CopyOnWriteList(); - - private final boolean loadParamsFromFile; - private String parameterFile = ""; - - private String queryString = ""; - - @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 { - - this.token = token.trim(); - this.remoteJenkinsName = remoteJenkinsName; - this.job = job.trim(); - 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())); - } - - 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; - } - - // TODO: clean this up a bit - // split the parameter-string into an array based on the new-line character - String[] params = parameters.split("\n"); - - // convert the String array into a List of Strings, and remove any empty entries - this.parameterList = new ArrayList(Arrays.asList(params)); - - } - - public RemoteBuildConfiguration(String remoteJenkinsName, boolean shouldNotFailBuild, - boolean preventRemoteBuildQueue, boolean blockBuildUntilComplete, int pollInterval, String job, - String token, String parameters, boolean enhancedLogging) throws MalformedURLException { - - this.token = token.trim(); - this.remoteJenkinsName = remoteJenkinsName; - this.parameters = parameters; - 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; - - // split the parameter-string into an array based on the new-line character - String[] params = parameters.split("\n"); - - // convert the String array into a List of Strings, and remove any empty entries - this.parameterList = new ArrayList(Arrays.asList(params)); - - } - - /** - * 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) { - - FilePath workspace = build.getWorkspace(); - BufferedReader br = null; - List ParameterList = new ArrayList(); - try { - - String filePath = workspace + this.getParameterFile(); - String sCurrentLine; - String fileContent = ""; - - br = new BufferedReader(new FileReader(filePath)); - - while ((sCurrentLine = br.readLine()) != null) { - // fileContent += sCurrentLine; - ParameterList.add(sCurrentLine); - } - - // ParameterList = new ArrayList(Arrays.asList(fileContent)); - - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - try { - if (br != null) { - br.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - // FilePath. - 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, " ")); - } - - /** - * 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 - */ - private List getCleanedParameters(List parameters) { - List params = new ArrayList(parameters); - removeEmptyElements(params); - removeCommentsFromParameters(params); - 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. - */ - 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 - * - * @return query-parameter-formated URL-encoded string - * @throws InterruptedException - * @throws IOException - * @throws MacroEvaluationException - */ - 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, "&"); - } - - /** - * Lookup up a Remote Jenkins Server based on display name - * - * @param displayName - * Name of the configuration you are looking for - * @return A RemoteSitez object - */ - public RemoteJenkinsServer findRemoteHost(String displayName) { - RemoteJenkinsServer match = null; - - for (RemoteJenkinsServer host : this.getDescriptor().remoteSites) { - // if we find a match, then stop looping - if (displayName.equals(host.getDisplayName())) { - match = host; - break; - } - } - - return match; - } - - /** - * 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; - } else { - newQueryString = currentQueryString + "&" + 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 - * 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 - */ - private String buildTriggerUrl(String job, String securityToken, Collection params, boolean isRemoteJobParameterized) { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - String triggerUrlString = remoteServer.getAddress().toString(); - - // start building the proper URL based on known capabiltiies of the remote server - if (remoteServer.getHasBuildTokenRootSupport()) { - triggerUrlString += buildTokenRootUrl; - triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized); - - this.addToQueryString("job=" + this.encodeValue(job)); - - } else { - triggerUrlString += "/job/"; - triggerUrlString += this.encodeValue(job); - 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)); - } - - // turn our Collection into a query string - String buildParams = buildUrlQueryString(params); - - if (!buildParams.isEmpty()) { - this.addToQueryString(buildParams); - } - - // by adding "delay=0", this will (theoretically) force this job to the top of the remote queue - this.addToQueryString("delay=0"); - - triggerUrlString += "?" + this.getQueryString(); - - return triggerUrlString; - } - - /** - * 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 securityToken - * Security token used to trigger remote job - * @return fully formed, fully qualified remote trigger URL - */ - private String buildGetUrl(String job, String securityToken) { - - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - String urlString = remoteServer.getAddress().toString(); - - urlString += "/job/"; - urlString += this.encodeValue(job); - - // don't try to include a security token in the URL if none is provided - if (!securityToken.equals("")) { - this.addToQueryString("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 listener - * Build Listener - * @throws IOException - */ - 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()); - } - } - - @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"; - - 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; - - 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); - } - - String jobName = replaceToken(build, listener, this.getJob()); - - String securityToken = replaceToken(build, listener, this.getToken()); - - boolean isRemoteParameterized = isRemoteJobParameterized(jobName, build, listener); - String triggerUrlString = this.buildTriggerUrl(jobName, securityToken, cleanedParams, isRemoteParameterized); - - // Trigger remote job - // print out some debugging information to the console - - //listener.getLogger().println("URL: " + triggerUrlString); - listener.getLogger().println("Triggering this remote job: " + jobName); - - // 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); - preCheckUrlString += "/lastBuild"; - preCheckUrlString += "/api/json/"; - JSONObject preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", build, listener); - - 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) - try { - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { - this.failBuild(e, listener); - } - } - listener.getLogger().println("Remote job remote job " + jobName + " is not currenlty building."); - } else { - this.failBuild(new Exception("Got a blank response from Remote Jenkins Server, cannot continue."), listener); - } - - } 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() + "]"); - } - - 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; - } - - // 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 ); - // } - - // 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"; - - buildStatusStr = getBuildStatus(jobLocation, build, listener); - - 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); - } - } - - 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("--------------------------------------------------------------------------------"); - } - - // 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); - } - } else { - listener.getLogger().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; - } - - 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 (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; - } - - String value = parameter.getString("value"); - // If we got the expected value, skip to the next parameter - if (expected.equals(value)) continue; - - // 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; - } - - public String getBuildStatus(String buildUrlString, AbstractBuild build, BuildListener listener) throws IOException { - String buildStatus = "UNKNOWN"; - - 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("result") == null && responseObject.getBoolean("building") == false) { - // build not started - buildStatus = "not started"; - } else if (responseObject.getBoolean("building")) { - // build running - buildStatus = "running"; - } else if (responseObject.getString("result") != null) { - // build finished - buildStatus = responseObject.getString("result"); - } else { - // Add additional else to check for unhandled conditions - listener.getLogger().println("WARNING: Unhandled condition!"); - } - - 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) - throws IOException { - - return getConsoleOutput( urlString, requestType, build, listener, 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 - * @throws IOException - */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener) - throws IOException { - - return sendHTTPCall( urlString, requestType, build, listener, 1 ); - } - - public String getConsoleOutput(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) - throws IOException { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - 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); - } - - byte[] encodedAuthKey = Base64.encodeBase64(usernameTokenConcat.getBytes()); - connection.setRequestProperty("Authorization", "Basic " + new String(encodedAuthKey)); - } - - try { - connection.setDoInput(true); - connection.setRequestProperty("Accept", "application/json"); - 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"); - } - rd.close(); - - - consoleOutput = response.toString(); - } 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."); - 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); - } - - - listener.getLogger().println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); - numberOfAttempts++; - consoleOutput = getConsoleOutput(urlString, requestType, build, listener, 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); - } 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); - } - - } 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; - } - - /** - * 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 - * - * @see sendHTTPCall - * @param numberOfAttempts number of time that the connection has been attempted - * @return - * @throws IOException - */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) - throws IOException { - RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); - 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; - - 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)); - } - - try { - connection.setDoInput(true); - connection.setRequestProperty("Accept", "application/json"); - 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; - } else { - responseObject = (JSONObject) JSONSerializer.toJSON(response.toString()); - } - - } catch (IOException e) { - listener.getLogger().println(e.getMessage()); - //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."); - 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); - } - - - listener.getLogger().println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); - numberOfAttempts++; - responseObject = sendHTTPCall(urlString, requestType, build, listener, 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); - }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); - } - - } 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 responseObject; - } - - /** - * Helper function for character encoding - * - * @param dirtyValue - * @return encoded value - */ - private 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; - } - - // Getters - public String getRemoteJenkinsName() { - return this.remoteJenkinsName; - } - - public String getJob() { - return this.job; - } - - public boolean getShouldNotFailBuild() { - return this.shouldNotFailBuild; - } - - public boolean getEnhancedLogging() { - return this.enhancedLogging; - } - - public boolean getPreventRemoteBuildQueue() { - return this.preventRemoteBuildQueue; - } - - public boolean getBlockBuildUntilComplete() { - return this.blockBuildUntilComplete; - } - - public int getPollInterval() { - return this.pollInterval; - } - - /** - * @return the connectionRetryLimit - */ - public int getConnectionRetryLimit() { - return connectionRetryLimit; - } - - public String getToken() { - return this.token; - } - - public boolean getLoadParamsFromFile() { - return this.loadParamsFromFile; - } - - public String getParameterFile() { - return this.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; - } - } - - /** - * 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 RemoteBuildConfiguration.paramerizedBuildUrl; - } else { - return RemoteBuildConfiguration.normalBuildUrl; - } - } - - /** - * 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 - */ - private boolean isRemoteJobParameterized(String jobName, AbstractBuild build, BuildListener listener) { - 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; - } - - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - 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(); - } - - /** - * Convenience function for setting the query string to empty - */ - private void clearQueryString() { - this.setQueryString(""); - } - - // 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. -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(); - } - - /** - * 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); - } - - public ListBoxModel doFillRemoteJenkinsNameItems() { - ListBoxModel model = new ListBoxModel(); - - 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 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 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; + + /** + * 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; + + /** + * We need to keep this for compatibility - old config deserialization! + * + * @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; + private boolean preventRemoteBuildQueue; + private int httpPostReadTimeout; + private int httpGetReadTimeout; + private int pollInterval; + 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; + /** + * 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; + private boolean useJobInfoCache; + private boolean abortTriggeredJob; + private boolean disabled; + + private transient Map hostLocks = new HashMap<>(); + private Map hostPermits = new HashMap<>(); + + private static Logger logger = Logger.getLogger(RemoteBuildConfiguration.class.getName()); + + @DataBoundConstructor + public RemoteBuildConfiguration() { + httpGetReadTimeout = DEFAULT_HTTP_GET_READ_TIMEOUT; + httpPostReadTimeout = DEFAULT_HTTP_POST_READ_TIMEOUT; + pollInterval = DEFAULT_POLLINTERVALL; + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + } + + /* + * 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; + + // migrate parameters/parameterFile to JobParameters + if (parameters2 == null) { + parameters2 = JobParameters.migrateOldParameters(parameters, parameterFile); + } + parameters = null; + parameterFile = null; + + if (hostLocks == null) { + hostLocks = new HashMap<>(); + } + if (hostPermits == null) { + hostPermits = new HashMap<>(); + } + 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; + } + + @DataBoundSetter + public void setMaxConn(int maxConn) { + this.maxConn = (maxConn > 5) ? 5 : Math.max(maxConn, 1); + } + + @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 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; + } + + @DataBoundSetter + 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) + 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 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 Map getParameterMap(BuildContext context) throws AbortException { + return getParameters2() + .getParametersMap(context); + } + + /** + * 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)); + } + + 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(); + if (s == null || lastPermit == null || maxConn != lastPermit) { + s = new Semaphore(maxConn); + hostLocks.put(url.getHost(), s); + hostPermits.put(url.getHost(), maxConn); + } + } + + if (this.overrideTrustAllCertificates) { + server.setTrustAllCertificates(this.trustAllCertificates); + } + + return server; + } + + public 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 + * + * @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; + 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); + } 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()); + } + } + + 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) + throws InterruptedException, IOException, IllegalArgumentException { + FilePath workspace = build.getWorkspace(); + if (workspace == null) + throw new IllegalArgumentException("The workspace can not be null"); + 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. + * + * @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 { + Handle handle = null; + BuildContext context = null; + RemoteJenkinsServer effectiveRemoteServer = null; + 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); + performWaitForBuild(context, handle); + } catch (InterruptedException e) { + this.abortRemoteTask(effectiveRemoteServer, handle, context); + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 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(@NonNull BuildContext context) throws IOException, InterruptedException { + Map parameters = getParameterMap(context); + String jobNameOrUrl = this.getJob(); + String securityToken = this.getToken(); + try { + 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, parameters); + + final JSONObject remoteJobMetadata = getRemoteJobMetadata(jobNameOrUrl, context); + boolean isRemoteParameterized = isRemoteJobParameterized(remoteJobMetadata); + + String triggerUrlString = HttpHelper.buildTriggerUrl(jobNameOrUrl, securityToken, + 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.tryPost(triggerUrlString, context, parameters, + 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); + } 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.getPollInterval(buildInfo.getStatus()) + " seconds until next poll."); + Thread.sleep(this.getPollInterval(buildInfo.getStatus()) * 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 ..."); + } + + 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()) { + if (this.getEnhancedLogging()) { + consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); + } else { + 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); + } + if (this.getEnhancedLogging()) { + consoleOffset = printOffsetConsoleOutput(context, consoleOffset, buildInfo); + 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 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, RemoteBuildStatus.QUEUED); + 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()) { + URL remoteBuildURL = queueItem.getBuildURL(); + String effectiveRemoteServerAddress = context.effectiveRemoteServer.getAddress(); + URL effectiveRemoteBuildURL = generateEffectiveRemoteBuildURL(remoteBuildURL, + effectiveRemoteServerAddress); + buildInfo.setBuildData(queueItem.getBuildNumber(), effectiveRemoteBuildURL); + } + return buildInfo; + } + + // Only avoid url cache while loop inquiry + String buildUrlString = String.format("%sapi/json/?tree=result,building&seed=%d", buildInfo.getBuildURL(), + System.currentTimeMillis()); + JSONObject responseObject = doGet(buildUrlString, context, buildInfo.getStatus()).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 ignored) { + } + 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 == null || offset.equals("-1")) { + return "-1"; + } + String buildUrlString = String.format("%slogText/progressiveText?start=%s", buildInfo.getBuildURL(), offset); + ConnectionResponse response = doGet(buildUrlString, context, buildInfo.getStatus()); + + 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"; + } + } + + /** + * 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 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 { + return HttpHelper.tryGet(urlString, context, this.getHttpGetReadTimeout(), + this.getPollInterval(remoteBuildStatus), this.getConnectionRetryLimit(), this.getAuth2(), + getLock(urlString)); + } + + 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(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(context.run.getParent()); + context.logger.printf(" Using globally defined " + authString + "%n"); + } else { + context.logger.println(" No credentials configured"); + } + } + + private void logConfiguration(@NonNull BuildContext context, Map effectiveParams) throws IOException { + String _job = getJob(); + String _jobExpanded = getJobExpanded(context); + 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(); + context.logger.println( + "################################################################################################################"); + context.logger.println(" Parameterized Remote Trigger Configuration:"); + context.logger.printf(" - job: %s %s%n", _job, _jobExpandedLogEntry); + if (!isEmpty(_remoteJenkinsName)) { + context.logger.printf(" - remoteJenkinsName: %s%n", _remoteJenkinsName); + } + if (!isEmpty(_remoteJenkinsUrl)) { + context.logger.printf(" - remoteJenkinsUrl: %s%n", _remoteJenkinsUrl); + } + if (_auth != null && !(_auth instanceof NullAuth)) { + final String authString = context.run == null + ? _auth.getDescriptor().getDisplayName() + : _auth.toString(context.run.getParent()); + context.logger.printf(" - auth: %s%n", authString); + } + 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( + "################################################################################################################"); + } + + public boolean isAbortTriggeredJob() { + return abortTriggeredJob; + } + + public int getMaxConn() { + return maxConn; + } + + /** + * @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); + } + + public int getHttpGetReadTimeout() { + return httpGetReadTimeout; + } + + public int getHttpPostReadTimeout() { + return httpPostReadTimeout; + } + + /** + * @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(RemoteBuildStatus remoteBuildStatus) { + switch (remoteBuildStatus) { + case NOT_TRIGGERED: + case QUEUED: + return QUEUED_ITEMS_POLLINTERVALL; + default: + 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 boolean getEnhancedLogging() { + return enhancedLogging; + } + + public JobParameters getParameters2() { + return (parameters2 != null) ? parameters2 : DEFAULT_PARAMETERS; + } + + public int getConnectionRetryLimit() { + return connectionRetryLimit; // For now, this is a constant + } + + public boolean isDisabled() { + return disabled; + } + + private @NonNull JSONObject getRemoteJobMetadata(String jobNameOrUrl, @NonNull BuildContext context) + throws IOException, InterruptedException { + + String remoteJobUrl = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); + 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) { + return jsonObject; + } + + ConnectionResponse response = doGet(remoteJobUrl, context, RemoteBuildStatus.FINISHED); + if (response.getResponseCode() < 400 && response.getBody() != null) { + return DropCachePeriodicWork.safePutJobInfo(remoteJobUrl, response.getBody(), isUseJobInfoCache()); + + } 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(final 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; + } + } + } + + 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."); + } + 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(); + } + + public boolean isUseCrumbCache() { + return useCrumbCache; + } + + 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 + 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(); } + */ + + @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. + */ + @NonNull + @Override + public String getDisplayName() { + return "Trigger a remote parameterized job"; + } + + @Override + public boolean configure(StaplerRequest2 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 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, + @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(""); + Arrays.stream(getRemoteSites()) + .filter(Objects::nonNull) + .map(RemoteJenkinsServer::getDisplayName) + .forEach(model::add); + 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 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/RemoteJenkinsServer.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java index 22475360..9c3a829c 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer.java @@ -1,81 +1,158 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; +import static org.apache.commons.lang.StringUtils.trimToEmpty; + +import java.io.Serializable; +import javax.net.ssl.*; import java.net.URL; -import java.util.ArrayList; +import java.net.URLConnection; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.List; -import net.sf.json.JSONObject; +import edu.umd.cs.findbugs.annotations.CheckForNull; +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; +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. - * + * * @author Maurice W. - * + * */ -public class RemoteJenkinsServer extends AbstractDescribableImpl { +public class RemoteJenkinsServer extends AbstractDescribableImpl implements Cloneable, Serializable { + + private static final long serialVersionUID = -9211781849078964416L; + + /** + * Default for this class is No Authentication + */ + 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. + */ + @CheckForNull + private transient List auth; + + @CheckForNull + private String displayName; + private boolean hasBuildTokenRootSupport; + private boolean trustAllCertificates; + private boolean overrideTrustAllCertificates; + + @CheckForNull + private Auth2 auth2; + @CheckForNull + private String address; + private boolean useProxy; - private final URL address; - private final String displayName; - private final boolean hasBuildTokenRootSupport; - private final String username; - private final String apiToken; + @DataBoundConstructor + public RemoteJenkinsServer() { + } - private CopyOnWriteList auth = new CopyOnWriteList(); + /* + * 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; + } - @DataBoundConstructor - public RemoteJenkinsServer(String address, String displayName, boolean hasBuildTokenRootSupport, JSONObject auth) - throws MalformedURLException { + @DataBoundSetter + public void setTrustAllCertificates(boolean trustAllCertificates) { + this.trustAllCertificates = trustAllCertificates; + } - this.address = new URL(address); - this.displayName = displayName.trim(); - this.hasBuildTokenRootSupport = hasBuildTokenRootSupport; + @DataBoundSetter + public void setOverrideTrustAllCertificates(boolean overrideTrustAllCertificates) { + this.overrideTrustAllCertificates = overrideTrustAllCertificates; + } - // 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 = ""; + @DataBoundSetter + public void setDisplayName(String displayName) + { + this.displayName = trimToEmpty(displayName); + } - // this.auth = new Auth(auth); - this.auth.replaceBy(new Auth(auth)); + @DataBoundSetter + public void setHasBuildTokenRootSupport(boolean hasBuildTokenRootSupport) + { + this.hasBuildTokenRootSupport = hasBuildTokenRootSupport; + } + @DataBoundSetter + public void setUseProxy(boolean useProxy) { + this.useProxy = useProxy; } - // Getters + @DataBoundSetter + public void setAuth2(Auth2 auth2) + { + this.auth2 = (auth2 != null) ? auth2 : DEFAULT_AUTH; + } - public Auth[] getAuth() { - return auth.toArray(new Auth[this.auth.size()]); + @DataBoundSetter + public void setAddress(String address) + { + this.address = address; } + // Getters + + @CheckForNull public String getDisplayName() { String displayName = null; if (this.displayName == null || this.displayName.trim().equals("")) { - displayName = this.getAddress().toString(); + if (address != null) displayName = address; + else displayName = null; } else { displayName = this.displayName; } return displayName; } - public URL getAddress() { - return address; + public boolean getHasBuildTokenRootSupport() { + return hasBuildTokenRootSupport; } - public boolean getHasBuildTokenRootSupport() { - return this.hasBuildTokenRootSupport; + public boolean isUseProxy() { + return useProxy; + } + + @CheckForNull + public Auth2 getAuth2() { + return (auth2 != null) ? auth2 : NoneAuth.INSTANCE; + } + + @CheckForNull + public String getAddress() { + return address; } @Override @@ -83,73 +160,146 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } - @Extension - public static class DescriptorImpl extends Descriptor { + public boolean getTrustAllCertificates() { return trustAllCertificates; } - private JSONObject authenticationMode; + public boolean getOverrideTrustAllCertificates() { return overrideTrustAllCertificates; } - /** - * In order to load the persisted global configuration, you have to call load() in the constructor. - */ - /* - * public DescriptorImpl() { load(); } - */ + + @Extension + public static class DescriptorImpl extends Descriptor { 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"); + /** + * 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()); + 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. - * + * * @param address * Remote address to be validated * @return FormValidation object */ - public FormValidation doValidateAddress(@QueryParameter String address) { + @Restricted(NoExternalUse.class) + public FormValidation doCheckAddress(@QueryParameter String address, @QueryParameter boolean trustAllCertificates) { 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."); + } + + 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); - URI uri = host.toURI(); + 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 try { - HttpURLConnection connection = (HttpURLConnection) host.openConnection(); - connection.setConnectTimeout(5000); - connection.connect(); + 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(); + + if (trustAllCertificates) { + return FormValidation.warning( + "Connection established! Accepting all certificates is potentially unsafe." + ); + } } catch (Exception e) { - return FormValidation.warning("Address looks good, but we were not able to connect to it"); + return FormValidation.warning(e, "Address looks good, but a connection could not be established."); } - return FormValidation.okWithMarkup("Address looks good"); + return FormValidation.ok(host + " is reachable."); + } + + public static List getAuth2Descriptors() { + return Auth2.all(); + } + + public static Auth2Descriptor getDefaultAuth2Descriptor() { + return NoneAuth.DESCRIPTOR; } } + @Override + public RemoteJenkinsServer clone() throws CloneNotSupportedException { + RemoteJenkinsServer clone = (RemoteJenkinsServer)super.clone(); + clone.auth2 = (auth2 == null) ? null : auth2.clone(); + 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/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..46d60c28 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2.java @@ -0,0 +1,72 @@ +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 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, Cloneable { + + 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 + { + } + + /** + * 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; + + /** + * Whether a Jenkins crumb is required on {@code POST} requests. + */ + public boolean requiresCrumb() { + return true; + } + + 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); + + + @Override + public Auth2 clone() throws CloneNotSupportedException { + return (Auth2)super.clone(); + }; + +} 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..6e1060ee --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/BearerTokenAuth.java @@ -0,0 +1,93 @@ +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 org.kohsuke.stapler.DataBoundSetter; + +import hudson.Extension; +import hudson.model.Item; +import hudson.util.Secret; + + +public class BearerTokenAuth extends Auth2 { + + private static final long serialVersionUID = 3614172320192170597L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new BearerTokenAuthDescriptor(); + + private Secret token; + + @DataBoundConstructor + public BearerTokenAuth() { + this.token = null; + } + + @DataBoundSetter + public void setToken(Secret token) { + this.token = token; + } + + public Secret getToken() { + return this.token; + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + connection.setRequestProperty("Authorization", "Bearer " + getToken().getPlainText()); + } + + @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 true; + } else { + return false; + } + } + return token.equals(other.token); + } +} 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..18645a82 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth.java @@ -0,0 +1,189 @@ +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, false); + 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.getCurrentRequest2().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; + } + } + + @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 new file mode 100644 index 00000000..81c6671f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NoneAuth.java @@ -0,0 +1,67 @@ +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(); + + public static final NoneAuth INSTANCE = new NoneAuth(); + + + @DataBoundConstructor + 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); + } + + @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"; + } + } + + @Override + public int hashCode() { + return "NoneAuth".hashCode(); + } + + @Override + public boolean equals(Object obj) { + 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 new file mode 100644 index 00000000..f6525ffe --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/NullAuth.java @@ -0,0 +1,64 @@ +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(); + + public static final NullAuth INSTANCE = new NullAuth(); + + @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"; + } + } + + @Override + public int hashCode() { + return "NullAuth".hashCode(); + } + + @Override + public boolean equals(Object obj) { + return this.getClass().isInstance(obj); + } + +} \ No newline at end of file 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..3f53420c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth.java @@ -0,0 +1,119 @@ +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; +import hudson.util.Secret; + +public class TokenAuth extends Auth2 { + + private static final long serialVersionUID = 7912089565969112023L; + + @Extension + public static final Auth2Descriptor DESCRIPTOR = new TokenAuthDescriptor(); + + private String userName; + private Secret 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(Secret apiToken) { + this.apiToken = apiToken; + } + + public Secret getApiToken() { + return this.apiToken; + } + + @Override + public void setAuthorizationHeader(URLConnection connection, BuildContext context) throws IOException { + String authHeaderValue = Base64Utils.generateAuthorizationHeaderValue(AUTHTYPE_BASIC, getUserName(), getApiToken().getPlainText(), context, true); + connection.setRequestProperty("Authorization", authHeaderValue); + } + + @Override + public boolean requiresCrumb() { + return false; + } + + @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"; + } + } + + @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/exceptions/CredentialsNotFoundException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/CredentialsNotFoundException.java new file mode 100644 index 00000000..9b61e7c7 --- /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/ExceedRetryLimitException.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/exceptions/ExceedRetryLimitException.java new file mode 100644 index 00000000..ca4734f5 --- /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 exceeded."; + } + +} 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..4960a034 --- /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/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; + } + +} 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..c414d460 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/FileParameters.java @@ -0,0 +1,119 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; + +import java.io.BufferedReader; + +import edu.umd.cs.findbugs.annotations.NonNull; +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(ordinal = 0) + 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..92d6f1e4 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/JobParameters.java @@ -0,0 +1,83 @@ +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; +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 (!isNullOrEmpty(parameterFile)) { + return new FileParameters(parameterFile); + } + + if (!isNullOrEmpty(parameters)) { + 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..0fdf9dd4 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameter.java @@ -0,0 +1,87 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import edu.umd.cs.findbugs.annotations.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..67c7e430 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/MapParameters.java @@ -0,0 +1,100 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import static java.util.stream.Collectors.toMap; + +import edu.umd.cs.findbugs.annotations.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(ordinal = 2) + 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..0e4f850d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/parameters2/StringParameters.java @@ -0,0 +1,82 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.parameters2; + +import edu.umd.cs.findbugs.annotations.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(ordinal = 1) + 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/Handle.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java new file mode 100644 index 00000000..d017dcba --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/Handle.java @@ -0,0 +1,380 @@ +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.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.Optional; + +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; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteJenkinsServer; +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; + +/** + * 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. + */ +public class Handle implements Serializable { + + private static final long serialVersionUID = 4418782245518194292L; + + @NonNull + private final RemoteBuildConfiguration remoteBuildConfiguration; + + @NonNull + private RemoteBuildInfo buildInfo; + + @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; + + /* + * 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 + */ + @NonNull + private String lastLog; + + + public Handle(@NonNull RemoteBuildConfiguration remoteBuildConfiguration, @NonNull RemoteBuildInfo buildInfo, @NonNull String currentItem, + @NonNull RemoteJenkinsServer effectiveRemoteServer, @NonNull JSONObject remoteJobMetadata) + { + this.remoteBuildConfiguration = remoteBuildConfiguration; + this.buildInfo = buildInfo; + 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"); + } + + /** + * 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 buildInfo.isQueued(); + } + + /** + * 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 { + return buildInfo.isFinished(); + } + + /** + * @return the name or URL of the remote job as configured in the job/pipeline. + */ + 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; + } + + /** + * @return the id of the remote job on the queue. + */ + @CheckForNull + public String getQueueId() { + return buildInfo.getQueueId(); + } + + /** + * Get the build URL of the remote build. + * + * @return the URL, or null if it could not be identified (yet). + */ + @CheckForNull + @Whitelisted + public URL getBuildUrl() { + return buildInfo.getBuildURL() == null ? null : buildInfo.getBuildURL(); + } + + /** + * Get the build number of the remote build. + * + * @return the build number, or 0 if it could not be identified (yet). + */ + @NonNull + @Whitelisted + public int getBuildNumber() { + return buildInfo.getBuildNumber(); + } + + /** + * 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 {@link org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob.RemoteBuildStatus} the build status + */ + @NonNull + @Whitelisted + public RemoteBuildStatus getBuildStatus() { + return buildInfo.getStatus(); + } + + /** + * Updates the current build status of the remote job. + * + * @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, + * 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 RemoteBuildStatus updateBuildStatus() throws IOException, InterruptedException { + return updateBuildStatus(false); + } + + /** + * 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 + * 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 RemoteBuildStatus updateBuildStatusBlocking() throws IOException, InterruptedException { + return updateBuildStatus(true); + } + + @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(); + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + while(!buildInfo.isFinished()) { + BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); + buildInfo = remoteBuildConfiguration.updateBuildInfo(buildInfo, context); + if(!blockUntilFinished) break; + } + return buildInfo.getStatus(); + } finally { + lastLog = log.getContent(); + } + } + + public void setBuildInfo(RemoteBuildInfo buildInfo) + { + this.buildInfo = buildInfo; + } + + /** + * 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(); + } + + /** + * 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. + */ + @NonNull + @Whitelisted + public String lastLog() { + String log = lastLog.trim(); + lastLog = ""; + return log; + } + + @Whitelisted + @Override + 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())); + if(buildInfo != null) sb.append(String.format(", buildNumber=%s, buildUrl=%s", buildInfo.getBuildNumber(), buildInfo.getBuildURL())); + 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(); + } + + /** + * 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(); + URL fileUrl = new URL(remoteBuildUrl, "artifact/" + filename); + + PrintStreamWrapper log = new PrintStreamWrapper(); + try { + BuildContext context = new BuildContext(log.getPrintStream(), effectiveRemoteServer, this.currentItem); + return remoteBuildConfiguration.doGet(fileUrl.toString(), context, getBuildStatus()).getBody(); + } finally { + lastLog = log.getContent(); + } + } + + @CheckForNull + private String getParameterFromJobMetadata(JSONObject remoteJobMetadata, String string) + { + try { + return Optional.ofNullable(remoteJobMetadata) + .map(meta->meta.getString("name")) + .map(StringUtils::trimToNull) + .orElse(null); + } + 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..c640e3e2 --- /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..2faf660f --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep.java @@ -0,0 +1,496 @@ +/* + * 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 static java.util.stream.Collectors.toMap; + +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 edu.umd.cs.findbugs.annotations.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; +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.ParameterizedRemoteTrigger.utils.OtelUtils; +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; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import hudson.AbortException; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.FilePath; +import hudson.model.Descriptor; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; + +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(); + 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 setTrustAllCertificates(boolean trustAllCertificates) { + remoteBuildConfig.setTrustAllCertificates(trustAllCertificates); + } + + @DataBoundSetter + public void setOverrideTrustAllCertificates(boolean overrideTrustAllCertificates) { + remoteBuildConfig.setOverrideTrustAllCertificates(overrideTrustAllCertificates); + } + + @DataBoundSetter + 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); + } + + @DataBoundSetter + public void setBlockBuildUntilComplete(boolean blockBuildUntilComplete) { + remoteBuildConfig.setBlockBuildUntilComplete(blockBuildUntilComplete); + } + + @DataBoundSetter + public void setToken(String token) { + remoteBuildConfig.setToken(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) { + 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 { + 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. + * @param parameterFile + * The parameter file. + */ + @Deprecated + @DataBoundSetter + public void setParameterFile(String parameterFile) { + remoteBuildConfig.setParameters2(new FileParameters(parameterFile)); + } + + @DataBoundSetter + public void setEnhancedLogging(boolean enhancedLogging) { + remoteBuildConfig.setEnhancedLogging(enhancedLogging); + } + + @DataBoundSetter + public void setUseJobInfoCache(boolean useJobInfoCache) { + remoteBuildConfig.setUseJobInfoCache(useJobInfoCache); + } + + @DataBoundSetter + 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 { + 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, 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 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 { + + 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 (AutoCloseable ignored = OtelUtils.isOpenTelemetryAvailable() ? OtelUtils.activeSpanIfAvailable(stepContext) : OtelUtils.noop()) { + if (!remoteBuildConfig.isStepDisabled(listener.getLogger())) { + 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 getTrustAllCertificates() { + return remoteBuildConfig.getTrustAllCertificates(); + } + + public boolean getOverrideTrustAllCertificates() { + return remoteBuildConfig.getOverrideTrustAllCertificates(); + } + + 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); + } + + public boolean getBlockBuildUntilComplete() { + return remoteBuildConfig.getBlockBuildUntilComplete(); + } + + public String getToken() { + return remoteBuildConfig.getToken(); + } + + public JobParameters getParameters() { + return remoteBuildConfig.getParameters2(); + } + + public boolean getEnhancedLogging() { + return remoteBuildConfig.getEnhancedLogging(); + } + + 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 boolean isDisabled() { + return remoteBuildConfig.isDisabled(); + } + + /** + * @deprecated Still there to allow old configuration (3.1.5 and below) to work. + * 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; + } +} 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..9887783a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItem.java @@ -0,0 +1,49 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.util.List; +import java.util.Map; + +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. + * + */ +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)); + } + } + + @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 new file mode 100644 index 00000000..69535bb3 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemData.java @@ -0,0 +1,143 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.net.MalformedURLException; +import java.net.URL; + +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; + +/** + * Contains information about the remote job while is waiting on the queue. + * + */ +public class QueueItemData +{ + @NonNull + private QueueItemStatus status; + + @Nullable + private String why; + + @NonNull + private int buildNumber; + + @Nullable + private URL buildURL; + + + public QueueItemData() throws MalformedURLException + { + this.status = QueueItemStatus.WAITING; + } + + public boolean isWaiting() + { + return status == QueueItemStatus.WAITING; + } + + public boolean isBlocked() + { + return status == QueueItemStatus.BLOCKED; + } + + public boolean isBuildable() + { + return status == QueueItemStatus.BUILDABLE; + } + + public boolean isPending() + { + return status == QueueItemStatus.PENDING; + } + + public boolean isLeft() + { + return status == QueueItemStatus.LEFT; + } + + public boolean isExecuted() + { + return status == QueueItemStatus.EXECUTED; + } + + public boolean isCancelled() + { + return status == QueueItemStatus.CANCELLED; + } + + @NonNull + public QueueItemStatus getStatus() { + return status; + } + + @CheckForNull + public String getWhy() { + return why; + } + + @NonNull + public int getBuildNumber() + { + return buildNumber; + } + + @CheckForNull + public URL getBuildURL() + { + return buildURL; + } + + /** + * 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. + */ + 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; + 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.isNullObject())) { + try { + buildNumber = remoteJobInfo.getInt("number"); + } catch (JSONException e) { + 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(String.format("[WARNING] The attribute \"url\" was not found. Unexpected response: %s", queueResponse.toString())); + } + } + } catch (JSONException e) { + context.logger.println(String.format("[WARNING] The attribute \"executable\" was not found. Unexpected response: %s", queueResponse.toString())); + } + if (buildNumber != 0 && buildURL != null) status = QueueItemStatus.EXECUTED; + } + } + + private boolean getOptionalBoolean(@NonNull JSONObject queueResponse, @NonNull String attribute) + { + if (queueResponse.containsKey(attribute)) + return queueResponse.getBoolean(attribute); + else return false; + } +} 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..6f43a27a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/QueueItemStatus.java @@ -0,0 +1,97 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +/** + * 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 +{ + /** + * 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 new file mode 100644 index 00000000..4e1dea52 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfo.java @@ -0,0 +1,151 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +import java.io.Serializable; +import java.net.URL; + +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; + +/** + * 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 +{ + private static final long serialVersionUID = -5177308623227407314L; + + @CheckForNull + private String queueId; + + @NonNull + private int buildNumber; + + @CheckForNull + private URL buildURL; + + @NonNull + private RemoteBuildStatus status; + + @NonNull + private Result result; + + + public RemoteBuildInfo() + { + status = RemoteBuildStatus.NOT_TRIGGERED; + result = Result.NOT_BUILT; + } + + @CheckForNull + public String getQueueId() { + return queueId; + } + + @NonNull + public int getBuildNumber() + { + return buildNumber; + } + + @CheckForNull + public URL getBuildURL() + { + return buildURL; + } + + @NonNull + public RemoteBuildStatus getStatus() + { + return status; + } + + @NonNull + public Result getResult() + { + return result; + } + + public void setQueueId(String queueId) { + this.queueId = queueId; + this.status = RemoteBuildStatus.QUEUED; + } + + 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.status = RemoteBuildStatus.RUNNING; + } + + public void setBuildStatus(RemoteBuildStatus 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 void setBuildResult(Result result) + { + this.status = RemoteBuildStatus.FINISHED; + this.result = result; + } + + public void setBuildResult(String result) + { + this.status = RemoteBuildStatus.FINISHED; + this.result = Result.fromString(result); + } + + @NonNull + @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 isNotTriggered() { + return status == RemoteBuildStatus.NOT_TRIGGERED; + } + + public boolean isQueued() { + return status == RemoteBuildStatus.QUEUED; + } + + 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/RemoteBuildInfoExporterAction.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java new file mode 100644 index 00000000..5858afd8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildInfoExporterAction.java @@ -0,0 +1,205 @@ +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 hudson.EnvVars; +import hudson.model.AbstractBuild; +import hudson.model.EnvironmentContributingAction; +import hudson.model.Run; + +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"; + 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_"; + public static final String BUILD_RUN_COUNT_PREFIX = "TRIGGERED_BUILD_RUN_COUNT_"; + public static final String RUN = "_RUN_"; + + private List builds; + + public RemoteBuildInfoExporterAction(Run parentBuild, BuildReference buildRef) { + super(); + + this.builds = new ArrayList(); + addBuildReferenceSafe(buildRef); + } + + 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; + synchronized(parentBuild) { + action = parentBuild.getAction(RemoteBuildInfoExporterAction.class); + if (action == null) { + action = new RemoteBuildInfoExporterAction(parentBuild, reference); + 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) + { + synchronized (builds) { + 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) { + addBuildReferenceSafe(buildRef); + } + + public static class BuildReference { + public final String projectName; + public final int buildNumber; + public final RemoteBuildInfo buildInfo; + public final URL jobURL; + + public BuildReference(String projectName, int buildNumber, URL jobURL, RemoteBuildInfo buildInfo) { + this.projectName = projectName; + this.buildNumber = buildNumber; + this.buildInfo = buildInfo; + this.jobURL = jobURL; + } + } + + public String getIconFileName() { + // TODO Auto-generated method stub + return null; + } + + public String getDisplayName() { + // TODO Auto-generated method stub + return null; + } + + public String getUrlName() { + // TODO Auto-generated method stub + return null; + } + + public void buildEnvVars(AbstractBuild build, EnvVars env) { + for (String project : getProjectsWithBuilds()) { + String sanatizedProjectName = sanitizeProjectName(project); + List refs = getBuildRefs(project); + + 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 + sanatizedProjectName + RUN + Integer.toString(br.buildNumber); + env.put(tiggeredBuildRunResultKey, br.buildInfo.getResult().toString()); + } + } + BuildReference lastBuild = null; + for (int i = (refs.size()); i > 0; i--) { + if (refs.get(i - 1).buildNumber != 0) { + lastBuild = refs.get(i - 1); + break; + } + } + if (lastBuild != null) { + 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.buildInfo.getResult().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(); + synchronized (builds) { + for (BuildReference br : builds) { + if (br.projectName.equals(project)) refs.add(br); + } + } + return refs; + } + + /** + * Gets a string for all of the build numbers + * + * @param refs List of build references to process. + * @param separator + * @return String containing all the build numbers from refs, never null but + * can be empty + */ + private String getBuildNumbersString(List refs, String separator) { + StringBuilder buf = new StringBuilder(); + boolean first = true; + + for (BuildReference s : refs) { + if (s.buildNumber != 0) { + if (first) { + first = false; + } else { + buf.append(separator); + } + buf.append(s.buildNumber); + } + } + return buf.toString(); + } + + /** + * 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. + */ + protected Set getProjectsWithBuilds() { + Set projects = new LinkedHashSet(); + 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; + } +} \ No newline at end of file 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..3b6a9c05 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/RemoteBuildStatus.java @@ -0,0 +1,61 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +/** + * 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 +{ + /** + * The remote job was not triggered and it did not enter the queue. + */ + NOT_TRIGGERED("NOT_TRIGGERED"), + + /** + * The remote job was triggered and it did enter the queue. + */ + QUEUED("QUEUED"), + + /** + * The remote job left the queue and it is running currently. + */ + RUNNING("RUNNING"), + + /** + * The remote build is finished. + */ + FINISHED("FINISHED"); + + + private final String id; + + + private RemoteBuildStatus(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + +} 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..ec53f2f1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/Base64Utils.java @@ -0,0 +1,66 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import static org.apache.commons.lang.StringUtils.isEmpty; + +import java.io.IOException; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.UnsupportedEncodingException; + +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. + * @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 + * if there is a failure while encoding user:password. + */ + @NonNull + public static String generateAuthorizationHeaderValue(String authType, String user, String password, + 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; + if (applyMacro) { + 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/DropCachePeriodicWork.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java new file mode 100644 index 00000000..a976ce9c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/DropCachePeriodicWork.java @@ -0,0 +1,111 @@ +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, boolean isCacheEnable) { + if (!isCacheEnable) + return jenkinsCrumb; + try { + crumbLock.lock(); + crumbMap.put(key, jenkinsCrumb); + return jenkinsCrumb; + } finally { + crumbLock.unlock(); + } + } + + public static JenkinsCrumb safeGetCrumb(String key, boolean isCacheEnable) { + if (!isCacheEnable) + return null; + 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, boolean isCacheEnable) { + if (!isCacheEnable) + return jobInfo; + try { + jobInfoLock.lock(); + jobInfoMap.put(key, jobInfo); + return jobInfo; + } finally { + jobInfoLock.unlock(); + } + } + + public static JSONObject safeGetJobInfo(String key, boolean isCacheEnable) { + if (!isCacheEnable) + return null; + 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/FormValidationUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java new file mode 100644 index 00000000..05505438 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/FormValidationUtils.java @@ -0,0 +1,122 @@ +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 org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import hudson.util.FormValidation; + +@Restricted(NoExternalUse.class) +public class FormValidationUtils +{ + + public static enum AffectedField { + JOB_NAME_OR_URL, REMOTE_JENKINS_URL, REMOTE_JENKINS_NAME + } + + 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')"; + + // TODO warn about URLs ending with slash + if(isEmpty(jobNameOrUrl)) { + return new RemoteURLCombinationsResult( + FormValidation.error("'Remote Job Name or URL' ('job') not specified"), + 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.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.JOB_NAME_OR_URL); + } else { + return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), + 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.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.JOB_NAME_OR_URL); + } else { + return RemoteURLCombinationsResult.OK(); + } + } else { + return new RemoteURLCombinationsResult(FormValidation.error(TEXT_ERROR_NO_URL_AT_ALL), + AffectedField.JOB_NAME_OR_URL, AffectedField.REMOTE_JENKINS_NAME, AffectedField.REMOTE_JENKINS_URL); + } + } + + /** + * 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/HttpHelper.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java new file mode 100644 index 00000000..4219b666 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/HttpHelper.java @@ -0,0 +1,623 @@ +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.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 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; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +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; +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; +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; + +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<String> in an encoded query-string. + * + * @param parameters + * the parameters needed to trigger the remote job. + * @return query-parameter-formated URL-encoded string. + */ + 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("&")); + } + + /** + * 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, @NonNull Map params) { + boolean isParameterized = isRemoteJobParameterized || params.size() > 0; + + 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) { + 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, StandardCharsets.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); + } + } + + 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. + * + * @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 auth, boolean isCacheEnabled) + 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; + 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, isCacheEnabled); + if (jenkinsCrumb != null) { + context.logger.println("reuse cached crumb: " + globalHost); + return jenkinsCrumb; + } + HttpURLConnection connection = (HttpURLConnection) getAuthorizedConnection(context, crumbProviderUrl, auth); + 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 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(":"); + 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 (%s)%n%s", + responseCode, connection.getResponseMessage(), readErrorStream(connection))); + } + } catch (FileNotFoundException e) { + context.logger.println("CSRF protection is disabled on the remote server."); + return DropCachePeriodicWork.safePutCrumb(globalHost, new JenkinsCrumb(), isCacheEnabled); + } + } + + /** + * 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 auth, + boolean isCacheEnabled) throws IOException { + String method = connection.getRequestMethod(); + if (method != null && method.equalsIgnoreCase("POST") && auth.requiresCrumb()) { + JenkinsCrumb crumb = getCrumb(context, auth, isCacheEnabled); + if (crumb.isEnabledOnRemote()) { + connection.setRequestProperty(crumb.getHeaderId(), crumb.getCrumbValue()); + } + } + } + + /** + * 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: 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 auth + * @return An authorized connection with or without a NaiveTrustManager + * @throws IOException + */ + private static URLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 auth) + throws IOException { + URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url) + : url.openConnection(); + + auth.setAuthorizationHeader(connection, context); + + 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 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 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, + boolean isRemoteJobParameterized, BuildContext context) throws IOException { + + String triggerUrlString; + String query = ""; + + if (context.effectiveRemoteServer.getHasBuildTokenRootSupport()) { + // start building the proper URL based on known capabilities 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, emptyMap()); + query = addToQueryString(query, "job=" + encodeValue(jobNameOrUrl)); // TODO: does it work with full URL? + + } else { + triggerUrlString = generateJobUrl(context.effectiveRemoteServer, jobNameOrUrl); + triggerUrlString += getBuildTypeUrl(isRemoteJobParameterized, emptyMap()); + } + + // don't try to include a security token in the URL if none is provided + if (!securityToken.equals("")) { + query = addToQueryString(query, "token=" + encodeValue(securityToken)); + } + + // 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; + } + + /** + * 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. + * + * @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 readTimeout + * read timeout in milliseconds + * @param pollInterval + * interval between each retry in second + * @param retryLimit + * the retry uplimit + * @param auth + * 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. + * + */ + private static ConnectionResponse sendHTTPCall(String urlString, String requestType, BuildContext context, + Map postParams, int readTimeout, int numberOfAttempts, int pollInterval, int retryLimit, + Auth2 auth, StringBuilder rawRespRef, boolean isCrubmCacheEnabled) + 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(StandardCharsets.UTF_8); + } + + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) getAuthorizedConnection(context, url, auth); + + try { + 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, auth, isCrubmCacheEnabled); + // wait up to 5 seconds for the connection to be open + conn.setConnectTimeout(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); + } + + 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) { + 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)) { + return new ConnectionResponse(responseHeader, response, responseCode); + } else { + 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); + } + } + } + + } 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" + 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 to retry - %s seconds until next attempt. URL: %s, parameters: %s%n%s", + (responseCode == 0 ? e.getMessage() : responseCode), pollInterval, + getUrlWithoutParameters(urlString), parmsString, readErrorStream(conn))); + + // Sleep for 'pollInterval' seconds. + // Sleep takes milliseconds so need to convert this.pollInterval to milliseconds + // (x 1000) + // 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, readTimeout, + numberOfAttempts, pollInterval, retryLimit, auth, rawRespRef, isCrubmCacheEnabled); + + } else { + context.logger.println(String.format( + "Connection to remote server failed [%s], number of retries exceeded. URL: %s, parameters: %s%n%s", + (responseCode == 0 ? e.getMessage() : responseCode), + getUrlWithoutParameters(urlString), parmsString, readErrorStream(conn))); + + // reached the maximum number of retries, time to fail + throw new ExceedRetryLimitException(); + } + + } finally { + // always make sure we close the connection + if (conn != null) { + conn.disconnect(); + } + } + return new ConnectionResponse(responseHeader, responseObject, responseCode); + } + + private static ConnectionResponse tryCall(String urlString, String method, BuildContext context, + 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, auth, rawRespRef, isCrubmCacheEnabled); + } + Boolean isAcquired = null; + try { + try { + 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 acquire lock because of interrupt, skip locking...", e); + } + if (isAcquired != null && !isAcquired) { + logger.warning("fail to acquire lock because of timeout, skip locking..."); + } + + ConnectionResponse cr = sendHTTPCall(urlString, method, context, params, readTimeout, + 1, pollInterval, retryLimit, auth, rawRespRef, isCrubmCacheEnabled); + return cr; + + } finally { + if (isAcquired != null && isAcquired) { + lock.release(); + } + } + } + + 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, + 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, + effectiveAuth(context, overrideAuth), null, lock, false); + } + +} 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/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..bb240aa2 --- /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 edu.umd.cs.findbugs.annotations.NonNull; + +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); + } + + @NonNull + public static AutoCloseable noop() { + return () -> { + }; + } + + @NonNull + public static boolean isOpenTelemetryAvailable() { + return Optional.ofNullable(Jenkins.get().getPlugin("opentelemetry")) + .map(Plugin::getWrapper) + .map(PluginWrapper::isActive) + .orElse(false); + } + + @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/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java new file mode 100644 index 00000000..53c104f9 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/RestUtils.java @@ -0,0 +1,52 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.BuildContext; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.ConnectionResponse; +import org.jenkinsci.plugins.ParameterizedRemoteTrigger.RemoteBuildConfiguration; +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 + * */ +public class RestUtils { + + private static Logger logger = Logger.getLogger(RestUtils.class.getName()); + + public static ConnectionResponse cancelQueueItem(String rootUrl, Handle handle, BuildContext context, + RemoteBuildConfiguration remoteConfig) throws IOException, InterruptedException { + + String cancelQueueUrl = String.format("%s/queue/cancelItem?id=%s", rootUrl, handle.getQueueId()); + ConnectionResponse resp = null; + try { + 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 + // whether the action was succeed, + // Only try once and treat it as success + logger.warning("Canceled queue item and not sure if it was succeed"); + } + context.logger.println(String.format("Remote Queued Items:%s was canceled!", handle.getQueueId())); + return resp; + } + + public static ConnectionResponse stopRemoteJob(Handle handle, BuildContext context, + RemoteBuildConfiguration remoteConfig) throws IOException, InterruptedException { + + RemoteBuildInfo buildInfo = handle.getBuildInfo(); + String stopJobUrl = String.format("%sstop", buildInfo.getBuildURL()); + 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/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..3d2f4ffb --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/StringTools.java @@ -0,0 +1,22 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +public class StringTools +{ + + /** + * System specific new line character/string + */ + 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"; + return newLine; + } + +} 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..5344d16e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/utils/TokenMacroUtils.java @@ -0,0 +1,40 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils; + +import java.io.IOException; +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 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) { + throw new IOException(e); + } catch (InterruptedException e) { + throw new IOException(e); + } + return input; + } + + 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 output; + } + + public static boolean isUseTokenMacro(BasicBuildContext context) { + return context != null && context.run != null && context.workspace != null && context.listener != null; + } + +} 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 d65fac76..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,27 +1,28 @@ + - - - - + +
+ + - + - + - - + +
-
+
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..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,55 +1,72 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..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,7 +1,8 @@ + - - - - - + + + + + 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..311851a1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-auth2.html @@ -0,0 +1,24 @@ +
+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. +
  • +
  • Bearer Authentication
    + The bearer token is used. +
  • +
+ +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/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/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. + 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..2d9f9592 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-maxConn.html @@ -0,0 +1,6 @@ +
+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. + +
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/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/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/RemoteBuildConfiguration/help-useCrumbCache.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration/help-useCrumbCache.html new file mode 100644 index 00000000..02b5df87 --- /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 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 new file mode 100644 index 00000000..c3e0cf17 --- /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 cleared every 10 minutes. +
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..219bca69 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 @@ + @@ -7,21 +8,21 @@ - - - - - + + - + + - + - + + +
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..a5210ff4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServer/help-auth2.html @@ -0,0 +1,21 @@ +
+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. +
  • +
  • Bearer Authentication
    + The bearer token is used. +
  • +
+ +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-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 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/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/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..e9f47ae6 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/CredentialsAuth/config.jelly @@ -0,0 +1,8 @@ + + + + + + + + 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..d6105e21 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/TokenAuth/config.jelly @@ -0,0 +1,12 @@ + + + + + + + + + + + + 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/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..2ab24529 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/config.jelly @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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-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. +
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-maxConn.html b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html new file mode 100644 index 00000000..f0e85cb1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-maxConn.html @@ -0,0 +1,6 @@ +
+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. + +
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..8295f445 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/RemoteBuildPipelineStep/help-parameters.html @@ -0,0 +1,25 @@ +
+
+ Job Parameters +
+ Parameters which will be used when triggering the remote job. +
+ If no parameters are needed, then just leave this blank. +
+ 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/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-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/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..02b5df87 --- /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 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 new file mode 100644 index 00000000..c3e0cf17 --- /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 cleared every 10 minutes. +
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/OpenTelemeterTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/OpenTelemeterTest.java new file mode 100644 index 00000000..df5023e4 --- /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 edu.umd.cs.findbugs.annotations.NonNull; +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); + } + + @NonNull + 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; + } + + @NonNull + 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; + } + + @NonNull + 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); + } +} diff --git a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java index c7e14599..60536286 100644 --- a/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfigurationTest.java @@ -1,39 +1,556 @@ package org.jenkinsci.plugins.ParameterizedRemoteTrigger; -import hudson.model.FreeStyleProject; -import net.sf.json.JSONObject; +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.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; +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; +import java.util.AbstractMap; +import java.util.HashMap; +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; +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; 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.MockFolder; +import org.jvnet.hudson.test.WithoutJenkins; + +import hudson.AbortException; +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.security.AuthorizationStrategy.Unsecured; +import hudson.security.HudsonPrivateSecurityRealm; +import hudson.security.SecurityRealm; +import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.LogTaskListener; +import hudson.util.Secret; +import jenkins.model.Jenkins; public class RemoteBuildConfigurationTest { - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); - - @Test - public void testRemoteBuild() throws Exception { - jenkinsRule.jenkins.setCrumbIssuer(null); - - JSONObject authenticationMode = new JSONObject(); - authenticationMode.put("value", "none"); - JSONObject auth = new JSONObject(); - auth.put("authenticationMode", authenticationMode); - - String remoteUrl = jenkinsRule.getURL().toString(); - RemoteJenkinsServer remoteJenkinsServer = - new RemoteJenkinsServer(remoteUrl, "JENKINS", false, auth); - RemoteBuildConfiguration.DescriptorImpl descriptor = - jenkinsRule.jenkins.getDescriptorByType(RemoteBuildConfiguration.DescriptorImpl.class); - descriptor.setRemoteSites(remoteJenkinsServer); - - FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); - - FreeStyleProject project = jenkinsRule.createFreeStyleProject(); - RemoteBuildConfiguration remoteBuildConfiguration = new RemoteBuildConfiguration( - remoteJenkinsServer.getDisplayName(), false, remoteProject.getFullName(), "", - "", true, null, null, false, true, 1); - project.getBuildersList().add(remoteBuildConfiguration); - - jenkinsRule.buildAndAssertSuccess(project); - } + + @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(ApiTokenProperty.class).generateNewToken("test").plainValue; + + mockAuth.grant(Jenkins.ADMINISTER).everywhere().toAuthenticated(); + } + + @Test + public void testRemoteBuild() throws Exception { + disableAuth(); + _testRemoteBuild(false); + } + + @Test + public void testRemoteBuildWithAuthentication() throws Exception { + enableAuth(); + _testRemoteBuild(true); + } + + @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 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); + + 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()); + } + } + + } + + 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, true, remoteProject); + } + + @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.getParameters() 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 { + 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, 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); + } + + @Test + public void testRemoteBuildWith5KByteString() throws Exception { + enableAuth(); + FreeStyleProject remoteProject = jenkinsRule.createFreeStyleProject(); + remoteProject.addProperty( + new ParametersDefinitionProperty(new StringParameterDefinition("parameterName1", "default1"), + new StringParameterDefinition("parameterName2", "default2"))); + Map params = new HashMap<>(); + params.put("parameterName1", TestConst.garbled5KString1); + params.put("parameterName2", TestConst.garbled5KString2); + this._testRemoteBuild(true, true, remoteProject, params); + } + + @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); + } + + @Test @WithoutJenkins + public void testParseStringParameters() { + 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 + )); + 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 + + assertEquals(expectedParametersMap, actualParametersMap); + } + } 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..d87326f5 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteJenkinsServerTest.java @@ -0,0 +1,93 @@ +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.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; + + +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; + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Test + public void testCloneBehaviour() throws Exception { + TokenAuth auth = new TokenAuth(); + auth.setApiToken(Secret.fromString(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 + verifyEqualsHashCode(server, clone); + assertEquals("address", ADDRESS, clone.getAddress()); + assertEquals("address", server.getAddress(), clone.getAddress()); + 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(); + assertNotNull(cloneAuth); + cloneAuth.setApiToken(Secret.fromString("changed")); + cloneAuth.setUserName("changed"); + TokenAuth serverAuth = (TokenAuth)server.getAuth2(); + assertNotNull(serverAuth); + assertEquals("auth.apiToken", TOKEN, serverAuth.getApiToken().getPlainText()); + 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/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/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 = "ivxuKazauCeEHuXxTkp5hbzFUqFcvaCNmimtib96VkuhX5DdQpP57TMJ9iCNKeRQzjGzjmCP6Hwd8H5zLhjJ2BWYAm5Kd2eHGvKqLUP" + + "MqRxuJYUHenA3ngXt42AzheFy8xr8xCVmwpVGU7M4AE8AZ3ran3WfQTnBYbixLHfMYYjmM6de2GqjNvQXdUFht4LpSkXY2DFLu4nUx6ENkarfpeSu2MxruUJEqpZ88VSHhVQ8Pid" + + "tyKgfU3LWfHArurLu2MAg9nm8WMaxBmHjrPMwkqwHqvNSwEX6Ah9vw9MKQVxTKarZtb5TYbCrkL7wPdWZapSnriWJudtfYZMaVQqdcKhFiz7SxWxza7L9MgHDH8wFhVwpALTnGVD" + + "am7MNU7Vb5vcRv3MckwWjuyGMBQqDtCTxpfBKLX77UghhRAPAfuFAFM7kSK6m74wGzEDgv5ekUYNvngt6v2eSweHxAhKcUqUA2KRZKfeLFrEkqzfA8CZCwW3CKfTUyKAqkENNSXkQ" + + "QHpRAMqEnPxgd7p2BmB85FqvVFmK9wZztXJt6Nni7YYq9FGqaxfpPVuwxfQ8Jb5bvdU8WfGnVChPDdM5Hy5CzZbFw57cufXT7c2EMpHyXYZz86FxcM2St8PjPwafFN4YWXCAb5Lm" + + "95XSzjh3ZSR6tg4tQ94CMBTqVnchurvE25fZGq5v7HRFV9kdKXbzgchkM3HinhuRQJBAZKwvgdd7mdB6DEMCjnKxZXQrKkiB4de58fu7dwizVgQgGe2QXFyrVdb5BWiphLthknqS6" + + "hzx3CCwxPZD7rwf7hYaS4pmkrP52LuGrAaTQg6HxEJhdkdaWhcN2U4yKie5Jn23PUqDYt48YYFd3xvD238wqwywgeG63k8r3Nh6hC7H6YQfqTW2EcH8Pap2E5c7M9YX7D4LEfWQwx" + + "jXiHCUygEDpzPu7Z2DFzvgZxDKJCcuAXPXEuhTPU3xLXMPXQJaxMfXF9Gfe72rp9gqy4Q7rqpVHTDURM3B2rTuHPaVjMPMvi3BK55VzXcKi2t8mVfcg58Ugbffw2NZVXyDjataQt" + + "DupjKxqVAvQuiEkKRaEU3eHrhP5erYUhX3BLhHT3LQU2u7gdkb6NbaGDzKA3tjx2DY8xNL3QcBbR7ra6Rg76pEN5xniuAkEgxACTWNqNVNuufGrZfDDeJ7NcEUmNZhBFidNL3f34" + + "VEN7kKRUBH3JefxV4Z3fMLkpJkPD45Xx9hkhkckufxjrJSeSyTYeVYADhRc4qPrEceCXAjgpZLh4rvh8bJc3tyRDT3nihvzZHAaThhtzPgjnPKbXyLynNbdmfrUCWypz28F7SN3kx" + + "XAPUS8baWL4VVYQQ2iyyPQ9QkAycubeUcAv2xwej5iuKcwaGcHPKjAatMErAA3wn7E3zuvQUKGi2yi7mqNXnTgE3kq9d9jV6vZDi7tRaBhqeMSiS8gNTfcHnrSyk6UJdMCAgYnX9Y" + + "2y9dHdp7vwci7rV7anmRXYjqq3eeyPhLmFF6V4EFtFixpU7LdcJzmjzxuxCp77NUXii6hf9tnMyYMDHrWvrvaTctxBk67TAzZVXYPkxKYNXrfU86DM4rGTycNRvLJaEQUBQZhynLj" + + "Qyyh9TvCcbgrLiFRjRXhfKwKR2B2PaHL7fZ3TgyVAbKmMa3r4RDeeMtgBa6fVv54RVP4RiD7rmFqUFpqy2VWkGABCSHKiWWcBmikUzCjYz9KcUkPkjfBVqcZe4uk4yBySb8ubhPVX" + + "M76H7d2VBXXyJiAfGfC5q6T663zkDZYRrGHfDeHcSLU7uYqJ3RQwpqZA6axgftSKJnRCGSv3iMpWnayBAreXrw6bvRAgA5J4vvFbdi7E8TBriStZ7wir9KLZ8uGhbQ5DUEAN4YuwM" + + "iEvx7bYwA7vGzAnGWJHwzWPiTtgquASCHbcXPeW6hYhVQMSf6SUkATKFRLQVLbkQzQnxvtAYNEKr3q62wWvWtNgtW8bezVPTHrMVVCe3e6XkPaTdjCr7cxW7QVcYVBiuF9H6TmCmw" + + "WemkJBhC2BYyBXSGDCWkZS6AvNjjNaJHmD5H3cpbec8j8yRiDY25gSH2HX7nkuA3pcGVxZU455WvkcVZNd5caSxSzpRtB2ADjKx66h2p49biG2nKkBhfhzPR6J6L4LRD5udL8mi4" + + "XVkW5JBQMvZi8Cwtt8DtTSMPyaq3uAHBY94GQQenVJyUyRq6Rh8g3THXS4EeAm4K9MEaJdYHeMnnGY8hXBRDeANFFPFrWTKbB73hDVAkRzEyW52McFFgK4KByeDqxZGyqQ4Ch6uLe" + + "LfkY3ZwMnKMcMLMYXPEy9iwRnuyL5xmR9NU8YcnhrvAcJmLmJKqxbXEBSiRmNaDYDnnhAiYgQETAfw7kGCrnbrK6GbRuqZAzA8CTdxcqHxucxZXyRdDqhBTCKcTcXnBu96X43kqmS" + + "gxn5vHRmQ4Wfjki4RYLVvN7UZGCCDVJGZZrWKFH7bDgeih5ptMMpjyYBmc25VDXA8X4UUQrfauiX4XpP2LnKTb3jHJh6JT7JmzD2HipSWNiPeP2BpYnhAgyuQU4qWjtRWEfLdGKkj" + + "QBazv5X7FPKtPwm47Kz579cyFHbm4uhttjNrfhQDy9vWDXw6umhE48rhViVhmutqraFL9ZPpfF6bzinHMYyvj9BqyunVESYdi4c2Hf5jFuycEyapUjTY5yAgRWPczU6gh9TnRJJZZ" + + "EmwZ8m9ubT26CcBC7wEPjV7Wd5dUZuE9LQ9K8ziFrcpqEwbxQPACZq89RpJ72KDjayUUDSqLUVDuDRjTer3futTYcmqAaXkhQyVkcA7t7QWQie7qW2m8JSpkriAw8mN7nUbEJ76G5" + + "FFfkLFtH99ajmZ872AZP9RAxd2Tzx7YYDSXt9hy6tYgVt2u2zVTUe8T8PSFnEdgrCDhb35U7drJY23Yn63C7iH9bSEPiC4VfEjDv85akuYmYGiPh3JHAFyG3jryXPRuyNDLMw3Xin" + + "VbWBcHyD67CJnBLD4Xm6CG2tMcNrznvQTKmvw9zJtamVmXzbcC7xtQ8zn3Q6KFWVYaJ89mCw3wv9tMdKHkPT9wCQb8NkpuaAU2g4fMqtpD227QyYh6B829pRcwtRQJS3rF4bxhAau" + + "uSEptiUD435rzhMnt4XZmzVN4Z54cKmDZKWJ4pix9mVnypur7Bv2QW7ckRGiZ2BbSMCfTi3UTiXnyP7wjjmX8JbukyNVaGKiw279MYjpCbg72TZrh7hAakvvrbEj9kGX759Sc7HNY" + + "ZgajYpxPzYQw846puC9BATLHPFAQpjQkjeVk2dx6bSKrXK7kN39QDLhjSeTwGQaUuTQYU9cbTnU67RPn2HceNuK6wJBiwKdZb7PAiqRHk4zTTUuWZLf8Tcdqud6RfVZJvZmvSnqa" + + "Vq9eGdD9G37tUcpHLY4FAT2Cu6NC7YMqLQcJwW5Z32n2nw5pzSytaKkb68RSQvubDbrTJ7yPj4iF4nLrKtTATMx7w2qyerbTeKvnv6dxP8VWBwdQxygKggSS8WUL2cZgHNrdH2rXG" + + "fXH4gU3p4FXb5w3zdBMkDVSfiMWgUSA7A9VuU4crtJmRTLxS2eEhhH2gXaLgTJqMeqR4YGyHgxwfrYGDzKNKeuu37h829QyjBmWceyiEZVGF6f7mLKS78xEy5CQPqAQqBZk4vHeZN" + + "YJTh6et7tJS4SFAdEQvSmg3mCmNYkKTb9iuWhqjFAmi5Uu7NhYnzqML3UY9hAD8B6zMuYvDWdNWrw56mePGF97b258hKeiZMRmVxFvP8YcqR8Gmbuic7HPjD4kTYwrzWkKteyCXf" + + "e9KtTPqZKzeBy8rL2Cc6ZFvgW7MMC8QNE2e73qiPNDkvvZDemECbRXed58DpaxFCF4CLjh6ytm27UAB4jSwvjYLYjcdfqTZtVRyN9FCqwKNKrFNHY3H9tdYRXycmLXCnGQKFiRhF" + + "nEZtHXizG2e7AbyPvnej6S9chNKXzv6fU99VfCYxdzbje5GzWSzP7vwDmMNUg9NKTdDPrJA99XeVN8c2R8GavaJBpvUrCUZWCJHVRYBd9x39WgkbkACHiHiMfq39PKxgzwtEftgXg" + + "TrVX3XSiuKBrEXAxMXeAtyiSjDtC5uCMS8cYDbHhPrEdXk8gjywX5fPg293MayrQTTJhXC6d3pjnS8t8SvcHD9CvKPJjF6W4W7M8Ahd2nEJiYHRyKudC6inM6xHtpKfL47xAWKdT" + + "WnTbECJyqTYRxSfEJdTtHtJ3RMt5mAUBGcNnEDfn7PUwT3hnNMHhkfh8AcU564EQMyNJcUnv5Bwg74tHaraQxyRKAx7ZG5w8RF3tgAHvCV7t3ETz2krRwEn4UH7wzY2yfbX6wYpQ" + + "f4Tqbb8nv4QdcdhZVZNwbPQbaf6qgnc6LZ3GFpQTV99CMuGj8mg3zUkwfRDAK4ffiUTktYv3ZLYivYSn5BQpdDMe45a323hY9xny5cEhq2fBjPSQ8k3Y3utBgkcJhR2TfWzV7jg5" + + "Ti3wbwHxeXfBUj5vXfDxUgZdwtKg53Tp482iguJ7dzYHbhk66wzYDmERunpp84pwjZinVVLRKehSDdPpQqqqSUz8ApnGyBYykDuT8cKGhgtW9mzffPAbH8Nnhw7vYnZHhMbLDSXX" + + "VSbU6HU25Vy7xKf2wHTdkT96avqbqV5MPMrz35rY5eRHdPCKyi7NaLj3wrPqxN7HDYcRbTDLt9EAqUC7rrfp3BFWZwHMMcxaBQcSPyEGuT2hVCS76UrFUPCCVTxp77EdvUSLmhxh" + + "KhpbhanYK9bBUv6eAGTxyw8SSnZfN5WPCTL99MUxkYmTn3yJvtPyH9puEX8YnNnp6pQbzEFce8TEH5vLMjZv9Ljn6MnJWmv7C6i2JZLVEyHztfCYvMQZj9X9yvp9fEZQvQdTec7m" + + "5KeAxd4NXuUtyj5FuyGYANYWjFhVRD3Z2mhBBfcwcwu3DCzYWZmtWMeSSRbmpYhhZ7RHZvgahDn34Q6vZ7yXZwEhMHxJcEPDWS5fQ4p29HjiSFc9iKVuD"; + public static final String garbled5KString2 = "h5RV8eEnPk2jyfShzrMUJRzE2Y5XvzAEygg6ZVRT7Upn98Hkx9wqjUNg2xmpTSmPREzXtPevMDMGMXeWR7MC5cn6xvQXyExRPMVjtJ" + + "HrhkQKdt5AEKc5GKVFwfiF4BXf8dD6C7xagcn34GuB4PvciLXJ7ERGPygaXUMVkqhjh9d2vTCxtumbKLeXYbFDzAmyRRZJyPkgnqyzkVTxg937Nz4QtYUNKMVLLvgCpGYX6eC2m" + + "uamtJFPWVd56L62n63YXpyntJQkHcWxwk2CLtH3mKVMhprpRtTE9v59DPifKkYfjiyM8qNMxfZcSk8HdbmhLdPCR5DyLqxf78gtChTmHHwEXb8EwT5UhrtPpF54pVpUE3Jj5r9vc" + + "ChxxZDAfk6Kwv7e8YeUZSHk3X5D6itVSE6UMQZj9L6nhytTynBrftVhrAJpuvBDpqh49n8LGhG283dBtawNm2GMjgEjuwndXKyhAqBvqkGgGGdyuKbCtM75VaFUTSfy9qx8MXHFFJ" + + "P5mYR2rQgEyCHLyHXMvz84HWcAqPWuHUayd88wpzwgeiMxj5J5BrXGgPXnxbmyMFgjynBhYg7YyfLCk3hvP2wZcqMqHpvRktYTiBJ7AKvfE4w5NKY3BePtKaZuCyLHp8vkX9AXH" + + "7cADHGUixVgCfYjYLTQHDCQiarwBQqMVTBkvUCRb6ZvcLiRBf5DKbybh7my3Jpeui4UD96iqHJUwVhytecAwe4iXjSfMe3q9x3qKcDpfQuGghx5pjfJ94BBFvTGRDRqvHKzLpBRC" + + "vdqnRPPC4vyNjbV6YqJAGHceTrXpCkuZcPhhxFBvdcWPZiFcW56HhwA8BvHnxRd5iBLV3GcaxWCctRaTZW2cBGvuyBAfDHYZVv7hPg8tW69YWTyCQvCAVDJSr7iiBW7gJUrjSw5N" + + "hBRuHxPGDib3TLqCMC9fcZX7XctptkgjBfhnbpPJ8wEmE4CVwXYpqF97YKMU2SyNFAERWK6QRfPhXwuK9UXc8jXe2QEr2ZF2eamDrvzScxy4z5Vaafhi4kiL7xXEkYJcKFrrgzx9" + + "qyHrkumAdegFpVZQn6yUXpnndKQSYyXfhWQhnM2QayxHFGWp78SJ8ZqzTtNbwyDUeLqUUhmfxub9NeUny8eSZiY6tdyJkqawtSQJEXz9krpqdGGxJPpfpZiD3RSikAavWe4mHa8m" + + "yJCuSHzxtaL48cvyKPiJyFqYGy7dj43tTQXfiYuxSTTNkxr9wwurmCFBXrMDNM3PXKt7xYDRkkCKHLhyYTmVmBKq2EcSdZbxCfhnGvJe3qycSBHMmP9Qf2672F2Ep5Ccku9cv3NV" + + "jC8pYmCdrx3JgiMkCghSrr3tpw4deZGPiGjiR2D9thu74rF4rVvc9HxfeREhBKWprKey4SAgdcJqqzZuEgKKAVkrd57VNy8UMJCCQg7uKrRxXPdfCMtyPT9RFnjdbiX6CUCekNh2" + + "AFGNqEhpQZqVjSNpq5ecwHhmctwpM98tuk29Tz3bwwPJa3SzCinm6PLeCcXUWBuhLkA8hZnxTUEayLyTnNc9Z2ngjhpaD5ZpH2hacaMLZrVHKu5QUc2WVnNxDmDRdGhbGUfpRWBy" + + "k2ZmNz26q6pU3vBQQAeM8jrHcY78h892naukUNwVb5gaxkZnd8YrimfNj8V48VyTun62BZ8EEMg6qqhnhfheaa44D4RyYWCgEam244N9PmdPifDDDUtiEzYqtfPfeRLjUmktA99f" + + "YZju3TNycgF4C45zEyaqnrUXyukEABewyGXjCXt5FR5dBAGi6ezixvFzAbQHDVhhfJ7ngR8za8CBBqRRHcQNz8nhCq5iewcyqpbRMt6xtS5cgwFvBZ8prNbKkVR5pEpKCjqzrb5n" + + "RvaP6GptKu7AeFKeqGTNxuHvxVhAvxCRUDTxFw4bk7w7tCQEQ3f7Zk6mQgr6cefewCxHvHMukywrn6EBMakgY5DWDB6BXn7YGQ7nQ694MDUkG63zWWgWQg6KmHzKafJabTLLwGwm" + + "F9Qj9HkRB6QSY7Ty2NEwKePybpbK73VybNNSyrkpLYhuKqhaW4PyR2HvQZLHz7bMeKcfreSTDLSQNcGVFAitFt9rFGm2YQMdnabxBE3TNmDS3utNGFEexjYLA3XbhgKw6zDQCfX" + + "ux6YEepFYmatUXatEKnNnnRDCwJGLRNHPZVmrYq64bTUR3KMy9jpaRjvNv68Gzd54KaqLaYdtafHgX7g5WrhbGZVqg7hvkGeyQXdN8wEjAeMgMDnyZUU5fJgPCVAQWLAZGrzyaK2" + + "Uu8dpqttfyd8PJccXD9Y9YkWynuNwMdSJKKKPuCTrddDGgtq9FzREYCRGC7Y3Zj3EF96BxT7kFyWStLbdc2VvR299QwAvwgcNp8KiL9E6vNDWKz5F9dik5ze4ecUd6V7uqTLDEHk" + + "4VQdQSVcQg8Gn2FgmwQaba5xRQvkymK9RM9tnkQNPeLQRR33pf3yHcCFXKTKNAAtDwZBwrXfXwSt3bb72DTfRXTP8kK9KWNQhH3533DQSvM3xAmJFEG6BTf89QuYJgm7yhbzRNkm" + + "ThEcciiGxkceRRdAHBpxiZ9CYvraxjtWLF9hNWX9bJLDrnqE6VYuATDrgE3F2Rg3ZFpVBZpy2tC27paVYBfxaYpyxNVXjnP3bFUHNxfh9TjprtMXu6Xr685tgqn6bDqMXGYcFCXG" + + "jUdJ4qDUm6br74hAgUycjXgfeyVjvdWy3mG9zZmNeWEfU6eaACLcQX3fn3uTk2zaNVWSWPAzFmuGT7bTAFShdNGcMd22viUCG2pxMTPVMqJ8tjDjmwTd2WUXG4c7v8FVC9egzRj8" + + "Q55nABG6FVVRLwYQkYVAk9FkDGrYKRcQCJLrjDnwAWLKuqrQxBXPC4DU6ztBJbMxNZkPU5zZ75SqE2AQLbZF9i2QrM2abg4jCkjnefTBTpzNgWdVkAA4E5egKXdJZyXQuZrAQCp" + + "CMpUeiM8FhTEZVKhT7ytEAXy9M9DnMUh8JPfPtWDSx9KFYm7pbrwLvPKZcTKFd68urnKg7SgbM36Tb2NjWpegzSd6gqYtJ698WXQBArnpgcuNjG6kNa3pNe8XNp2C7VmFLANP7E" + + "W5b95NGTMB9tzrijUu9rR7Tzq2Xi7QXNniPvgFiWC85SBgJ4KN3JXrQrGxCW27v29phwR44m4vzfxxwfB8adFTgC6AU9KRUF8HFVCMxnkjRYj3DZbS2G85Z2mixWgDN4hR2e9pb" + + "ixDCLxUuF3aCCVJZ9rdaFSK3vk2gwrVkEwhBD3XWxwST4PfqYePAwFgrgvjBN33BELT6FSRKruYMzAkkGryt9r53CPmRdU5fKMzXxtPTGrhAyxJmitMtabJjj8f9WQnnMtcqWHTM" + + "DT2jLbFWUaktyyeTwXnR37g22DeRd63unUpP4kE3bNwv9ZPvSg4ES3ehgNf5aj6veTctaGCBmLMFZhQuVzp5vXt2QgNa4iSR2JdDpBYcgpub7GpyeGp2RkDZh3S3Rjfcv8k5GSx" + + "2N6BHxUhMUvAbevuMMVwh8enLBz6SLq6fFztfGfwCeCuS9fD3KGiFwiwQatB3ZE8ScVhXf6AjCuGYWPMZPyK56njcqL4NFECbiEN8jeugfYjSh5TypDkhDrHifaiCVuDSitKdNzU" + + "B5968FdGELVSNQvEDCuNqT9RLLTUGiJ2KG4i5vrF9ccXdPzaRNUQffaYxJwFKb9F8P4SmhPdqcuULjeetRm8BfwmLhpSYihuzQKRMUkZbBKgCY3eeEfU7Vif44xQA7e6mr6Qrbt6" + + "WrzytRuWQKcPyj29DUk8SiAcMSkYYLciHqWw6B89b9PKv48GY7Lq6NchTGULFu6ErWc2MYxWmcwBacuGUj9tSXERcf8HwAuPW85RknaDdpM3FBjMTvxnN9LnWgZkuL4mFU66tUSn" + + "VJ4nUzfbYfSRwehQ9SJhammDiANDntCfXdQGdKEhcqbLWEdChmgaXqX3DwHwADBEYvugN8ngJ7USRz5hLmx6uDc87atDL55JLD3qjpw8hjv4CRztuhVfeqShCwV4CA84i3cY5LuE" + + "wBVXrDLpK5494Q8Gkcc6MJiTLMyYUZr42p6D5jBRhEUdBMBeUjgYBN6K6A69GV5mVpySj6JamiECA2yVz8ptpbneZqpRyRmbqieKMrikwJCbBwzz8EmDLKfwpT9DgdwPEkUpXDn4" + + "Nuc5iKwXxnFf4BSTcRZxH5GtwgYvUiDrZqaNBL8SSJe8NU7jB2FGyBRAwHr4VPf2pa4YSDf4pvLvPPpUnm8LJLedAThQTSESfKKbreQYatLVmK7g53mEUijKLceuMawuAawyaLk" + + "xEhPEEhh7vktLReU4kj89XGJFtd6JfQBSJudEjm9WzD6NjHFMacLeeW9QXpvNxEgVqzFU4BNbAaRaTpf83khgzb28eDMCbxEbNHGDkGfkp3kyxRRKpGckeeU7KdqyU85wGp7DG7z" + + "3QDWSE6TvUTdATqn2qKXqSuetUtRwg8gnJTiqYemmBuPt83eKcBxJ2X94TZTqhBnxphKYfcaTpccqR4AbuHHRnhX9MBGEY5F6QFCQyEy4dMyTcFS9NiuTnpM8RJ2CjycuJmgXrQd" + + "TgtdDUCuzhXLWjZUxexhtATUm5NJFeDXg5cD7ddbA9kC6Nxf2PVJDWywbdrqQpxg62jLSgXYcLeaM6YLxSf3RRj7PWKSTWPgrTEDPzcCFK6g8y3ZZG5LqJhkiQKEpCnXwH2vQEBC" + + "zUaCZKJeEvxVx5eQG2vb8eeHTMcbcSY7TfTAHGSN5YWHRMr78ADyac4eYxppghSmkC8fVZViXmKyhpNxyyzESEKNhD8v8e5E7HcyZMRzih6ySXbUSJeLfJiQ7yLNRAtPXi9QcQuT" + + "EzSRcnKMFWFBpEXUweu3gqTczKPY4uLAXwbxqurhm2gzBBPzp7KrwS2byeqJYg9AGSkPZDJd5VdfJBSfH2PUzjVfWaPEUSeaWchFJX387YLUp7LtxfQVVRD2Hc9PWpNB6yVML7fj" + + "EUUSMAgWw"; + + +} 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..8e5a2018 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/auth2/Auth2Test.java @@ -0,0 +1,105 @@ +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.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 { + BearerTokenAuth original = new BearerTokenAuth(); + original.setToken(Secret.fromString("original")); + BearerTokenAuth clone = (BearerTokenAuth)original.clone(); + verifyEqualsHashCode(original, clone); + + //Test changing clone + clone.setToken(Secret.fromString("changed")); + verifyEqualsHashCode(original, clone, false); + assertEquals("original", original.getToken().getPlainText()); + assertEquals("changed", clone.getToken().getPlainText()); + } + + @Test + public void testCredentialsAuthCloneBehaviour() throws CloneNotSupportedException { + CredentialsAuth original = new CredentialsAuth(); + original.setCredentials("original"); + CredentialsAuth clone = (CredentialsAuth)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(Secret.fromString("original")); + original.setUserName("original"); + TokenAuth clone = (TokenAuth)original.clone(); + verifyEqualsHashCode(original, clone); + + //Test changing clone + clone.setApiToken(Secret.fromString("changed")); + clone.setUserName("changed"); + verifyEqualsHashCode(original, clone, false); + assertEquals("original", original.getApiToken().getPlainText()); + assertEquals("original", original.getUserName()); + assertEquals("changed", clone.getApiToken().getPlainText()); + assertEquals("changed", clone.getUserName()); + } + + @Test + public void testNullAuthCloneBehaviour() throws CloneNotSupportedException { + NullAuth original = NullAuth.INSTANCE; + NullAuth clone = (NullAuth)original.clone(); + 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 = NoneAuth.INSTANCE; + NoneAuth clone = (NoneAuth)original.clone(); + 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); + } + + 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() original", clone.equals(original)); + assertEquals("clone has different hashCode() than original", original.hashCode(), clone.hashCode()); + } else { + assertFalse("clone still equals() original", clone.equals(original)); + assertNotEquals("clone still has same hashCode() than original", original.hashCode(), clone.hashCode()); + } + } +} 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..54fab1e4 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/pipeline/HandleTest.java @@ -0,0 +1,34 @@ +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, "- 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()"); + 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/remoteJob/BuildInfoExporterActionTest.java b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java new file mode 100644 index 00000000..ed09f06d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoExporterActionTest.java @@ -0,0 +1,188 @@ +package org.jenkinsci.plugins.ParameterizedRemoteTrigger.remoteJob; + +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; +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.Result; +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; + + /** + * 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")); + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + for (int i = 1; i <= PARALLEL_JOBS; i++) { + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, "Job" + i, i, new URL("http://jenkins/jobs/Job" + i), buildInfo); + } + 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 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 { + 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 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 { + 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; + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + RemoteBuildInfoExporterAction.addBuildInfoExporterAction(parentBuild, jobName, i, + new URL("http://jenkins/jobs/Job" + i), buildInfo); + System.out.println("AddActionCallable finished for Job" + i); + + 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)", + (success ? "was successful " : "failed"), "Job"+i, projectsWithBuilds.size()) ; + System.out.println(message); + if(!success) Assert.fail(message); + return success; + } + } + + /** + * 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 { + Run parentBuild; + + public BuildEnvVarsCallable(Run parentBuild) { + this.parentBuild = parentBuild; + } + + public EnvVars call() throws MalformedURLException, InterruptedException, TimeoutException { + 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(RemoteBuildInfoExporterAction.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; + } + } +} 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..6d753b8f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/remoteJob/BuildInfoTest.java @@ -0,0 +1,68 @@ +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(); + + assert(buildInfo.isNotTriggered()); + 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."); + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildStatus(RemoteBuildStatus.FINISHED); + } + + @Test + public void buildResultTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + + assert(buildInfo.isFinished()); + assert(buildInfo.getResult() == Result.SUCCESS); + } + + @Test + public void stringBuildResultTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + + assert(buildInfo.isFinished()); + assert(buildInfo.getResult() == Result.SUCCESS); + } + + @Test + public void buildInfoToStringTest() { + + RemoteBuildInfo buildInfo = new RemoteBuildInfo(); + + assert(buildInfo.toString().equals("status=NOT_TRIGGERED")); + + buildInfo = new RemoteBuildInfo(); + buildInfo.setBuildResult(Result.SUCCESS); + + assert(buildInfo.toString().equals("status=FINISHED, result=SUCCESS")); + } +} 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..d7ea4529 --- /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.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; +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, HttpURLConnection.HTTP_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 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 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}")); + } + +}