diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f5250aafb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{html,java,jelly,xml}] +indent_style = space +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..fb83349a8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @jenkinsci/lockable-resources-plugin-developers diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..fdc58d1e9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 000000000..0279984d7 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,15 @@ +# Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins + +name: cd +on: + workflow_dispatch: + check_run: + types: + - completed + +jobs: + maven-cd: + uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 + secrets: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml new file mode 100644 index 000000000..3580cafa0 --- /dev/null +++ b/.github/workflows/crowdin.yml @@ -0,0 +1,42 @@ +# This workflow requires additional setup, see: https://jenkins.io/doc/developer/crowdin/ + +name: Crowdin + +on: + schedule: + - cron: '0 */24 * * *' + workflow_dispatch: + +permissions: + actions: write + contents: write + pull-requests: write + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + if: github.repository_owner == 'jenkinsci' + + steps: + + - name: Checkout + uses: actions/checkout@v6 + + - name: crowdin action + uses: crowdin/github-action@v2.14.0 + with: + upload_translations: false + download_translations: true + skip_untranslated_files: true + push_translations: true + export_only_approved: true + commit_message: 'New Crowdin translations' + create_pull_request: true + pull_request_title: 'Update localization' + pull_request_labels: 'localization' + base_url: 'https://jenkins.crowdin.com' + config: 'crowdin.yml' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: 26 + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/jenkins-security-scan.yml b/.github/workflows/jenkins-security-scan.yml new file mode 100644 index 000000000..2e1301124 --- /dev/null +++ b/.github/workflows/jenkins-security-scan.yml @@ -0,0 +1,24 @@ +# Jenkins Security Scan +# For more information, see: https://www.jenkins.io/doc/developer/security/scan/ + +name: Jenkins Security Scan + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + security-events: write + contents: read + actions: read + +jobs: + security-scan: + uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 + with: + java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. + # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. diff --git a/.gitignore b/.gitignore index 6a8ed57db..61bc0c38b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ -target/ -bin/ -work/ -.* -.idea *.iml +.DS_Store +/.idea/ +/target/ +/work/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 000000000..a096e0dbd --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,12 @@ +tasks: + - init: mvn clean verify + +vscode: + extensions: + - bierner.markdown-preview-github-styles + - vscjava.vscode-java-pack + - redhat.java + - vscjava.vscode-java-debug + - vscjava.vscode-java-dependency + - vscjava.vscode-java-test + - vscjava.vscode-maven diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 000000000..9440b1807 --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.13 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 000000000..f7daf60d0 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,3 @@ +-Pconsume-incrementals +-Pmight-produce-incrementals +-Dchangelist.format=%d.v%s diff --git a/CHANGELOG.old.md b/CHANGELOG.old.md new file mode 100644 index 000000000..946e52daf --- /dev/null +++ b/CHANGELOG.old.md @@ -0,0 +1,88 @@ +# Old Changelog + +This is the old changelog, see +[GitHub Releases](https://github.com/jenkinsci/lockable-resources-plugin/releases) +for recent versions. + +## Release 2.5 (2019-03-25) + +- [Fix security issue](https://jenkins.io/security/advisory/2019-03-25/) + +## Release 2.4 (2019-01-18) + +- [JENKINS-46555](https://issues.jenkins-ci.org/browse/JENKINS-46555) - Fix NPE + on invalid entries. + +## Release 2.3 (2018-06-26) + +- [JENKINS-34433](https://issues.jenkins-ci.org/browse/JENKINS-34433) - Signal + queued Pipeline tasks on unreserve + +- Allow locking multiple resources in Pipeline + +## Release 2.2 (2018-03-06) + +- [JENKINS-40997](https://issues.jenkins-ci.org/browse/JENKINS-40997) - New + configuration option to get the name of the locked resource inside the lock + block (Pipeline). + +- [JENKINS-49734](https://issues.jenkins-ci.org/browse/JENKINS-49734) - + Add a PauseAction to the build when waiting for locking, so Pipeline + representations in the UI are correctly shown. +- [JENKINS-43574](https://issues.jenkins-ci.org/browse/JENKINS-43574) - Fixed + the "empty" resources lock (message: "acquired lock on \[\]") + +## Release 2.1 (2017-11-13) + +- [JENKINS-47235](https://issues.jenkins-ci.org/browse/JENKINS-47235) - + Trim whitespace from resource names. +- [JENKINS-47754](https://issues.jenkins-ci.org/browse/JENKINS-47754) - + Fix broken Freestyle behavior. + +## Release 1.11.2 (2017-03-15) + +- [JENKINS-40368](https://issues.jenkins-ci.org/browse/JENKINS-40368) - Locked + resources are not always freed up on Pipeline hard kill when there + are other pipelines waiting on the Resource + +## Release 1.11.1 (2017-02-20) + +- [JENKINS-40879](https://issues.jenkins-ci.org/browse/JENKINS-40879) - Locked + areas are executed multiple times in parallel + +## Release 1.11 (2016-12-19) + +- [JENKINS-34268](https://issues.jenkins-ci.org/browse/JENKINS-34268) - + lock multiple resources concurrently +- [JENKINS-34273](https://issues.jenkins-ci.org/browse/JENKINS-34273) - + add the number of resources to lock from a given label + +## Release 1.10 (2016-07-12) + +- [JENKINS-36479](https://issues.jenkins-ci.org/browse/JENKINS-36479) - + properly clean up resources locked by hard-killed or deleted while + in progress Pipeline builds. + +## Release 1.9 (2016-06-01) + +- Reserved resources parameter visibility in environment (related to + SECURITY-170) + +## Release 1.8 (2016-04-14) + +- Pipeline compatibility: lock step + +## Release 1.2 (2014-02-05) + +- Manual reservation/un-reservation of resources now require specific + permissions + +## Release 1.1 (2014-02-03) + +- Allow jobs to require a subset of specified resources (the number of required + resources is configurable) +- Allow manual reservation/un-reservation of resources + +## Release 1.0 (2013-12-12) + +- Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..4cd68725f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,88 @@ + +# Contributing + +If you want to contribute to this plugin, you probably will need a Jenkins plugin development +environment. This basically means a current version of Java (Java 11 should probably be okay for now) +and [Apache Maven]. See the [Jenkins Plugin Tutorial] for details. +You could also go the [GitPod](https://gitpod.io/#https://github.com/jenkinsci/lockable-resources-plugin) way. + +If you have the proper environment, typing: + +```sh +mvn verify +``` + +should create a plugin as `target/*.hpi`, which you can install in your Jenkins instance. Running + +```sh +mvn hpi:run +``` + +allows you to spin up a test Jenkins instance on [localhost] to test your +local changes before committing. + +[Apache Maven]: https://maven.apache.org/ +[Jenkins Plugin Tutorial]: https://jenkins.io/doc/developer/tutorial/prepare/ +[localhost]: http://localhost:8080/jenkins/ + +## Code Style + +This plugin tries to migrate to [Google Java Code Style], please try to adhere to that style +whenever adding new files or making big changes to existing files. If your IDE doesn't support +this style, you can use the [fmt-maven-plugin], like this: + +```sh + mvn fmt:format -DfilesNamePattern=ChangedFile\.java +``` + +to reformat Java code in the proper style. + +[Google Java Code Style]: https://google.github.io/styleguide/javaguide.html +[fmt-maven-plugin]: https://github.com/coveo/fmt-maven-plugin + +## Code coverage + +Test coverage is a percentage measure of the degree to which the source code of a program is executed when a test is run. A program with high test coverage has more of its source code executed during testing, which suggests it has a lower chance of containing undetected software bugs compared to a program with low test coverage. The best way to improve code coverage is writing of automated tests. + +To get local line-by-line coverage report execute this command + +```sh +mvn -P enable-jacoco clean verify jacoco:report +``` + +The report is then located in *target/site/jacoco/index.html*. + +## License + +The MIT License (MIT) + +- Copyright 2013-2015 6WIND +- Copyright 2016-2018 Antonio Muñiz +- Copyright 2019 TobiX +- Copyright 2017-2022 Jim Klimov + +See [LICENSE](LICENSE.txt) + +## Localization + +[![Crowdin](https://badges.crowdin.net/e/656dcffac5a09ad0fbdedcb430af1904/localized.svg)](https://jenkins.crowdin.com/lockable-resources-plugin) + +Internationalization documentation for Jelly, Java and Groovy can be found [here](https://www.jenkins.io/doc/developer/internationalization/). + +To translate this plugin we recommend to use [Crowdin](https://jenkins.crowdin.com/lockable-resources-plugin). + +Read on [how to use the crowdin web interface](https://www.jenkins.io/doc/developer/crowdin/) to translate plugins. + +When you want to help us, please create a new [feature request](https://github.com/jenkinsci/lockable-resources-plugin/issues/new?assignees=&labels=enhancement&template=2-feature-request.yml) with following content + +Title: +l10n: \ +Description +I would provide new (or update) translations for \ + +We will then add you to the Crowdin project. + +For short translations / updates we can also send you invitation (time limited) + +**Privacy policy notice** +When you start translating via Crowdin service, your browsers will send cookies to Crowdin so that Crowdin can identify translators contributing to the project. You might need to update the privacy policy to reflect this aspect of cookies usage. diff --git a/Jenkinsfile b/Jenkinsfile index a229fa517..7d8a99286 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1 +1,11 @@ -buildPlugin() +/* + See the documentation for more options: + https://github.com/jenkins-infra/pipeline-library/ +*/ +buildPlugin( + forkCount: '1C', // Run parallel tests on ci.jenkins.io for lower costs, faster feedback + useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests + configurations: [ + [platform: 'linux', jdk: 25], + [platform: 'windows', jdk: 21], +]) diff --git a/LICENSE.txt b/LICENSE.txt index 79e40d7ee..a73259d71 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,8 @@ The MIT License -Copyright (c) 2013, 6WIND S.A. All rights reserved. +Copyright 2013-2015, 6WIND S.A. All rights reserved. +Copyright 2016-2018, Antonio Muñiz +Copyright 2019, TobiX Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..5c71e74bf --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,81 @@ + + + +See [JENKINS-XXXXX](https://issues.jenkins.io/browse/JENKINS-XXXXX). + +See #XXXXX + + + + +### Testing done + + + +### Proposed upgrade guidelines + +N/A + +### Localizations + + + +- [ ] English +- [ ] German +- [ ] French +- [ ] Slovak +- [ ] Czech +- [ ] ... + +### Submitter checklist + +- [ ] The Jira / Github issue, if it exists, is well-described. +- [ ] The changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developers, depending on the change) and are in the imperative mood (see [examples](https://github.com/jenkins-infra/jenkins.io/blob/main/content/_data/changelogs/weekly.yml)). + - The changelog generator for plugins uses the **pull request title as the changelog entry**. + - Fill in the **Proposed upgrade guidelines** section only if there are breaking changes or changes that may require extra steps from users during the upgrade. +- [ ] There is automated testing or an explanation that explains why this change has no tests. +- [ ] New public functions for internal use only are annotated with `@NoExternalUse`. In case it is used by non java code the `Used by {@code .jelly}` Javadocs are annotated. + +- [ ] New or substantially changed JavaScript is not defined inline and does not call `eval` to ease the future introduction of Content Security Policy (CSP) directives (see [documentation](https://www.jenkins.io/doc/developer/security/csp/)). +- [ ] For dependency updates, there are links to external changelogs and, if possible, full differentials. +- [ ] For new APIs and extension points, there is a link to at least one consumer. +- [ ] Any localizations are transferred to *.properties files. +- [ ] Changes in the interface are documented also as [examples](src/doc/examples/readme.md). + +### Maintainer checklist + +Before the changes are marked as `ready-for-merge`: + +- [ ] There is at least one (1) approval for the pull request and no outstanding requests for change. +- [ ] Conversations in the pull request are over, or it is explicit that a reviewer is not blocking the change. +- [ ] Changelog entries in the **pull request title** and/or **Proposed changelog entries** are accurate, human-readable, and in the imperative mood. +- [ ] Proper changelog labels are set so that the changelog can be generated automatically. See also [release-drafter-labels](https://github.com/jenkinsci/.github/blob/ce466227c534c42820a597cb8e9cac2f2334920a/.github/release-drafter.yml#L9-L50). +- [ ] If the change needs additional upgrade steps from users, the `upgrade-guide-needed` label is set and there is a **Proposed upgrade guidelines** section in the pull request title (see [example](https://github.com/jenkinsci/jenkins/pull/4387)). +- [ ] java code changes are tested by automated test. diff --git a/README.md b/README.md index b8072fbda..1fa85e3cb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,418 @@ # Jenkins Lockable Resources Plugin -This plugins allows to define "lockable resources" in the global configuration. -These resources can then be "required" by jobs. If a job requires a resource -which is already locked, it will be put in queue until the resource is released. +[![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/lockable-resources.svg)](https://plugins.jenkins.io/lockable-resources) +[![GitHub release](https://img.shields.io/github/release/jenkinsci/lockable-resources-plugin.svg?label=release)](https://github.com/jenkinsci/lockable-resources-plugin/releases/latest) +[![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/lockable-resources.svg?color=blue)](https://plugins.jenkins.io/lockable-resources) +[![Build Status](https://ci.jenkins.io/buildStatus/icon?job=Plugins%2Flockable-resources-plugin%2Fmaster)](https://ci.jenkins.io/job/Plugins/job/lockable-resources-plugin/job/master/) +[![GitHub license](https://img.shields.io/github/license/jenkinsci/lockable-resources-plugin.svg)](https://github.com/jenkinsci/lockable-resources-plugin/blob/master/LICENSE.txt) +[![Maintenance](https://img.shields.io/maintenance/yes/2025.svg)](https://github.com/jenkinsci/lockable-resources-plugin) +[![Crowdin](https://badges.crowdin.net/e/656dcffac5a09ad0fbdedcb430af1904/localized.svg)](https://jenkins.crowdin.com/lockable-resources-plugin) +[![Join the chat at https://gitter.im/jenkinsci/lockable-resources](https://badges.gitter.im/jenkinsci/lockable-resources.svg)](https://gitter.im/jenkinsci/lockable-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +This plugin allows defining lockable resources (such as printers, phones, +computers, etc.) that can be used by builds. If a build requires a resource +which is already locked, it will wait for the resource to be free. + +---- +## Support + +“Open source” does not mean “includes free support” + +You can support the contributor and buy him a coffee. +[![coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/mpokornyetm) +Every second invested in an open-source project is a second you can't invest in your own family / friends / hobby. +That`s the reason, why supporting the contributors is so important. + +Thx very much for supporting us. + +---- + +## Usage + +### Adding lockable resources + +1. In *Manage Jenkins* > *Configure System* go to **Lockable Resources + Manager** +2. Select *Add Lockable Resource* + +Each lockable resource has the following properties: + +- **Name** - A mandatory name (not containing spaces!) for this particular resource, i.e. + `DK_Printer_ColorA3_2342` +- **Description** - Optional verbose description of this particular resource, + i.e. `Printers in the Danish Office` +- **Labels** - Optional space-delimited list of Labels (A label can not containing spaces) used to + identify a pool of resources. i.e. `DK_Printers_Office Country:DK device:printer`, + `DK_Printer_Production`, `DK_Printer_Engineering` +- **Reserved by** - Optional reserved / locked cause. If non-empty, + the resource will be unavailable for jobs. i.e. `All printers are currently not available due to maintenance.` + This option is still possible, but we recommend to use the page `/lockable-resources/` + +A resource is always the one thing that is locked (or free or reserved). +It exists once and has an unique name (if we take the hardware example, this may be `office_printer_14`). +Every resource can have multiple labels (the printer could be labeled `dot-matrix-printer`, `in-office-printer`, `a4-printer`, etc.). +All resources with the same label form a "pool", so if you try to lock an `a4-printer`, one of the resources with the label `a4-printer` will be locked when it is available. +If all resources with the label `a4-printer` are in use, your job waits until one is available. +This is similar to nodes and node labels. + +### Using a resource in a freestyle job + +When configuring the job, select **This build requires lockable resources**. +Please see the help item for each field for details. + +### Using a resource in a pipeline job + +When the `lock` step is used in a Pipeline, if the resource to be locked isn't +already defined in the Jenkins global configuration, an ephemeral resource is +used: These resources only exist as long as any running build is referencing +them. + +Examples: + +#### Acquire lock + +Example for scripted pipeline: + +```groovy +echo 'Starting' +lock('my-resource-name') { + echo 'Do something here that requires unique access to the resource' + // any other build will wait until the one locking the resource leaves this block +} +echo 'Finish' + +``` + +Example for declarative pipeline: + +```groovy +pipeline { + agent any + + stages { + stage("Build") { + steps { + lock(label: 'printer', quantity: 1, resource : null) { + echo 'printer locked' + } + } + } + } +} +``` + +Setting `quantity` to `null`, `0` or a smaller number, all available resources of that label are locked at once. + +#### Take first position in queue + +```groovy +lock(resource: 'staging-server', inversePrecedence: true) { + node { + servers.deploy 'staging' + } + input message: "Does ${jettyUrl}staging/ look good?" +} +``` + +> It is not allowed to mixed **inversePrecedence** and **priority**. + +start time | job | resource | inversePrecedence +------ |--- |--- |--- +00:01 | j1 | resource1 | false +00:02 | j2 | resource1 | false +00:03 | j3 | resource1 | true +00:04 | j4 | resource1 | false +00:05 | j5 | resource1 | true +00:06 | j6 | resource1 | false + +Resulting lock order: j1 -> j5 -> j3 -> j2 -> j4 -> j6 + +#### lock (queue) priority + +```groovy +lock(resource: 'staging-server', priority: 10) { + node { + servers.deploy 'staging' + } + input message: "Does ${jettyUrl}staging/ look good?" +} +``` + + start time | job | resource | priority + ------ |--- |--- |--- + 00:01 | j1 | resource1 | 0 + 00:02 | j2 | resource1 | + 00:03 | j3 | resource1 | -1 + 00:04 | j4 | resource1 | 10 + 00:05 | j5 | resource1 | -2 + 00:06 | j6 | resource1 | 100 + + Resulting lock order: j1 -> j6 -> j4 -> j2 -> j3 -> j5 + +#### Resolve a variable configured with the resource name and properties + +```groovy +lock(label: 'some_resource', variable: 'LOCKED_RESOURCE') { + echo env.LOCKED_RESOURCE + echo env.LOCKED_RESOURCE0_PROP_ABC +} +``` + +When multiple locks are acquired, each will be assigned to a numbered variable: + +```groovy +lock(label: 'some_resource', variable: 'LOCKED_RESOURCE', quantity: 2) { + // comma separated names of all acquired locks + echo env.LOCKED_RESOURCE + + // first lock + echo env.LOCKED_RESOURCE0 + echo env.LOCKED_RESOURCE0_PROP_ABC + + // second lock + echo env.LOCKED_RESOURCE1 + echo env.LOCKED_RESOURCE1_PROP_ABC +} +``` + +#### Skip executing the block if there is a queue + +```groovy +lock(resource: 'some_resource', skipIfLocked: true) { + echo 'Do something now or never!' +} +``` + +Detailed documentation can be found as part of the +[Pipeline Steps](https://jenkins.io/doc/pipeline/steps/lockable-resources/) +documentation. + +### Jenkins label parser allows sophisticated filtering + +The plugin uses the Jenkins-internal label parser for filtering lockable resources. A full list of supported operators and syntax examples can be found in the [official documentation](https://www.jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#node-allocate-node). + +```groovy +lock(label: 'labelA && labelB', variable : 'someVar') { + echo 'labelA && labelB acquired by: ' + env.someVar; +} + +lock(label: 'labelA || labelB', variable : 'someVar') { + echo 'labelA || labelB acquired by: ' + env.someVar; +} + +lock(label: 'labelA || labelB || labelC', variable : 'someVar', quantity : 100) { + echo 'labelA || labelB || labelC acquired by: ' + env.someVar; +} +``` + +#### Multiple resource lock + +```groovy +lock(label: 'label1', extra: [[resource: 'resource1']]) { + echo 'Do something now or never!' +} +echo 'Finish' +``` + +```groovy +lock( + variable: 'var', + extra: [ + [resource: 'resource4'], + [resource: 'resource2'], + [label: 'label1', quantity: 2] + ] +) { + def lockedResources = env.var.split(',').sort() + echo "Resources locked: ${lockedResources}" +} +echo 'Finish' +``` + +More examples are [here](src/doc/examples/readme.md). + +---- + +## Node mirroring + +Lockable resources plugin allow to mirror nodes (agents) into lockable resources. This eliminate effort by re-creating resources on every node change. + +That means when you create new node, it will be also created new lockable-resource with the same name. When the node has been deleted, lockable-resource will be deleted too. + +Following properties are mirrored: + +- name. +- labels. Please note, that labels still contains node-name self. +- description. + +To allow this behavior start jenkins with option `-Dorg.jenkins.plugins.lockableresources.ENABLE_NODE_MIRROR=true` or run this groovy code. + +```groovy +System.setProperty("org.jenkins.plugins.lockableresources.ENABLE_NODE_MIRROR", "true"); +``` + +> *Note:* When the node has been deleted, during the lockable-resource is locked / reserved / queued, then the lockable-resource will be NOT deleted. + +---- + +## Improve performance + +To be safe thread over all jobs and resources, need to be all operations synchronized. +This might lead to slow down your jobs. The jenkins self, has low CPU usage, but your jobs are very slow. Why? + +The most time are spend to write the lockable-resources states into local file system. This is important to get the correct state after Jenkins reboots. + +To eliminate this saving time has been added a property *DISABLE_SAVE*. + ++ The best way is to use it with JCaC plugin. So you are sure, you have still correct +resources on Jenkins start. ++ When you set pipeline durability level to *PERFORMANCE_OPTIMIZED*, it makes also sense to set this property to true. + +> *Note:* Keep in mind, that you will lost all your manual changes! + +> *Note:* This option is experimental. It has been tested in many scenarios, but no body know. + +To allow this behavior start jenkins with option `-Dorg.jenkins.plugins.lockableresources.DISABLE_SAVE=true` or run this groovy code. + +```groovy +System.setProperty("org.jenkins.plugins.lockableresources.DISABLE_SAVE", "true"); +``` + +## Detailed lock cause + +Tle plugin step lock() will inform you in the build log detailed block cause. The size of cause depends on count of ordered resources and size of current queue. To eliminate big unreadable logs we limited the size. To see all cause change the properties as follow: + +```groovy +System.setProperty("org.jenkins.plugins.lockableresources.PRINT_BLOCKED_RESOURCE", "-1"); +System.setProperty("org.jenkins.plugins.lockableresources.PRINT_QUEUE_INFO", "-1"); +``` + +*PRINT_BLOCKED_RESOURCE* means how many of ordered resources are printed. Per default 2. +*PRINT_QUEUE_INFO* how many queue items are printed. Per default 2. + + 0 means disabled + -1 means all / unlimited. + +## Configuration as Code + +This plugin can be configured via +[Configuration-as-Code](https://github.com/jenkinsci/configuration-as-code-plugin). + +### Example configuration + +```yml +unclassified: + lockableResourcesManager: + declaredResources: + - name: "S7_1200_1" + description: "S7 PLC model 1200" + labels: "plc:S7 model:1200" + - name: "S7_1200_2" + labels: "plc:S7 model:1200" + - name: "Resource-with-properties" + properties: + - name: "Property-A" + value: "Value" +``` + +Properties *description*, *labels* and *properties* are optional. + +Fields like *reservedBy*, *reservedTimestamp* or *note* are not supported, they will be ignored. + +---- + +## lockable-resources overview + +The page `/lockable-resources/` provides an overview over all lockable-resources. + +### Resources + +Provides an status overview over all resources and actions to change resource status. + +Name | Permission | Description +-----|------------|------------ +Reserve | RESERVE | Reserves an available resource for currently logged user indefinitely (until that person, or some explicit scripted action, decides to release the resource). +Unreserve | RESERVE | Un-reserves a resource that may be reserved by some person already. The user can unreserve only own resource. Administrator can unreserve any resource. +Unlock | UNLOCK | Unlocks a resource that may be or not be locked by some job (or reserved by some user) already. +Steal lock | STEAL | Reserves a resource that may be or not be locked by some job (or reserved by some user) already. Giving it away to currently logged user indefinitely (until that person, or some explicit scripted action, later decides to release the resource). +Reassign | STEAL | Reserves a resource that may be or not be reserved by some person already. Giving it away to currently logged user indefinitely (until that person, or some explicit scripted action, decides to release the resource). +Reset | UNLOCK | Reset a resource that may be reserved, locked or queued. +Note | RESERVE | Add or edit resource note. + +### Labels + +Provides an overview over all lockable-resources labels. + +> *Note:* Please keep in mind, that lockable-resource-label is not the same as node-label! + +### Queue + +Provides an overview over currently queued requests. +A request is queued by the pipeline step `lock()`. When the requested resource(s) is currently in use (not free), then any new request for this resource will be added into the queue. + +A resource may be requested by: + +- name, such as in `lock('ABC') { ... }` +- label, such as in `lock(label : 'COFFEE_MACHINE')` + +> *Note:* Please keep in mind that groovy expressions are currently supported only in free-style jobs. Free-style jobs do not update this queue and therefore can not be shown in this view. + +> *Note:* An empty value in the column 'Requested at' means that this build has been started in an older plugin version - [1117.v157231b_03882](https://github.com/jenkinsci/lockable-resources-plugin/releases/tag/1117.v157231b_03882) and early. In this case we cannot recognize the timestamp. + +---- + +## Upgrading from 1102.vde5663d777cf + +Due an [issue](https://github.com/jenkinsci/lockable-resources-plugin/issues/434) **is not possible anymore to read resource-labels** from the config file org.jenkins.plugins.lockableresources.LockableResourcesManager.xml, **which is generated in the release** [1102.vde5663d777cf](https://github.com/jenkinsci/lockable-resources-plugin/releases/tag/1102.vde5663d777cf) + +This issue does not **effect** instances configured by [Configuration-as-Code](https://github.com/jenkinsci/configuration-as-code-plugin) plugin. + +A possible solution is to remove the `` tags from your `org.jenkins.plugins.lockableresources.LockableResourcesManager.xml` config file manually, before you upgrade to new version (Keep in mind that a backup is still good idea). + +Example: + +change this one + +```xml + + tests-integration-installation + +``` + +to + +```xml + + tests-integration-installation + +``` + +---- + +## Changelog + +- See [GitHub Releases](https://github.com/jenkinsci/lockable-resources-plugin/releases) + for recent versions. +- See the [old changelog](CHANGELOG.old.md) for versions 2.5 and older. + +---- + +## Report an Issue + +Please report issues and enhancements through the [Jenkins issue tracker in GitHub](https://github.com/jenkinsci/lockable-resources-plugin/issues/new/choose) + +---- + +## Contributing + +Contributions are welcome, please +refer to the separate [CONTRIBUTING](CONTRIBUTING.md) document +for details on how to proceed! +Join [Gitter channel](https://gitter.im/jenkinsci/lockable-resources) to discuss your ideas with the community. + +---- + +## License + +All source code is licensed under the MIT license. +See [LICENSE](LICENSE.txt) diff --git a/check_licenses.py b/check_licenses.py deleted file mode 100644 index f802aa2f7..000000000 --- a/check_licenses.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/env python - -import os, sys, glob - -JAVA_LICENSE = """\ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (c) 2013, 6WIND S.A. All rights reserved. * - * * - * This file is part of the Jenkins Lockable Resources Plugin and is * - * published under the MIT license. * - * * - * See the "LICENSE.txt" file for more information. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -""" -XML_LICENSE = """\ - -""" - -LICENSED_FILES = { - '.java': JAVA_LICENSE, - '.jelly': XML_LICENSE, - '.xml': XML_LICENSE, -} - -def check_file(file, do_modify): - _, ext = os.path.splitext(file) - missing = 0 - inserted = 0 - if ext in LICENSED_FILES.keys(): - license = LICENSED_FILES[ext] - - with open(file, 'r') as fd: - buffer = fd.read() - - if license not in buffer: - missing = 1 - if do_modify: - with open(file, 'w') as fd: - if buffer.startswith('#!') or buffer.startswith(' 1 and sys.argv[1] == "--modify" - - missing = 0 - inserted = 0 - - for pom in glob.glob('pom.xml'): - miss, ins = check_file(pom, do_modify) - missing += miss - inserted += ins - for source_folder in glob.glob('src'): - for root, dirs, files in os.walk(source_folder): - for file in files: - miss, ins = check_file(os.path.join(root, file), do_modify) - missing += miss - inserted += ins - - print - print missing, "license headers missing.", inserted, "inserted" - - -if __name__ == "__main__": - main() - diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..cb7b6a84b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,8 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN +files: + - source: /src/main/resources/org/jenkins/plugins/lockableresources/**/*.properties + ignore_settings: 1 + ignore: + - /src/main/resources/org/jenkins/plugins/lockableresources/**/%file_name%_%two_letters_code%.properties + translation: /src/main/resources/org/jenkins/plugins/lockableresources/**/%file_name%_%two_letters_code%.properties diff --git a/pom.xml b/pom.xml index b477f183b..9eb6a98de 100644 --- a/pom.xml +++ b/pom.xml @@ -1,175 +1,186 @@ - + - 4.0.0 - - org.jenkins-ci.plugins - plugin - 2.24 - - + 4.0.0 - org.6wind.jenkins - lockable-resources - 2.6-SNAPSHOT - hpi - Lockable Resources plugin - - This plugin allows to define lockable resources (such as printers, phones, - computers) that can be used by builds. If a build requires an external - resource which is already locked, it will wait for the resource to be free. - - https://wiki.jenkins-ci.org/display/JENKINS/Lockable+Resources+Plugin + + org.jenkins-ci.plugins + plugin + 5.2102.v5f5fe09fccf1 + + - - - 1.14 - 1.609.1 - - false - + org.6wind.jenkins + lockable-resources + ${changelist} + hpi - - - MIT - http://www.opensource.org/licenses/mit-license.php - - + Lockable Resources plugin + This plugin allows to define lockable resources (such as printers, phones, + computers) that can be used by builds. If a build requires an external + resource which is already locked, it will wait for the resource to be free. + https://github.com/jenkinsci/${project.artifactId}-plugin + 2013 + + + MIT + https://opensource.org/licenses/MIT + + - - - robin-jarry - Robin Jarry - robin.jarry@6wind.com - - developer - maintainer - - CET - - - amuniz - Antonio Muñiz - amuniz@cloudbees.com - - developer - maintainer - - CET - - + + + TobiX + Tobias Gruetzmacher + tobias-git@23.gs + + + amuniz + Antonio Muñiz + amuniz@cloudbees.com + + + mPokornyETM + Martin Pokorny + martin.pokorny@etm.at + + - - - org.jenkins-ci.plugins - mailer - 1.5 - - - org.jenkins-ci.plugins.workflow - workflow-step-api - ${workflow.version} - - - org.jenkins-ci.plugins.workflow - workflow-support - ${workflow.version} - - - org.jenkins-ci.plugins - matrix-project - 1.4 - - - org.jenkins-ci.plugins - script-security - 1.26 - - - com.infradna.tool - bridge-method-annotation - 1.14 - true - + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + ${scmTag} + https://github.com/${gitHubRepo} + - - - org.jenkins-ci.plugins.workflow - workflow-aggregator - ${workflow.version} - test - - - org.jenkins-ci.plugins.workflow - workflow-support - ${workflow.version} - tests - test - - - org.jenkins-ci.modules - sshd - 1.6 - test - - - org.jenkins-ci.plugins - junit - 1.13 - test - - + + 999999-SNAPSHOT + + 2.479 + ${jenkins.baseline}.3 + jenkinsci/${project.artifactId}-plugin + Max + Low + false + false + - - - - org.jenkins-ci.tools - maven-hpi-plugin - - - FINE - - 2.0 - - - - maven-release-plugin - 2.5.2 - - false - - - - + + + + io.jenkins.tools.bom + bom-${jenkins.baseline}.x + 5054.v620b_5d2b_d5e6 + pom + import + + + - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - + + + io.jenkins.plugins + caffeine-api + + + io.jenkins.plugins + data-tables-api + + + org.jenkins-ci.plugins + mailer + + + org.jenkins-ci.plugins + matrix-project + true + + + org.jenkins-ci.plugins + script-security + + + org.jenkins-ci.plugins + structs + + + org.jenkins-ci.plugins + variant + + + org.jenkins-ci.plugins.workflow + workflow-support + + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + + + org.jenkins-ci.plugins.workflow + workflow-basic-steps + test + + + org.jenkins-ci.plugins.workflow + workflow-cps + test + + + org.jenkins-ci.plugins.workflow + workflow-job + test + - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - + + + org.jenkins-ci.plugins.workflow + workflow-support + tests + test + + + org.jenkinsci.plugins + pipeline-model-definition + test + + + org.mockito + mockito-junit-jupiter + test + + - - scm:git:https://github.com/jenkinsci/lockable-resources-plugin.git - scm:git:git@github.com:jenkinsci/lockable-resources-plugin.git - https://github.com/jenkinsci/lockable-resources-plugin - HEAD - + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.jenkins-ci.tools + maven-hpi-plugin + + + FINE + + 2.0 + org.jenkins.plugins.lockableresources + + + + diff --git a/src/doc/examples/lock-nodes.md b/src/doc/examples/lock-nodes.md new file mode 100644 index 000000000..4394864b8 --- /dev/null +++ b/src/doc/examples/lock-nodes.md @@ -0,0 +1,185 @@ +# Examples + +## Node dependent resources + +Locking a resource that depends on a specific node can be very helpful in many cases. +That means a job must pick a target node that has the requested resource available. + +```groovy +// allocate node +node('some-build-node') { + // Lock resource named *whatever-resource-some-build-node* + lock("whatever-resource-${env.NODE_NAME}") { + echo "Running on node ${env.NODE_NAME} with locked resource ${env.LOCKED_RESOURCE}" + } +} +``` + +But much more useful is lock node first. + +```groovy +// Lock resource named *some-build-node* +lock('some-build-node') { + // allocate node + node(env.LOCKED_RESOURCE) { + + echo "I am on node ${env.NODE_NAME} and locked resource ${env.LOCKED_RESOURCE}" + } +} +``` + +Let explain in more complex use case. + +*Request:* +Our job tests server-client integration. That means we need 2 nodes (1 server and 1 client). +On every node must be test sources up-to-date. +Tests are running only on client side. + +*Solution:* +Create 2 nodes: + +- node-server +- node-client + +and execute it parallel like this: + +```groovy +Map stages = [:]; +stages['server'] = { + node('node-server') { + prepareTests() + startServer() + } +} +stages['client'] = { + node('node-client') { + prepareTests() + startClientTest() + } +} +// execute all prepare stages synchronous +parallel stages + +// Prepare tests on node +void prepareTests() { + checkout([$class: 'GitSCM', + branches: [[name: 'master']] + ]) +} +// Start server +void startServer() { + echo 'Server will be started in few seconds' + sh 'mvn clean hpi:run' + echo 'Server is done' +} +// Start client +void startClientTest() { + sleep 20 + sh 'mvn clean verify' +} +``` + +It looks pretty fine and easy, but !!!. + +Executing all steps parallel might leads to timing issues, because checkout on server-node might takes much longer then on client-node. This is serious issue because the client starts before the server and can not connect to server. + +The solution is to synchronized parallel stages like this. + +```groovy +Map prepareStages = [:]; +prepareStages['server'] = { + node('node-server') { + prepareTests() + } +} +prepareStages['client'] = { + node('node-client') { + startServer() + } +} +// execute all prepare stages synchronous +parallel prepareStages + +Map testStages = [:] +testStages['server'] = { + node('node-server') { + prepareTests(); + } +} +testStages['client'] = { + node('node-client') { + sleep 20 + startClientTest(); + } +} + +// execute all test stages at the same time +testStages.failFast = true +parallel testStages + +... +``` + +Ok we solve the timing issue, but what is wrong here? + +When the step *parallel prepareStages* is done then are on both nodes executors free. At this moment +it might happen, that some other job allocate one of the nodes. This will leads to more side effects, like: + +- no body can grant, that currently checked out workspace will be untouched +- no body can grant how long will be the node allocated +- ... and many others + +Instead, we lock both nodes with a single call to `lock`. + +Create two resources: +name | Labels | +---------------|--------| +nodes-server-1 | server-node | +nodes-client-1 | client-node | + + +```groovy +lock(variable: 'locked_resources', + extra: [ + [label: 'server-node', quantity: 1], + [label: 'client-node', quantity: 1] + ) { + final String serverNodeName = env.LOCKED_RESOURCE0; + final String clientNodeName = env.LOCKED_RESOURCE1; + Map prepareStages = [:]; + prepareStages['server'] = { + node(serverNodeName) { + prepareTests() + } + } + prepareStages['client'] = { + node(clientNodeName) { + startServer() + } + } + // execute all prepare stages synchronous + parallel prepareStages + + Map testStages = [:] + testStages['server'] = { + node(serverNodeName) { + prepareTests(); + } + } + testStages['client'] = { + node(clientNodeName) { + sleep 20 + startClientTest(); + } + } + + // execute all test stages at the same time + testStages.failFast = true + parallel testStages + +} + +... +``` + +Keep in mind, that `lock()` only helps when locks are consistently requested for resources. diff --git a/src/doc/examples/locking-multiple-stages-in-declarative-pipeline.md b/src/doc/examples/locking-multiple-stages-in-declarative-pipeline.md new file mode 100644 index 000000000..2c36706f5 --- /dev/null +++ b/src/doc/examples/locking-multiple-stages-in-declarative-pipeline.md @@ -0,0 +1,33 @@ +# Locking multiple stages in declarative pipeline + +You can lock the entire job in the options block of the pipeline: + + +```groovy +pipeline { +options { + lock 'lockable-resource' + } + + agent any + + stages { + stage('Build') { + steps { + sh 'make' + } + } + stage('Test'){ + steps { + sh 'make check' + junit 'reports/**/*.xml' + } + } + stage('Deploy') { + steps { + sh 'make publish' + } + } + } +} +``` diff --git a/src/doc/examples/locking-random-free-resource.md b/src/doc/examples/locking-random-free-resource.md new file mode 100644 index 000000000..c98636bbd --- /dev/null +++ b/src/doc/examples/locking-random-free-resource.md @@ -0,0 +1,36 @@ +## Locking a random free resource + +In same cases, you want a random resource to be locked instead of always choosing the first one. For example, +if resources like accounts could get rate limited if they are used too often by your pipelines. + +With the following resources created: + +| Name | Label | +|-----------|---------| +| account_1 | account | +| account_2 | account | + +You can pick a single random available resource like in the following declarative pipeline: + +```groovy +pipeline { + agent any + stages { + stage("Build") { + steps { + lock(label: "account", resourceSelectStrategy: 'random', resource: null, quantity: 1, variable: "account") { + echo "Using account " + env.account + // do your thing using the resource + } + } + } + } +} +``` + +The `quantity` can be changed to lock any amount of available resources with the given label. Not specifying the +quantity will lock all resources, but still randomize the order of resources in the numbered environment variable. + +Not specifying `resourceSelectStrategy`, will fall back to the default behaviour of locking resources according to +their order in the lockable resources list. You can also explicitly configure the default strategy +with `resourceSelectStrategy: 'sequential'`. diff --git a/src/doc/examples/readme.md b/src/doc/examples/readme.md new file mode 100644 index 000000000..e348b0ded --- /dev/null +++ b/src/doc/examples/readme.md @@ -0,0 +1,11 @@ +# Examples + +Examples of lockable resources include: + +If you have an example to share, please create a [new documentation issue](https://github.com/jenkinsci/lockable-resources-plugin/issues/new?assignees=&labels=documentation&template=3-documentation.yml) and provide additional examples as a [pull request](https://github.com/jenkinsci/lockable-resources-plugin/pulls) to the repository. +If you have a question, please open a [GitHub issue](https://github.com/jenkinsci/lockable-resources-plugin/issues/new/choose) with your question. + +- [Node depended resources](lock-nodes.md) +- [Locking multiple stages in declarative pipeline](locking-multiple-stages-in-declarative-pipeline.md) +- [Locking a random free resource](locking-random-free-resource.md) +- [Scripted vs declarative pipeline](scripted-vs-declarative-pipeline.md) diff --git a/src/doc/examples/scripted-vs-declarative-pipeline.md b/src/doc/examples/scripted-vs-declarative-pipeline.md new file mode 100644 index 000000000..a28e8be01 --- /dev/null +++ b/src/doc/examples/scripted-vs-declarative-pipeline.md @@ -0,0 +1,39 @@ +# Scripted vs declarative pipeline + +Due an historical reason is not possible to use exact same syntax in the declarative and scripted pipeline. +In declarative pipeline you must so far set **resource : null**. + +## Declarative + +``` groovy +pipeline { + agent any + + stages { + stage("Build") { + steps { + lock(label: 'printer', quantity: 1, resource : null) { + echo 'printer locked' + } + } + } + } +} +``` + +## Scripted + +``` groovy +node() { + stage("Build") { + lock(label: 'printer', quantity: 1) { + echo 'printer locked' + } + } +} +``` + +## Pitfalls + +Setting `quantity` to `null`, `0` or a smaller number, all available resources of that label are locked at once. +See [#198 - Lock All resources by setting quantity to 0 is not documented](https://github.com/jenkinsci/lockable-resources-plugin/issues/198). diff --git a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java index 12b7af7c4..d5bdd48e8 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java @@ -11,38 +11,54 @@ import hudson.init.InitMilestone; import hudson.init.Initializer; - import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; - -import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; -import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct; -import org.jenkins.plugins.lockableresources.LockableResource; -import org.jenkins.plugins.lockableresources.LockableResourcesManager; - import java.util.logging.Level; import java.util.logging.Logger; +import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; +import org.jenkinsci.plugins.workflow.steps.StepContext; +/** + * This class migrates "active" queuedContexts from LockableResource to LockableResourcesManager + * + * @deprecated Migration code for field introduced in 1.8 (since 1.11) + */ +@Deprecated +@ExcludeFromJacocoGeneratedReport public final class BackwardCompatibility { - private static final Logger LOG = Logger.getLogger(BackwardCompatibility.class.getName()); + private static final Logger LOG = Logger.getLogger(BackwardCompatibility.class.getName()); + + private BackwardCompatibility() {} - @Initializer(after = InitMilestone.JOB_LOADED) - public static void compatibilityMigration() { - LOG.log(Level.FINE, "lockable-resource-plugin compatibility migration task run"); - List resources = LockableResourcesManager.get().getResources(); - for (LockableResource resource : resources) { - List queuedContexts = resource.getQueuedContexts(); - if (queuedContexts.size() > 0) { - for (StepContext queuedContext : queuedContexts) { - List resourcesNames = new ArrayList(); - resourcesNames.add(resource.getName()); - LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resourcesNames, "", 0); - LockableResourcesManager.get().queueContext(queuedContext, Arrays.asList(resourceHolder), resource.getName()); - } - queuedContexts.clear(); - } - } - } -} \ No newline at end of file + @Initializer(after = InitMilestone.JOB_LOADED) + public static void compatibilityMigration() { + LockableResourcesManager lrm = LockableResourcesManager.get(); + synchronized (lrm.syncResources) { + List resources = lrm.getResources(); + LOG.log( + Level.FINE, + "lockable-resources-plugin compatibility migration task run for " + resources.size() + + " resources"); + for (LockableResource resource : resources) { + List queuedContexts = resource.getQueuedContexts(); + if (!queuedContexts.isEmpty()) { + for (StepContext queuedContext : queuedContexts) { + List resourcesNames = new ArrayList<>(); + resourcesNames.add(resource.getName()); + LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resourcesNames, "", 0); + LockableResourcesManager.get() + .queueContext( + queuedContext, + Collections.singletonList(resourceHolder), + resource.getName(), + null, + false, + 0); + } + queuedContexts.clear(); + } + } + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/ExcludeFromJacocoGeneratedReport.java b/src/main/java/org/jenkins/plugins/lockableresources/ExcludeFromJacocoGeneratedReport.java new file mode 100644 index 000000000..d776cc4ee --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/ExcludeFromJacocoGeneratedReport.java @@ -0,0 +1,10 @@ +package org.jenkins.plugins.lockableresources; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR}) +public @interface ExcludeFromJacocoGeneratedReport {} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/FreeDeadJobs.java b/src/main/java/org/jenkins/plugins/lockableresources/FreeDeadJobs.java new file mode 100644 index 000000000..68d761c34 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/FreeDeadJobs.java @@ -0,0 +1,53 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Copyright (c) 2016, Florian Hug. All rights reserved. * + * * + * This file is part of the Jenkins Lockable Resources Plugin and is * + * published under the MIT license. * + * * + * See the "LICENSE.txt" file for more information. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +package org.jenkins.plugins.lockableresources; + +import hudson.init.InitMilestone; +import hudson.init.Initializer; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Sometimes after re-starts (jenkins crashed or what ever) are resources still locked by build, but + * the build is no more running. This script will 'unlock' all resource assigned to dead builds + */ +@ExcludeFromJacocoGeneratedReport +public final class FreeDeadJobs { + private static final Logger LOG = Logger.getLogger(FreeDeadJobs.class.getName()); + + private FreeDeadJobs() {} + + @Initializer(after = InitMilestone.JOB_LOADED) + public static void freePostMortemResources() { + + LockableResourcesManager lrm = LockableResourcesManager.get(); + synchronized (lrm.syncResources) { + List orphan = new ArrayList<>(); + LOG.log(Level.FINE, "lockable-resources-plugin free post mortem task run"); + for (LockableResource resource : lrm.getResources()) { + if (resource.getBuild() != null && !resource.getBuild().isInProgress()) { + orphan.add(resource); + } + } + + for (LockableResource resource : orphan) { + LOG.log( + Level.INFO, + "lockable-resources-plugin reset resource " + + resource.getName() + + " due post mortem job: " + + resource.getBuildName()); + resource.recycle(); + } + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java index 739a0b043..13da11aeb 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java @@ -1,145 +1,239 @@ package org.jenkins.plugins.lockableresources; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; +import hudson.model.AutoCompletionCandidates; +import hudson.model.Item; +import hudson.model.TaskListener; +import hudson.util.FormValidation; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; - -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import java.util.Locale; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +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.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; - -import hudson.Extension; -import hudson.model.AutoCompletionCandidates; -import hudson.util.FormValidation; -import hudson.Util; - -import edu.umd.cs.findbugs.annotations.Nullable; -import edu.umd.cs.findbugs.annotations.CheckForNull; - -public class LockStep extends AbstractStepImpl implements Serializable { - - @CheckForNull - public String resource = null; - - @CheckForNull - public String label = null; - - public int quantity = 0; - - /** name of environment variable to store locked resources in */ - @CheckForNull - public String variable = null; - - public boolean inversePrecedence = false; - - @CheckForNull - public List extra = null; - - // it should be LockStep() - without params. But keeping this for backward compatibility - // so `lock('resource1')` still works and `lock(label: 'label1', quantity: 3)` works too (resource is not required) - @DataBoundConstructor - public LockStep(String resource) { - if (resource != null && !resource.isEmpty()) { - this.resource = resource; - } - } - - @DataBoundSetter - public void setInversePrecedence(boolean inversePrecedence) { - this.inversePrecedence = inversePrecedence; - } - - @DataBoundSetter - public void setLabel(String label) { - if (label != null && !label.isEmpty()) { - this.label = label; - } - } - - @DataBoundSetter - public void setVariable(String variable) { - if (variable != null && !variable.isEmpty()) { - this.variable = variable; - } - } - - @DataBoundSetter - public void setQuantity(int quantity) { - this.quantity = quantity; - } - - @DataBoundSetter - public void setExtra(List extra) { - this.extra = extra; - } - - @Extension - public static final class DescriptorImpl extends AbstractStepDescriptorImpl { - - public DescriptorImpl() { - super(LockStepExecution.class); - } - - @Override - public String getFunctionName() { - return "lock"; - } - - @Override - public String getDisplayName() { - return "Lock shared resource"; - } - - @Override - public boolean takesImplicitBlockArgument() { - return true; - } - - public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) { - return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value); - } - - public static FormValidation doCheckLabel(@QueryParameter String value, @QueryParameter String resource) { - return LockStepResource.DescriptorImpl.doCheckLabel(value, resource); - } - - public static FormValidation doCheckResource(@QueryParameter String value, @QueryParameter String label) { - return LockStepResource.DescriptorImpl.doCheckLabel(label, value); - } - } - - public String toString() { - if (extra != null && !extra.isEmpty()) { - StringBuilder builder = new StringBuilder(); - for (LockStepResource resource : getResources()) { - builder.append("{" + resource.toString() + "},"); - } - return builder.toString(); - } else { - return LockStepResource.toString(resource, label, quantity); - } - } - - /** - * Label and resource are mutual exclusive. - */ - public void validate() throws Exception { - LockStepResource.validate(resource, label, quantity); - } - - public List getResources() { - List resources = new ArrayList<>(); - resources.add(new LockStepResource(resource, label, quantity)); - - if (extra != null) { - resources.addAll(extra); - } - return resources; - } - - private static final long serialVersionUID = 1L; - +import org.kohsuke.stapler.interceptor.RequirePOST; + +public class LockStep extends Step implements Serializable { + private static final Logger LOG = Logger.getLogger(LockStep.class.getName()); + + private static final long serialVersionUID = -953609907239674360L; + + @CheckForNull + public String resource = null; + + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String label = null; + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public int quantity = 0; + + /** name of environment variable to store locked resources in */ + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String variable = null; + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public boolean inversePrecedence = false; + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String resourceSelectStrategy = ResourceSelectStrategy.SEQUENTIAL.name(); + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public boolean skipIfLocked = false; + + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public List extra = null; + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public int priority = 0; + + // it should be LockStep() - without params. But keeping this for backward compatibility + // so `lock('resource1')` still works and `lock(label: 'label1', quantity: 3)` works too (resource + // is not required) + @DataBoundConstructor + public LockStep(@Nullable String resource) { + if (resource != null && !resource.trim().isEmpty()) { + if (!resource.equals(resource.trim())) { + LOG.warning("The provided 'resource' should not start or end with spaces."); + } + this.resource = resource.trim(); + } + } + + @DataBoundSetter + public void setInversePrecedence(boolean inversePrecedence) { + this.inversePrecedence = inversePrecedence; + } + + @DataBoundSetter + public void setResourceSelectStrategy(String resourceSelectStrategy) { + this.resourceSelectStrategy = resourceSelectStrategy; + } + + @DataBoundSetter + public void setSkipIfLocked(boolean skipIfLocked) { + this.skipIfLocked = skipIfLocked; + } + + @DataBoundSetter + public void setLabel(String label) { + if (label != null && !label.trim().isEmpty()) { + if (!label.equals(label.trim())) { + LOG.warning("The provided 'label' should not start or end with spaces."); + } + this.label = label.trim(); + } + } + + @DataBoundSetter + public void setVariable(String variable) { + if (variable != null && !variable.trim().isEmpty()) { + if (!variable.equals(variable.trim())) { + LOG.warning("The provided 'variable' should not start or end with spaces."); + } + this.variable = variable.trim(); + } + } + + @DataBoundSetter + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + @DataBoundSetter + public void setPriority(int priority) { + this.priority = priority; + } + + @DataBoundSetter + public void setExtra(@CheckForNull List extra) { + this.extra = extra; + } + + @Extension + public static final class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "lock"; + } + + @NonNull + @Override + public String getDisplayName() { + return Messages.LockStep_displayName(); + } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } + + @RequirePOST + public AutoCompletionCandidates doAutoCompleteResource( + @QueryParameter String value, @AncestorInPath Item item) { + return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value, item); + } + + @RequirePOST + public static FormValidation doCheckLabel( + @QueryParameter String value, @QueryParameter String resource, @AncestorInPath Item item) { + return LockStepResource.DescriptorImpl.doCheckLabel(value, resource, item); + } + + @RequirePOST + public static FormValidation doCheckResource( + @QueryParameter String value, @QueryParameter String label, @AncestorInPath Item item) { + return LockStepResource.DescriptorImpl.doCheckLabel(label, value, item); + } + + @RequirePOST + public static FormValidation doCheckResourceSelectStrategy( + @QueryParameter String resourceSelectStrategy, @AncestorInPath Item item) { + // check permission, security first + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + if (resourceSelectStrategy != null && !resourceSelectStrategy.isEmpty()) { + try { + ResourceSelectStrategy.valueOf(resourceSelectStrategy.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + return FormValidation.error(Messages.error_invalidResourceSelectionStrategy( + resourceSelectStrategy, + Arrays.stream(ResourceSelectStrategy.values()) + .map(Enum::toString) + .map(strategy -> strategy.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.joining(", ")))); + } + } + return FormValidation.ok(); + } + + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + } + + @Override + public String toString() { + if (extra != null && !extra.isEmpty()) { + return getResources().stream() + .map(res -> "{" + res.toString() + "}") + .collect(Collectors.joining(",")); + } else if (resource != null || label != null) { + String ret = LockStepResource.toString(resource, label, quantity); + if (this.priority != 0) { + ret += ", Priority: " + this.priority; + } + return ret; + } else { + return "nothing"; + } + } + + // ------------------------------------------------------------------------- + /** Label and resource are mutual exclusive. */ + public void validate(boolean allowEmptyOrNullValues) { + LockStepResource.validate( + resource, label, resourceSelectStrategy, extra, priority, inversePrecedence, allowEmptyOrNullValues); + } + + // ------------------------------------------------------------------------- + public List getResources() { + List resources = new ArrayList<>(); + if (resource != null || label != null) { + resources.add(new LockStepResource(resource, label, quantity)); + } + + if (extra != null) { + resources.addAll(extra); + } + return resources; + } + + @Override + public StepExecution start(StepContext context) { + return new LockStepExecution(this, context); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java index 87aa18558..5a8e55afc 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStepExecution.java @@ -1,12 +1,22 @@ package org.jenkins.plugins.lockableresources; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.EnvVars; +import hudson.model.Run; +import hudson.model.TaskListener; import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Set; +import java.util.Locale; +import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; - +import java.util.stream.Collectors; +import org.jenkins.plugins.lockableresources.actions.LockedResourcesBuildAction; import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; @@ -14,132 +24,253 @@ import org.jenkinsci.plugins.workflow.steps.BodyInvoker; import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.jenkinsci.plugins.workflow.support.actions.PauseAction; -import com.google.common.base.Joiner; -import com.google.inject.Inject; +public class LockStepExecution extends AbstractStepExecutionImpl implements Serializable { -import hudson.EnvVars; -import hudson.model.Run; -import hudson.model.TaskListener; -import org.jenkinsci.plugins.workflow.support.actions.PauseAction; + private static final long serialVersionUID = 1391734561272059623L; + + private static final Logger LOGGER = Logger.getLogger(LockStepExecution.class.getName()); + + private final LockStep step; + + public LockStepExecution(LockStep step, StepContext context) { + super(context); + this.step = step; + } + + @Override + public boolean start() throws Exception { + // normally it might raise an exception, but we check it in the function .validate() + // therefore we can skip the try-catch here. + ResourceSelectStrategy resourceSelectStrategy = + ResourceSelectStrategy.valueOf(step.resourceSelectStrategy.toUpperCase(Locale.ENGLISH)); + + PrintStream logger = getContext().get(TaskListener.class).getLogger(); + + Run run = getContext().get(Run.class); + + List resourceHolderList = new ArrayList<>(); + + List available; + LinkedHashMap> lockedResources = new LinkedHashMap<>(); + LockableResourcesManager lrm = LockableResourcesManager.get(); + synchronized (LockableResourcesManager.syncResources) { + step.validate(lrm.isAllowEmptyOrNullValues()); + + LockableResourcesManager.printLogs("Trying to acquire lock on [" + step + "]", Level.FINE, LOGGER, logger); + + getContext().get(FlowNode.class).addAction(new PauseAction("Lock")); + + if (!lrm.isAllowEmptyOrNullValues() || acquireLock()) { + List resourceNames = new ArrayList<>(); + for (LockStepResource resource : step.getResources()) { + List resources = new ArrayList<>(); + if (resource.resource != null) { + if (lrm.createResource(resource.resource)) { + LockableResourcesManager.printLogs( + "Resource [" + resource.resource + "] did not exist. Created.", + Level.FINE, + LOGGER, + logger); + } + resources.add(resource.resource); + resourceNames.addAll(resources); + } else { + resourceNames.add("N/A"); + } + resourceHolderList.add(new LockableResourcesStruct(resources, resource.label, resource.quantity)); + } + LockedResourcesBuildAction.addLog(run, resourceNames, "try", step.toString()); + // determine if there are enough resources available to proceed + available = lrm.getAvailableResources(resourceHolderList, logger, resourceSelectStrategy); + if (available == null || available.isEmpty()) { + LOGGER.fine("No available resources: " + available); + onLockFailed(logger, resourceHolderList); + return false; + } + + if (!lrm.lock(available, run)) { + // this here is very defensive code, and you will probably never hit it. (hopefully) + LOGGER.warning("Internal program error: Can not lock resources: " + available); + onLockFailed(logger, resourceHolderList); + return true; + } + + // since LockableResource contains transient variables, they cannot be correctly serialized + // hence we use their unique resource names and properties + for (LockableResource resource : available) { + lockedResources.put(resource.getName(), resource.getProperties()); + } + } + + LockStepExecution.proceed(lockedResources, getContext(), step.toString(), step.variable); + } + + return false; + } + + // --------------------------------------------------------------------------- + /** + * Checks if a lock can be acquired based on the step's properties: label, resource, and extra. + * To acquire a lock, at least one of these properties must be non-null and non-empty. + */ + private boolean acquireLock() { + if (step.label != null) { + return true; + } + if (step.resource != null) { + return true; + } + if (step.extra != null && !step.extra.isEmpty()) { + return true; + } + LOGGER.warning("No lock will be acquired. Either the label, resource or extra is null or empty."); + LOGGER.warning("Step: " + step); + LOGGER.warning("Label: " + step.label); + LOGGER.warning("Resource: " + step.resource); + LOGGER.warning("Extra: " + step.extra); + return false; + } + + // --------------------------------------------------------------------------- + /** + * Executed when the lock() function fails. No available resources, or we failed to lock available + * resources if the resource is known, we could output the active/blocking job/build + */ + private void onLockFailed(PrintStream logger, List resourceHolderList) { + + if (step.skipIfLocked) { + this.printBlockCause(logger, resourceHolderList); + LockableResourcesManager.printLogs( + "[" + step + "] is not free, skipping execution ...", Level.FINE, LOGGER, logger); + getContext().onSuccess(null); + } else { + this.printBlockCause(logger, resourceHolderList); + LockableResourcesManager.printLogs( + "[" + step + "] is not free, waiting for execution ...", Level.FINE, LOGGER, logger); + LockableResourcesManager lrm = LockableResourcesManager.get(); + lrm.queueContext( + getContext(), + resourceHolderList, + step.toString(), + step.variable, + step.inversePrecedence, + step.priority); + } + } + + private void printBlockCause(PrintStream logger, List resourceHolderList) { + LockableResourcesManager lrm = LockableResourcesManager.get(); + LockableResource resource = this.step.resource != null ? lrm.fromName(this.step.resource) : null; + + if (resource != null) { + final String logMessage = resource.getLockCauseDetail(); + if (logMessage != null && !logMessage.isEmpty()) + LockableResourcesManager.printLogs(logMessage, Level.FINE, LOGGER, logger); + } else { + // looks like ordered by label + lrm.getAvailableResources(resourceHolderList, logger, null); + } + } + + // --------------------------------------------------------------------------- + @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "not sure which exceptions might be catch.") + public static void proceed( + final LinkedHashMap> lockedResources, + StepContext context, + String resourceDescription, + final String variable) { + Run build; + FlowNode node; + PrintStream logger; + try { + build = context.get(Run.class); + node = context.get(FlowNode.class); + logger = context.get(TaskListener.class).getLogger(); + LockableResourcesManager.printLogs( + "Lock acquired on [" + resourceDescription + "]", Level.FINE, LOGGER, logger); + } catch (Exception e) { + context.onFailure(e); + return; + } + + try { + List resourceNames = new ArrayList<>(lockedResources.keySet()); + final String resourceNamesAsString = String.join(",", lockedResources.keySet()); + LockedResourcesBuildAction.addLog(build, resourceNames, "acquired", resourceDescription); + PauseAction.endCurrentPause(node); + BodyInvoker bodyInvoker = + context.newBodyInvoker().withCallback(new Callback(resourceNames, resourceDescription)); + if (variable != null && !variable.isEmpty()) { + // set the variable for the duration of the block + bodyInvoker.withContext( + EnvironmentExpander.merge(context.get(EnvironmentExpander.class), new EnvironmentExpander() { + private static final long serialVersionUID = -3431466225193397896L; + + @Override + public void expand(@NonNull EnvVars env) { + final LinkedHashMap variables = new LinkedHashMap<>(); + variables.put(variable, resourceNamesAsString); + int index = 0; + for (Entry> lockResourceEntry : + lockedResources.entrySet()) { + String lockEnvName = variable + index; + variables.put(lockEnvName, lockResourceEntry.getKey()); + for (LockableResourceProperty lockProperty : lockResourceEntry.getValue()) { + String propEnvName = lockEnvName + "_" + lockProperty.getName(); + variables.put(propEnvName, lockProperty.getValue()); + } + ++index; + } + LOGGER.finest("Setting " + + variables.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", ")) + + " for the duration of the block"); + env.overrideAll(variables); + } + })); + } + bodyInvoker.start(); + } catch (IOException | InterruptedException e) { + LOGGER.warning("proceed done with failure " + resourceDescription); + throw new RuntimeException(e); + } + } + + private static final class Callback extends BodyExecutionCallback.TailCall { + + private static final long serialVersionUID = -2024890670461847666L; + private final List resourceNames; + private final String resourceDescription; + + Callback(List resourceNames, String resourceDescription) { + this.resourceNames = resourceNames; + this.resourceDescription = resourceDescription; + } -public class LockStepExecution extends AbstractStepExecutionImpl { - - private static final Joiner COMMA_JOINER = Joiner.on(','); - - @Inject(optional = true) - private LockStep step; - - @StepContextParameter - private transient Run run; - - @StepContextParameter - private transient TaskListener listener; - - @StepContextParameter - private transient FlowNode node; - - private static final Logger LOGGER = Logger.getLogger(LockStepExecution.class.getName()); - - @Override - public boolean start() throws Exception { - step.validate(); - - node.addAction(new PauseAction("Lock")); - listener.getLogger().println("Trying to acquire lock on [" + step + "]"); - - List resourceHolderList = new ArrayList<>(); - - for (LockStepResource resource : step.getResources()) { - List resources = new ArrayList(); - if (resource.resource != null) { - if (LockableResourcesManager.get().createResource(resource.resource)) { - listener.getLogger().println("Resource [" + resource + "] did not exist. Created."); - } - resources.add(resource.resource); - } - resourceHolderList.add(new LockableResourcesStruct(resources, resource.label, resource.quantity)); - } - - // determine if there are enough resources available to proceed - Set available = LockableResourcesManager.get().checkResourcesAvailability(resourceHolderList, listener.getLogger(), null); - if (available == null || !LockableResourcesManager.get().lock(available, run, getContext(), step.toString(), step.variable, step.inversePrecedence)) { - listener.getLogger().println("[" + step + "] is locked, waiting..."); - LockableResourcesManager.get().queueContext(getContext(), resourceHolderList, step.toString()); - } // proceed is called inside lock if execution is possible - return false; - } - - public static void proceed(final List resourcenames, StepContext context, String resourceDescription, final String variable, boolean inversePrecedence) { - Run r = null; - FlowNode node = null; - try { - r = context.get(Run.class); - node = context.get(FlowNode.class); - context.get(TaskListener.class).getLogger().println("Lock acquired on [" + resourceDescription + "]"); - } catch (Exception e) { - context.onFailure(e); - return; - } - - LOGGER.finest("Lock acquired on [" + resourceDescription + "] by " + r.getExternalizableId()); - try { - PauseAction.endCurrentPause(node); - BodyInvoker bodyInvoker = context.newBodyInvoker(). - withCallback(new Callback(resourcenames, resourceDescription, variable, inversePrecedence)). - withDisplayName(null); - if(variable != null && variable.length()>0) - // set the variable for the duration of the block - bodyInvoker.withContext(EnvironmentExpander.merge(context.get(EnvironmentExpander.class), new EnvironmentExpander() { - @Override - public void expand(EnvVars env) throws IOException, InterruptedException { - final String resources = COMMA_JOINER.join(resourcenames); - LOGGER.finest("Setting [" + variable + "] to [" + resources - + "] for the duration of the block"); - - env.override(variable, resources); - } - })); - bodyInvoker.start(); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private static final class Callback extends BodyExecutionCallback.TailCall { - - private final List resourceNames; - private final String resourceDescription; - private final String variable; - private final boolean inversePrecedence; - - Callback(List resourceNames, String resourceDescription, String variable, boolean inversePrecedence) { - this.resourceNames = resourceNames; - this.resourceDescription = resourceDescription; - this.variable = variable; - this.inversePrecedence = inversePrecedence; - } - - protected void finished(StepContext context) throws Exception { - LockableResourcesManager.get().unlockNames(this.resourceNames, context.get(Run.class), this.variable, this.inversePrecedence); - context.get(TaskListener.class).getLogger().println("Lock released on resource [" + resourceDescription + "]"); - LOGGER.finest("Lock released on [" + resourceDescription + "]"); - } - - private static final long serialVersionUID = 1L; - - } - - @Override - public void stop(Throwable cause) throws Exception { - boolean cleaned = LockableResourcesManager.get().unqueueContext(getContext()); - if (!cleaned) { - LOGGER.log(Level.WARNING, "Cannot remove context from lockable resource witing list. The context is not in the waiting list."); - } - getContext().onFailure(cause); - } - - private static final long serialVersionUID = 1L; + @Override + protected void finished(StepContext context) throws Exception { + Run build = context.get(Run.class); + LockedResourcesBuildAction.addLog(build, this.resourceNames, "released", this.resourceDescription); + LockableResourcesManager.get().unlockNames(this.resourceNames, build); + LockableResourcesManager.printLogs( + "Lock released on resource [" + this.resourceDescription + "]", + Level.FINE, + LOGGER, + context.get(TaskListener.class).getLogger()); + } + } + @Override + public void stop(@NonNull Throwable cause) { + boolean cleaned = LockableResourcesManager.get().unqueueContext(getContext()); + if (!cleaned) { + LOGGER.log( + Level.WARNING, + "Cannot remove context from lockable resource waiting list. The context is not in the waiting list."); + } + getContext().onFailure(cause); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java index 6665a13fe..1f1d75ae9 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java @@ -1,122 +1,199 @@ package org.jenkins.plugins.lockableresources; -import java.io.Serializable; - -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.QueryParameter; - +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.AutoCompletionCandidates; import hudson.model.Descriptor; +import hudson.model.Item; import hudson.util.FormValidation; -import hudson.Util; - -import edu.umd.cs.findbugs.annotations.Nullable; -import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; public class LockStepResource extends AbstractDescribableImpl implements Serializable { - @CheckForNull - public String resource = null; - - @CheckForNull - public String label = null; - - public int quantity = 0; - - LockStepResource(String resource, String label, int quantity) { - this.resource = resource; - this.label = label; - this.quantity = quantity; - } - - @DataBoundConstructor - public LockStepResource(String resource) { - if (resource != null && !resource.isEmpty()) { - this.resource = resource; - } - } - - @DataBoundSetter - public void setLabel(String label) { - if (label != null && !label.isEmpty()) { - this.label = label; - } - } - - @DataBoundSetter - public void setQuantity(int quantity) { - this.quantity = quantity; - } - - public String toString() { - return toString(resource, label, quantity); - } - - public static String toString(String resource, String label, int quantity) { - // a label takes always priority - if (label != null) { - if (quantity > 0) { - return "Label: " + label + ", Quantity: " + quantity; - } - return "Label: " + label; - } - // make sure there is an actual resource specified - if (resource != null) { - return resource; - } - return "[no resource/label specified - probably a bug]"; - } - - /** - * Label and resource are mutual exclusive. - */ - public void validate() throws Exception { - validate(resource, label, quantity); - } - - /** - * Label and resource are mutual exclusive. - */ - public static void validate(String resource, String label, int quantity) throws Exception { - if (label != null && !label.isEmpty() && resource != null && !resource.isEmpty()) { - throw new IllegalArgumentException("Label and resource name cannot be specified simultaneously."); - } - } - - private static final long serialVersionUID = 1L; - - @Extension - public static class DescriptorImpl extends Descriptor { - - @Override - public String getDisplayName() { - return "Resource"; - } - - public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) { - return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value); - } - - public static FormValidation doCheckLabel(@QueryParameter String value, @QueryParameter String resource) { - String resourceLabel = Util.fixEmpty(value); - String resourceName = Util.fixEmpty(resource); - if (resourceLabel != null && resourceName != null) { - return FormValidation.error("Label and resource name cannot be specified simultaneously."); - } - if ((resourceLabel == null) && (resourceName == null)) { - return FormValidation.error("Either label or resource name must be specified."); - } - return FormValidation.ok(); - } - - public static FormValidation doCheckResource(@QueryParameter String value, @QueryParameter String label) { - return doCheckLabel(label, value); - } - } - + @CheckForNull + public String resource = null; + + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String label = null; + + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public int quantity = 0; + + LockStepResource(@Nullable String resource, @Nullable String label, int quantity) { + this.resource = Util.fixEmptyAndTrim(resource); + this.label = Util.fixEmptyAndTrim(label); + this.quantity = quantity; + } + + @DataBoundConstructor + public LockStepResource(@Nullable String resource) { + this.resource = Util.fixEmptyAndTrim(resource); + } + + @DataBoundSetter + public void setLabel(String label) { + this.label = Util.fixEmptyAndTrim(label); + } + + @DataBoundSetter + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + @Override + public String toString() { + return toString(resource, label, quantity); + } + + public static String toString(String resource, String label, int quantity) { + // a label takes always priority + if (label != null) { + if (quantity > 0) { + return "Label: " + label + ", Quantity: " + quantity; + } + return "Label: " + label; + } + // make sure there is an actual resource specified + if (resource != null) { + return "Resource: " + resource; + } + return "[no resource/label specified - probably a bug]"; + } + + // ------------------------------------------------------------------------- + /** Label and resource are mutual exclusive. */ + public void validate(boolean allowEmptyOrNullValues) { + validate(resource, label, null, false, 0, false, allowEmptyOrNullValues); + } + + // ------------------------------------------------------------------------- + /** Validate input parameters*/ + public static void validate( + String resource, + String label, + String resourceSelectStrategy, + List extra, + int priority, + boolean inversePrecedence, + boolean allowEmptyOrNullValues) { + validate( + resource, + label, + resourceSelectStrategy, + extra != null && !extra.isEmpty(), + priority, + inversePrecedence, + allowEmptyOrNullValues); + if (extra != null) { + for (LockStepResource e : extra) { + e.validate(allowEmptyOrNullValues); + } + } + } + + // ------------------------------------------------------------------------- + /** + * Label and resource are mutual exclusive. The label, if provided, must be configured (at least + * one resource must have this label). + */ + public static void validate( + String resource, + String label, + String resourceSelectStrategy, + boolean hasExtra, + int priority, + boolean inversePrecedence, + boolean allowEmptyOrNullValues) { + + if (!allowEmptyOrNullValues && !hasExtra && label == null && resource == null) { + throw new IllegalArgumentException(Messages.error_labelOrNameMustBeSpecified()); + } + + if (priority != 0 && inversePrecedence) { + throw new IllegalArgumentException(Messages.error_inversePrecedenceAndPriorityAreSet()); + } + + if (label != null && resource != null) { + throw new IllegalArgumentException(Messages.error_labelAndNameSpecified()); + } + if (label != null && !LockableResourcesManager.get().isValidLabel(label)) { + throw new IllegalArgumentException(Messages.error_labelDoesNotExist(label)); + } + if (resourceSelectStrategy != null) { + try { + ResourceSelectStrategy.valueOf(resourceSelectStrategy.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(Messages.error_invalidResourceSelectionStrategy( + resourceSelectStrategy, + Arrays.stream(ResourceSelectStrategy.values()) + .map(Enum::toString) + .map(strategy -> strategy.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.joining(", ")))); + } + } + } + + private static final long serialVersionUID = 1L; + + @Extension + public static class DescriptorImpl extends Descriptor { + + @NonNull + @Override + public String getDisplayName() { + return Messages.LockStepResource_displayName(); + } + + @RequirePOST + public AutoCompletionCandidates doAutoCompleteResource( + @QueryParameter String value, @AncestorInPath Item item) { + return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value, item); + } + + @RequirePOST + public static FormValidation doCheckLabel( + @QueryParameter String value, @QueryParameter String resource, @AncestorInPath Item item) { + // check permission, security first + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + + String resourceLabel = Util.fixEmpty(value); + String resourceName = Util.fixEmpty(resource); + if (resourceLabel != null && resourceName != null) { + return FormValidation.error(Messages.error_labelAndNameSpecified()); + } + if ((resourceLabel == null) && (resourceName == null)) { + return FormValidation.error(Messages.error_labelOrNameMustBeSpecified()); + } + if (resourceLabel != null && !LockableResourcesManager.get().isValidLabel(resourceLabel)) { + return FormValidation.error(Messages.error_labelDoesNotExist(resourceLabel)); + } + return FormValidation.ok(); + } + + @RequirePOST + public static FormValidation doCheckResource( + @QueryParameter String value, @QueryParameter String label, @AncestorInPath Item item) { + return doCheckLabel(label, value, item); + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java index 47b1726c8..68f25c925 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java @@ -8,357 +8,649 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources; +import static java.text.DateFormat.MEDIUM; +import static java.text.DateFormat.SHORT; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import groovy.lang.Binding; -import groovy.lang.GroovyShell; import hudson.Extension; -import hudson.PluginManager; import hudson.Util; +import hudson.console.ModelHyperlinkNote; import hudson.model.AbstractDescribableImpl; -import hudson.model.AbstractBuild; import hudson.model.Descriptor; +import hudson.model.Label; import hudson.model.Queue; -import hudson.model.Run; import hudson.model.Queue.Item; import hudson.model.Queue.Task; +import hudson.model.Run; import hudson.model.User; +import hudson.model.labels.LabelAtom; import hudson.tasks.Mailer.UserProperty; - -import java.io.IOException; import java.io.Serializable; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; +import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; - import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jinterop.winreg.IJIWinReg.saveFile; +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.export.Exported; import org.kohsuke.stapler.export.ExportedBean; -import com.infradna.tool.bridge_method_injector.WithBridgeMethods; - -import edu.umd.cs.findbugs.annotations.CheckForNull; -import java.util.concurrent.ExecutionException; - -import javax.annotation.Nonnull; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - @ExportedBean(defaultVisibility = 999) public class LockableResource extends AbstractDescribableImpl implements Serializable { - private static final Logger LOGGER = Logger.getLogger(LockableResource.class.getName()); - public static final int NOT_QUEUED = 0; - private static final int QUEUE_TIMEOUT = 60; - public static final String GROOVY_LABEL_MARKER = "groovy:"; - - private final String name; - private String description = ""; - private String labels = ""; - private String reservedBy = null; - - private long queueItemId = NOT_QUEUED; - private String queueItemProject = null; - private transient Run build = null; - // Needed to make the state non-transient - private String buildExternalizableId = null; - private long queuingStarted = 0; - - /** - * Was used within the initial implementation of Pipeline functionality - * using {@link LockStep}, but became deprecated once several resources - * could be locked at once. See queuedContexts in {@link LockableResourcesManager}. - */ - @Deprecated - private List queuedContexts = new ArrayList(); - - @Deprecated - public LockableResource( - String name, String description, String labels, String reservedBy) { - this.name = name; - this.description = description; - this.labels = labels; - this.reservedBy = Util.fixEmptyAndTrim(reservedBy); - } - - @DataBoundConstructor - public LockableResource(String name) { - this.name = name; - } - - private Object readResolve() { - if (queuedContexts == null) { // this field was added after the initial version if this class - queuedContexts = new ArrayList(); - } - return this; - } - - @Deprecated - public List getQueuedContexts() { - return this.queuedContexts; - } - - @DataBoundSetter - public void setDescription(String description) { - this.description = description; - } - - @DataBoundSetter - public void setLabels(String labels) { - this.labels = labels; - } - - @Exported - public String getName() { - return name; - } - - @Exported - public String getDescription() { - return description; - } - - @Exported - public String getLabels() { - return labels; - } - - public boolean isValidLabel(String candidate, Map params) { - return labelsContain(candidate); - } - - private boolean labelsContain(String candidate) { - return makeLabelsList().contains(candidate); - } - - private List makeLabelsList() { - return Arrays.asList(labels.split("\\s+")); - } - - /** - * Checks if the script matches the requirement. - * @param script Script to be executed - * @param params Extra script parameters - * @return {@code true} if the script returns true (resource matches). - * @throws ExecutionException Script execution failed (e.g. due to the missing permissions). Carries info in the cause - */ - @Restricted(NoExternalUse.class) - public boolean scriptMatches(@Nonnull SecureGroovyScript script, @CheckForNull Map params) - throws ExecutionException { - Binding binding = new Binding(params); - binding.setVariable("resourceName", name); - binding.setVariable("resourceDescription", description); - binding.setVariable("resourceLabels", makeLabelsList()); - try { - Object result = script.evaluate(Jenkins.getInstance().getPluginManager().uberClassLoader, binding); - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Checked resource " + name + " for " + script.getScript() - + " with " + binding + " -> " + result); - } - return (Boolean) result; - } catch (Exception e) { - throw new ExecutionException("Cannot get boolean result out of groovy expression. See system log for more info", e); - } - } - - @Exported - public String getReservedBy() { - return reservedBy; - } - - @Exported - public boolean isReserved() { - return reservedBy != null; - } - - @Exported - public String getReservedByEmail() { - if (reservedBy != null) { - UserProperty email = null; - User user = Jenkins.getInstance().getUser(reservedBy); - if (user != null) - email = user.getProperty(UserProperty.class); - if (email != null) - return email.getAddress(); - } - return null; - } - - public boolean isQueued() { - this.validateQueuingTimeout(); - return queueItemId != NOT_QUEUED; - } - - // returns True if queued by any other task than the given one - public boolean isQueued(long taskId) { - this.validateQueuingTimeout(); - return queueItemId != NOT_QUEUED && queueItemId != taskId; - } - - public boolean isQueuedByTask(long taskId) { - this.validateQueuingTimeout(); - return queueItemId == taskId; - } - - public void unqueue() { - queueItemId = NOT_QUEUED; - queueItemProject = null; - queuingStarted = 0; - } - - @Exported - public boolean isLocked() { - return getBuild() != null; - } - - /** - * Resolve the lock cause for this resource. It can be reserved or locked. - * - * @return the lock cause or null if not locked - */ - @CheckForNull - public String getLockCause() { - if (isReserved()) { - return String.format("[%s] is reserved by %s", name, reservedBy); - } - if (isLocked()) { - return String.format("[%s] is locked by %s", name, buildExternalizableId); - } - return null; - } - - @WithBridgeMethods(value=AbstractBuild.class, adapterMethod="getAbstractBuild") - public Run getBuild() { - if (build == null && buildExternalizableId != null) { - build = Run.fromExternalizableId(buildExternalizableId); - } - return build; - } - - /** - * @see {@link WithBridgeMethods} - */ - @Deprecated - private Object getAbstractBuild(final Run owner, final Class targetClass) { - return owner instanceof AbstractBuild ? (AbstractBuild) owner : null; - } - - @Exported - public String getBuildName() { - if (getBuild() != null) - return getBuild().getFullDisplayName(); - else - return null; - } - - public void setBuild(Run lockedBy) { - this.build = lockedBy; - if (lockedBy != null) { - this.buildExternalizableId = lockedBy.getExternalizableId(); - } else { - this.buildExternalizableId = null; - } - } - - public Task getTask() { - Item item = Queue.getInstance().getItem(queueItemId); - if (item != null) { - return item.task; - } else { - return null; - } - } - - public long getQueueItemId() { - this.validateQueuingTimeout(); - return queueItemId; - } - - public String getQueueItemProject() { - this.validateQueuingTimeout(); - return this.queueItemProject; - } - - public void setQueued(long queueItemId) { - this.queueItemId = queueItemId; - this.queuingStarted = System.currentTimeMillis() / 1000; - } - - public void setQueued(long queueItemId, String queueProjectName) { - this.setQueued(queueItemId); - this.queueItemProject = queueProjectName; - } - - private void validateQueuingTimeout() { - if (queuingStarted > 0) { - long now = System.currentTimeMillis() / 1000; - if (now - queuingStarted > QUEUE_TIMEOUT) - unqueue(); - } - } - - @DataBoundSetter - public void setReservedBy(String userName) { - this.reservedBy = Util.fixEmptyAndTrim(userName); - } - - public void unReserve() { - this.reservedBy = null; - } - - public void reset() { - this.unReserve(); - this.unqueue(); - this.setBuild(null); - } - - @Override - public String toString() { - return name; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - LockableResource other = (LockableResource) obj; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - return true; - } - - @Extension - public static class DescriptorImpl extends Descriptor { - - @Override - public String getDisplayName() { - return "Resource"; - } - - } - - private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(LockableResource.class.getName()); + public static final int NOT_QUEUED = 0; + private static final int QUEUE_TIMEOUT = 60; + public static final String GROOVY_LABEL_MARKER = "groovy:"; + + private final String name; + private String description = ""; + /** + * @deprecated use labelsAsList instead due performance. + */ + @Deprecated + private transient String labels = null; + + private List labelsAsList = new ArrayList<>(); + private String reservedBy = null; + private Date reservedTimestamp = null; + private String note = ""; + + /** + * Track that a currently reserved resource was originally reserved for someone else, or locked + * for some other job, and explicitly taken away - e.g. for SUT post-mortem while a test job runs. + * Currently this does not track "who" it was taken from nor intend to give it back - just for + * bookkeeping and UI button naming. Cleared when the resource is unReserve'd. + */ + private boolean stolen = false; + + /** + * We can use arbitrary identifier in a temporary lock (e.g. a commit hash of built/tested + * sources), and not overwhelm Jenkins with lots of "garbage" locks. Such locks will be + * automatically removed when freed, if they were not explicitly declared in the Jenkins Configure + * System page. If an originally ephemeral lock is later defined in configuration, it becomes a + * usual persistent lock. If a "usual" lock definition is deleted while it is being held, it + * becomes ephemeral and will disappear when freed. + */ + private boolean ephemeral; + + private List properties = new ArrayList<>(); + + private long queueItemId = NOT_QUEUED; + private String queueItemProject = null; + private transient Run build = null; + // Needed to make the state non-transient + private String buildExternalizableId = null; + private long queuingStarted = 0; + + private static final long serialVersionUID = 1L; + + private transient boolean isNode = false; + + /** + * Was used within the initial implementation of Pipeline functionality using {@link LockStep}, + * but became deprecated once several resources could be locked at once. See queuedContexts in + * {@link LockableResourcesManager}. + * + * @deprecated Replaced with LockableResourcesManager.queuedContexts (since 1.11) + */ + @Deprecated + private List queuedContexts = new ArrayList<>(); + + /** + * @deprecated Use single-argument constructor instead (since 1.8) + */ + @Deprecated + @ExcludeFromJacocoGeneratedReport + public LockableResource(String name, String description, String labels, String reservedBy, String note) { + // todo throw exception, when the name is empty + // todo check if the name contains only valid characters (no spaces, new lines ...) + this.name = name; + this.setDescription(description); + this.setLabels(labels); + this.setReservedBy(reservedBy); + this.setNote(note); + } + + @DataBoundConstructor + public LockableResource(@CheckForNull String name) { + this.name = Util.fixNull(name); + // todo throw exception, when the name is empty + // todo check if the name contains only valid characters (no spaces, new lines ...) + } + + protected Object readResolve() { + if (queuedContexts == null) { // this field was added after the initial version if this class + queuedContexts = new ArrayList<>(); + } + if (properties == null) { + properties = new ArrayList<>(); + } + this.repairLabels(); + return this; + } + + private void repairLabels() { + if (this.labels == null) { + return; + } + + LOGGER.fine("Repair labels for resource " + this); + this.setLabels(this.labels); + this.labels = null; + } + + /** + * @deprecated Replaced with LockableResourcesManager.queuedContexts (since 1.11) + */ + @Deprecated + @ExcludeFromJacocoGeneratedReport + public List getQueuedContexts() { + return this.queuedContexts; + } + + public boolean isNodeResource() { + return isNode; + } + + public void setNodeResource(boolean b) { + isNode = b; + } + + @Exported + public String getName() { + return name; + } + + @Exported + public String getDescription() { + return description; + } + + @DataBoundSetter + public void setDescription(@Nullable String description) { + this.description = Util.fixNull(description); + } + + @Exported + public String getNote() { + return this.note; + } + + @DataBoundSetter + public void setNote(@Nullable String note) { + this.note = Util.fixNull(note); + } + + @DataBoundSetter + public void setEphemeral(boolean ephemeral) { + this.ephemeral = ephemeral; + } + + @Exported + public boolean isEphemeral() { + return ephemeral; + } + + /** + * Use getLabelsAsList instead todo This function is marked as deprecated but it is still used in + * tests and jelly (config) files. + */ + @Deprecated + @Exported + public String getLabels() { + if (this.labelsAsList == null) { + return ""; + } + return String.join(" ", this.labelsAsList); + } + + /** + * @deprecated no equivalent at the time. todo It shall be created new one function + * selLabelsAsList() and use that one. But it must be checked and changed all config.jelly + * files and this might takes more time as expected. That the reason why a deprecated + * function/property is still data-bound-setter + */ + // @Deprecated can not be used, because of JCaC + @DataBoundSetter + public void setLabels(@Nullable String labels) { + labels = Util.fixNull(labels); + // todo use label parser from Jenkins.Label to allow the same syntax + this.labelsAsList = new ArrayList<>(); + for (String label : labels.split("\\s+")) { + if (label == null || label.isEmpty()) { + continue; + } + this.labelsAsList.add(label); + } + } + + /** + * Get labels of this resource + * + * @return List of assigned labels. + */ + @Exported + public List getLabelsAsList() { + return this.labelsAsList; + } + + /** + * Checks if the resource has label *labelToFind* + * + * @param labelToFind Label to find. + * @return {@code true} if this resource contains the label. + */ + @Restricted(NoExternalUse.class) + public boolean hasLabel(@CheckForNull String labelToFind) { + return this.labelsContain(labelToFind); + } + + // ---------------------------------------------------------------------------- + /** + * @deprecated Use isValidLabel(String candidate) + */ + @Deprecated + @ExcludeFromJacocoGeneratedReport + public boolean isValidLabel(String candidate, Map params) { + return isValidLabel(candidate); + } + + // ---------------------------------------------------------------------------- + /** Check if the given *candidate* label is valid or not. + * candidate may be one label or Label expression (see also + * https://www.jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#node-allocate-node). + * Valid means that the resource contains the label or the Label-expression matched. + */ + public boolean isValidLabel(@Nullable String candidate) { + candidate = Util.fixEmptyAndTrim(Util.fixNull(candidate)); + if (candidate == null) { + return false; + } + + if (labelsContain(candidate)) { + return true; + } + + final Label labelExpression = Label.parseExpression(candidate); + Set atomLabels = new HashSet<>(); + for (String label : this.getLabelsAsList()) { + atomLabels.add(new LabelAtom(label)); + } + + return labelExpression.matches(atomLabels); + } + + // ---------------------------------------------------------------------------- + /** + * Checks if the resource contain label *candidate*. + * + * @param candidate Labels to find. + * @return {@code true} if resource contains label *candidate* + */ + private boolean labelsContain(String candidate) { + return this.getLabelsAsList().contains(candidate); + } + + @Exported + public List getProperties() { + return properties; + } + + @DataBoundSetter + public void setProperties(@Nullable List properties) { + this.properties = (properties == null ? new ArrayList<>() : properties); + } + + /** + * Checks if the script matches the requirement. + * + * @param script Script to be executed + * @param params Extra script parameters + * @return {@code true} if the script returns true (resource matches). + * @throws ExecutionException Script execution failed (e.g. due to the missing permissions). + * Carries info in the cause + */ + @Restricted(NoExternalUse.class) + public boolean scriptMatches(@NonNull SecureGroovyScript script, @CheckForNull Map params) + throws ExecutionException { + Binding binding = new Binding(params); + binding.setVariable("resourceName", name); + binding.setVariable("resourceDescription", description); + binding.setVariable("resourceLabels", this.getLabelsAsList()); + binding.setVariable("resourceNote", note); + try { + Object result = script.evaluate(Jenkins.get().getPluginManager().uberClassLoader, binding, null); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Checked resource " + + name + + " for " + + script.getScript() + + " with " + + binding + + " -> " + + result); + } + return (Boolean) result; + } catch (Exception e) { + throw new ExecutionException( + "Cannot get boolean result out of groovy expression. See system log for more info", e); + } + } + + @Exported + public Date getReservedTimestamp() { + return reservedTimestamp == null ? null : new Date(reservedTimestamp.getTime()); + } + + @DataBoundSetter + public void setReservedTimestamp(@Nullable final Date reservedTimestamp) { + this.reservedTimestamp = reservedTimestamp == null ? null : new Date(reservedTimestamp.getTime()); + } + + @Exported + public String getReservedBy() { + return reservedBy; + } + + /** Return true when resource is free. False otherwise */ + @Exported + public boolean isFree() { + return (!this.isLocked() && !this.isReserved() && !this.isQueued()); + } + + @Exported + public boolean isReserved() { + return reservedBy != null; + } + + @Restricted(NoExternalUse.class) + @CheckForNull + public static String getUserName() { + User current = User.current(); + if (current != null) { + return current.getFullName(); + } else { + return null; + } + } + + /** + * Function check if the resources is reserved by currently logged user + * + * @return true when reserved by current user, false otherwise. + */ + @Restricted(NoExternalUse.class) // called by jelly + public boolean isReservedByCurrentUser() { + return (this.reservedBy != null && Objects.equals(getUserName(), this.reservedBy)); + } + + @Exported + public String getReservedByEmail() { + if (isReserved()) { + UserProperty email = null; + User user = Jenkins.get().getUser(reservedBy); + if (user != null) email = user.getProperty(UserProperty.class); + if (email != null) return email.getAddress(); + } + return null; + } + + public boolean isQueued() { + this.validateQueuingTimeout(); + return queueItemId != NOT_QUEUED; + } + + // returns True if queued by any other task than the given one + public boolean isQueued(long taskId) { + this.validateQueuingTimeout(); + return queueItemId != NOT_QUEUED && queueItemId != taskId; + } + + public boolean isQueuedByTask(long taskId) { + this.validateQueuingTimeout(); + return queueItemId == taskId; + } + + public void unqueue() { + queueItemId = NOT_QUEUED; + queueItemProject = null; + queuingStarted = 0; + } + + @Exported + public boolean isLocked() { + return getBuild() != null; + } + + /** + * Resolve the lock cause for this resource. It can be reserved or locked. + * + * @return the lock cause or null if not locked + */ + @CheckForNull + @Exported + public String getLockCause() { + final DateFormat format = SimpleDateFormat.getDateTimeInstance(MEDIUM, SHORT); + final String timestamp = (reservedTimestamp == null ? "" : format.format(reservedTimestamp)); + if (isReserved()) { + return String.format("[%s] is reserved by %s at %s", name, reservedBy, timestamp); + } + if (isLocked()) { + return String.format("[%s] is locked by %s at %s", name, buildExternalizableId, timestamp); + } + return null; + } + + /** + * Resolve the lock detailed cause for this resource. + * Note: this function is used in lock() step and not in the UI. Therefore + * moving text into localization files does not make really sense. + * + * @return the lock cause or null if not locked + */ + @CheckForNull + @Restricted(NoExternalUse.class) + public String getLockCauseDetail() { + if (this.isReserved()) { + User user = Jenkins.get().getUser(reservedBy); + String userText = user == null ? reservedBy : ModelHyperlinkNote.encodeTo(user); + return String.format("The resource [%s] is reserved by %s.", name, userText); + } + if (this.isLocked()) { + final DateFormat format = SimpleDateFormat.getDateTimeInstance(MEDIUM, SHORT); + Date since = this.getReservedTimestamp(); + final String timestamp = (since == null ? "" : format.format(since)); + return String.format( + "The resource [%s] is locked by build %s since %s.", + name, getBuild().getFullDisplayName() + " " + ModelHyperlinkNote.encodeTo(getBuild()), timestamp); + } + return null; + } + + public Run getBuild() { + if (build == null && buildExternalizableId != null) { + build = Run.fromExternalizableId(buildExternalizableId); + } + return build; + } + + // --------------------------------------------------------------------------- + @Exported + public String getBuildName() { + if (getBuild() != null) return getBuild().getFullDisplayName(); + else return null; + } + + // --------------------------------------------------------------------------- + public void setBuild(@Nullable Run lockedBy) { + + this.build = lockedBy; + + if (lockedBy != null) { + this.buildExternalizableId = lockedBy.getExternalizableId(); + setReservedTimestamp(new Date()); + } else { + this.buildExternalizableId = null; + setReservedTimestamp(null); + } + } + + public Task getTask() { + Item item = Queue.getInstance().getItem(queueItemId); + if (item != null) { + return item.task; + } else { + return null; + } + } + + public long getQueueItemId() { + this.validateQueuingTimeout(); + return queueItemId; + } + + public String getQueueItemProject() { + this.validateQueuingTimeout(); + return this.queueItemProject; + } + + public void setQueued(long queueItemId) { + this.queueItemId = queueItemId; + this.queuingStarted = System.currentTimeMillis() / 1000; + } + + public void setQueued(long queueItemId, String queueProjectName) { + this.setQueued(queueItemId); + this.queueItemProject = queueProjectName; + } + + private void validateQueuingTimeout() { + if (queuingStarted > 0) { + long now = System.currentTimeMillis() / 1000; + if (now - queuingStarted > QUEUE_TIMEOUT) unqueue(); + } + } + + @DataBoundSetter + public void setReservedBy(String userName) { + this.reservedBy = Util.fixEmptyAndTrim(userName); + } + + public void setStolen() { + this.stolen = true; + } + + @Exported + public boolean isStolen() { + return this.stolen; + } + + public void reserve(String userName) { + setReservedBy(userName); + setReservedTimestamp(new Date()); + } + + public void unReserve() { + this.setReservedBy(null); + this.setReservedTimestamp(null); + this.stolen = false; + } + + public void reset() { + this.unReserve(); + this.unqueue(); + this.setBuild(null); + } + + /** + * Copy unconfigurable properties from another instance. Normally, called after "lockable + * resource" configuration change. + * + * @param sourceResource resource with properties to copy from + */ + public void copyUnconfigurableProperties(final LockableResource sourceResource) { + if (sourceResource != null) { + setReservedTimestamp(sourceResource.getReservedTimestamp()); + setNote(sourceResource.getNote()); + setReservedBy(sourceResource.getReservedBy()); + } + } + + /** + * Reset unconfigurable properties. Normally, called after "lockable resource" configuration + * change, to make sure that these fields are ignored if defined in CasC configuration file. + */ + public void resetUnconfigurableProperties() { + setReservedBy(null); + setReservedTimestamp(null); + setNote(""); + } + + /** + * Tell LRM to recycle this resource, including notifications for whoever may be waiting in the + * queue so they can proceed immediately. WARNING: Do not use this from inside the lock step + * closure which originally locked this resource, to avoid nasty surprises! Just stick with + * unReserve() and close the closure, if needed. + */ + public void recycle() { + try { + List resources = new ArrayList<>(); + resources.add(this); + org.jenkins.plugins.lockableresources.LockableResourcesManager.get().recycle(resources); + } catch (Exception e) { + this.reset(); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + LockableResource other = (LockableResource) obj; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + return true; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + @NonNull + @Override + public String getDisplayName() { + return Messages.LockableResource_displayName(); + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourceProperty.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourceProperty.java new file mode 100644 index 000000000..2647b623d --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourceProperty.java @@ -0,0 +1,59 @@ +package org.jenkins.plugins.lockableresources; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Descriptor; +import java.io.Serializable; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; + +@ExportedBean(defaultVisibility = 999) +public class LockableResourceProperty extends AbstractDescribableImpl + implements Serializable { + + private String name; + private String value; + + @DataBoundConstructor + public LockableResourceProperty() {} + + @DataBoundSetter + public void setName(String name) { + this.name = name; + } + + @DataBoundSetter + public void setValue(String value) { + this.value = value; + } + + @Exported + public String getName() { + return name; + } + + @Exported + public String getValue() { + return value; + } + + @Override + public String toString() { + return name; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + @NonNull + @Override + public String getDisplayName() { + return "Property"; + } + } + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java index 321de3453..8f493afb0 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java @@ -10,24 +10,19 @@ import hudson.Plugin; import hudson.model.Api; - -import java.util.Collections; import java.util.List; - import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; @ExportedBean public class LockableResources extends Plugin { - public Api getApi() { - return new Api(this); - } - - @Exported - public List getResources() { - return Collections.unmodifiableList(LockableResourcesManager.get() - .getResources()); - } + public Api getApi() { + return new Api(this); + } + @Exported + public List getResources() { + return LockableResourcesManager.get().getReadOnlyResources(); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java index dd28beff6..06ce7230e 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java @@ -8,795 +8,1414 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; -import hudson.Extension; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.BulkChange; -import hudson.model.AbstractBuild; +import hudson.Extension; +import hudson.Util; +import hudson.console.ModelHyperlinkNote; +import hudson.model.Descriptor; import hudson.model.Run; - import java.io.IOException; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import hudson.model.TaskListener; +import java.util.stream.Collectors; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; -import net.sf.json.JSONException; +import jenkins.util.SystemProperties; import net.sf.json.JSONObject; - -import org.apache.commons.lang.StringUtils; -import org.jenkins.plugins.lockableresources.queue.LockableResourcesCandidatesStruct; +import org.jenkins.plugins.lockableresources.actions.LockedResourcesBuildAction; import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; -import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct; +import org.jenkins.plugins.lockableresources.util.Constants; +import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.kohsuke.stapler.StaplerRequest; - -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nonnull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.StaplerRequest2; @Extension public class LockableResourcesManager extends GlobalConfiguration { - @Deprecated - private transient int defaultPriority; - @Deprecated - private transient String priorityParameterName; - private List resources; - - - /** - * Only used when this lockable resource is tried to be locked by {@link LockStep}, - * otherwise (freestyle builds) regular Jenkins queue is used. - */ - private List queuedContexts = new ArrayList(); - - public LockableResourcesManager() { - resources = new ArrayList(); - load(); - } - - public List getResources() { - return resources; - } - - public List getResourcesFromProject(String fullName) { - List matching = new ArrayList(); - for (LockableResource r : resources) { - String rName = r.getQueueItemProject(); - if (rName != null && rName.equals(fullName)) { - matching.add(r); - } - } - return matching; - } - - public List getResourcesFromBuild(Run build) { - List matching = new ArrayList(); - for (LockableResource r : resources) { - Run rBuild = r.getBuild(); - if (rBuild != null && rBuild == build) { - matching.add(r); - } - } - return matching; - } - - public Boolean isValidLabel(String label) - { - return this.getAllLabels().contains(label); - } - - public Set getAllLabels() - { - Set labels = new HashSet(); - for (LockableResource r : this.resources) { - String rl = r.getLabels(); - if (rl == null || "".equals(rl)) - continue; - labels.addAll(Arrays.asList(rl.split("\\s+"))); - } - return labels; - } - - public int getFreeResourceAmount(String label) - { - int free = 0; - for (LockableResource r : this.resources) { - if (r.isLocked() || r.isQueued() || r.isReserved()) - continue; - if (Arrays.asList(r.getLabels().split("\\s+")).contains(label)) - free += 1; - } - return free; - } - - public List getResourcesWithLabel(String label, - Map params) { - List found = new ArrayList(); - for (LockableResource r : this.resources) { - if (r.isValidLabel(label, params)) - found.add(r); - } - return found; - } - - /** - * Get a list of resources matching the script. - * @param script Script - * @param params Additional parameters - * @return List of the matching resources - * @throws ExecutionException Script execution failed for one of the resources. - * It is considered as a fatal failure since the requirement list may be incomplete - * @since TODO - */ - @Nonnull - public List getResourcesMatchingScript(@Nonnull SecureGroovyScript script, - @CheckForNull Map params) throws ExecutionException{ - List found = new ArrayList(); - for (LockableResource r : this.resources) { - if (r.scriptMatches(script, params)) - found.add(r); - } - return found; - } - - public LockableResource fromName(String resourceName) { - if (resourceName != null) { - for (LockableResource r : resources) { - if (resourceName.equals(r.getName())) - return r; - } - } - return null; - } - - public synchronized boolean queue(List resources, - long queueItemId, String queueProjectName) { - for (LockableResource r : resources) - if (r.isReserved() || r.isQueued(queueItemId) || r.isLocked()) - return false; - for (LockableResource r : resources) { - r.setQueued(queueItemId, queueProjectName); - } - return true; - } - - /** - * @deprecated USe {@link #tryQueue(org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct, long, java.lang.String, int, java.util.Map, java.util.logging.Logger)} - */ - @Deprecated - @CheckForNull - public synchronized List queue(LockableResourcesStruct requiredResources, - long queueItemId, - String queueItemProject, - int number, // 0 means all - Map params, - Logger log) { - try { - return tryQueue(requiredResources, queueItemId, queueItemProject, number, params, log); - } catch(ExecutionException ex) { - if (LOGGER.isLoggable(Level.WARNING)) { - String itemName = queueItemProject + " (id=" + queueItemId + ")"; - LOGGER.log(Level.WARNING, "Failed to queue item " + itemName, ex.getCause() != null ? ex.getCause() : ex); - } - return null; - } - } - - /** - * Try to acquire the resources required by the task. - * @param number Number of resources to acquire. {@code 0} means all - * @return List of the locked resources if the task has been accepted. - * {@code null} if the item is still waiting for the resources - * @throws ExecutionException Cannot queue the resource due to the execution failure. Carries info in the cause - * @since TODO - */ - @CheckForNull - public synchronized List tryQueue(LockableResourcesStruct requiredResources, - long queueItemId, String queueItemProject, int number, - Map params, Logger log) throws ExecutionException { - List selected = new ArrayList(); - - if (!checkCurrentResourcesStatus(selected, queueItemProject, queueItemId, log)) { - // The project has another buildable item waiting -> bail out - log.log(Level.FINEST, "{0} has another build waiting resources." + - " Waiting for it to proceed first.", - new Object[]{queueItemProject}); - return null; - } - - boolean candidatesByScript=false; - List candidates = new ArrayList(); - final SecureGroovyScript systemGroovyScript = requiredResources.getResourceMatchScript(); - if (requiredResources.label != null && requiredResources.label.isEmpty() && systemGroovyScript == null) { - candidates = requiredResources.required; - } else if (systemGroovyScript == null) { - candidates = getResourcesWithLabel(requiredResources.label, params); - } else { - candidates = getResourcesMatchingScript(systemGroovyScript, params); - candidatesByScript = true; - } - - for (LockableResource rs : candidates) { - if (number != 0 && (selected.size() >= number)) - break; - if (!rs.isReserved() && !rs.isLocked() && !rs.isQueued()) - selected.add(rs); - } - - // if did not get wanted amount or did not get all - final int required_amount; - if (candidatesByScript && candidates.size() == 0) { - /** - * If the groovy script does not return any candidates, it means nothing is needed, even - * if a higher amount is specified. A valid use case is a Matrix job, when not all - * configurations need resources. - */ - required_amount = 0; - } else { - required_amount = number == 0 ? candidates.size() : number; - } - - if (selected.size() != required_amount) { - log.log(Level.FINEST, "{0} found {1} resource(s) to queue." + - "Waiting for correct amount: {2}.", - new Object[]{queueItemProject, selected.size(), required_amount}); - // just to be sure, clean up - for (LockableResource x : resources) { - if (x.getQueueItemProject() != null && - x.getQueueItemProject().equals(queueItemProject)) - x.unqueue(); - } - return null; - } - - for (LockableResource rsc : selected) { - rsc.setQueued(queueItemId, queueItemProject); - } - return selected; - } - - // Adds already selected (in previous queue round) resources to 'selected' - // Return false if another item queued for this project -> bail out - private boolean checkCurrentResourcesStatus(List selected, - String project, - long taskId, - Logger log) { - for (LockableResource r : resources) { - // This project might already have something in queue - String rProject = r.getQueueItemProject(); - if (rProject != null && rProject.equals(project)) { - if (r.isQueuedByTask(taskId)) { - // this item has queued the resource earlier - selected.add(r); - } else { - // The project has another buildable item waiting -> bail out - log.log(Level.FINEST, "{0} has another build " + - "that already queued resource {1}. Continue queueing.", - new Object[]{project, r}); - return false; - } - } - } - return true; - } - - public synchronized boolean lock(Set resources, Run build, @Nullable StepContext context) { - return lock(resources, build, context, null, null, false); - } - - /** - * Try to lock the resource and return true if locked. - */ - public synchronized boolean lock(Set resources, - Run build, @Nullable StepContext context, @Nullable String logmessage, - final String variable, boolean inversePrecedence) { - boolean needToWait = false; - - for (LockableResource r : resources) { - if (r.isReserved() || r.isLocked()) { - needToWait = true; - break; - } - } - if (!needToWait) { - for (LockableResource r : resources) { - r.unqueue(); - r.setBuild(build); - } - if (context != null) { - // since LockableResource contains transient variables, they cannot be correctly serialized - // hence we use their unique resource names - List resourceNames = new ArrayList(); - for (LockableResource resource : resources) { - resourceNames.add(resource.getName()); - } - LockStepExecution.proceed(resourceNames, context, logmessage, variable, inversePrecedence); - } - } - save(); - return !needToWait; - } - - private synchronized void freeResources(List unlockResourceNames, @Nullable Run build) { - for (String unlockResourceName : unlockResourceNames) { - for (LockableResource resource : this.resources) { - if (resource != null && resource.getName() != null && resource.getName().equals(unlockResourceName)) { - if (build == null || (resource.getBuild() != null && build.getExternalizableId().equals(resource.getBuild().getExternalizableId()))) { - // No more contexts, unlock resource - resource.unqueue(); - resource.setBuild(null); - } - } - } - } - } - - public synchronized void unlock(List resourcesToUnLock, @Nullable Run build) { - unlock(resourcesToUnLock, build, null, false); - } - - public synchronized void unlock(@Nullable List resourcesToUnLock, - @Nullable Run build, String requiredVar, boolean inversePrecedence) { - List resourceNamesToUnLock = new ArrayList(); - if (resourcesToUnLock != null) { - for (LockableResource r : resourcesToUnLock) { - resourceNamesToUnLock.add(r.getName()); - } - } - - this.unlockNames(resourceNamesToUnLock, build, requiredVar, inversePrecedence); - } - - public synchronized void unlockNames(@Nullable List resourceNamesToUnLock, @Nullable Run build, String requiredVar, boolean inversePrecedence) { - // make sure there is a list of resource names to unlock - if (resourceNamesToUnLock == null || (resourceNamesToUnLock.size() == 0)) { - return; - } - - // process as many contexts as possible - List remainingResourceNamesToUnLock = new ArrayList<>(resourceNamesToUnLock); - - QueuedContextStruct nextContext = null; - while (!remainingResourceNamesToUnLock.isEmpty()) { - // check if there are resources which can be unlocked (and shall not be unlocked) - Set requiredResourceForNextContext = null; - nextContext = this.getNextQueuedContext(remainingResourceNamesToUnLock, inversePrecedence, nextContext); - - // no context is queued which can be started once these resources are free'd. - if (nextContext == null) { - this.freeResources(remainingResourceNamesToUnLock, build); - save(); - return; - } - - requiredResourceForNextContext = checkResourcesAvailability(nextContext.getResources(), null, remainingResourceNamesToUnLock); - - // resourceNamesToUnlock contains the names of the previous resources. - // requiredResourceForNextContext contains the resource objects which are required for the next context. - // It is guaranteed that there is an overlap between the two - the resources which are to be reused. - boolean needToWait = false; - for (LockableResource requiredResource : requiredResourceForNextContext) { - if (!remainingResourceNamesToUnLock.contains(requiredResource.getName())) { - if (requiredResource.isReserved() || requiredResource.isLocked()) { - needToWait = true; - break; - } - } - } - - if (!needToWait) { - // remove context from queue and process it - unqueueContext(nextContext.getContext()); - - List resourceNamesToLock = new ArrayList(); - - // lock all (old and new resources) - for (LockableResource requiredResource : requiredResourceForNextContext) { - try { - requiredResource.setBuild(nextContext.getContext().get(Run.class)); - resourceNamesToLock.add(requiredResource.getName()); - } catch (Exception e) { - // skip this context, as the build cannot be retrieved (maybe it was deleted while running?) - LOGGER.log(Level.WARNING, "Skipping queued context for lock. Can not get the Run object from the context to proceed with lock, " + - "this could be a legitimate status if the build waiting for the lock was deleted or" + - " hard killed. More information at Level.FINE if debug is needed."); - LOGGER.log(Level.FINE, "Can not get the Run object from the context to proceed with lock", e); - unlockNames(remainingResourceNamesToUnLock, build, requiredVar, inversePrecedence); - return; - } - } - - // determine old resources no longer needed - List freeResources = new ArrayList(); - for (String resourceNameToUnlock : remainingResourceNamesToUnLock) { - boolean resourceStillNeeded = false; - for (LockableResource requiredResource : requiredResourceForNextContext) { - if (resourceNameToUnlock != null && resourceNameToUnlock.equals(requiredResource.getName())) { - resourceStillNeeded = true; - break; - } - } - - if (!resourceStillNeeded) { - freeResources.add(resourceNameToUnlock); - } - } - - // keep unused resources - remainingResourceNamesToUnLock.retainAll(freeResources); - - // continue with next context - LockStepExecution.proceed(resourceNamesToLock, nextContext.getContext(), nextContext.getResourceDescription(), requiredVar, inversePrecedence); - } - } - save(); - } - - /** - * Returns the next queued context with all its requirements satisfied. - * - * @param resourceNamesToUnLock resource names locked at the moment but available is required (as they are going to be unlocked soon - * @param inversePrecedence false pick up context as they are in the queue or true to take the most recent one (satisfying requirements) - * @return the context or null - */ - @CheckForNull - private QueuedContextStruct getNextQueuedContext(List resourceNamesToUnLock, boolean inversePrecedence, QueuedContextStruct from) { - QueuedContextStruct newestEntry = null; - List requiredResourceForNextContext = null; - int fromIndex = from != null ? this.queuedContexts.indexOf(from) + 1 : 0; - if (!inversePrecedence) { - for (int i = fromIndex; i < this.queuedContexts.size(); i++) { - QueuedContextStruct entry = this.queuedContexts.get(i); - if (checkResourcesAvailability(entry.getResources(), null, resourceNamesToUnLock) != null) { - return entry; - } - } - } else { - long newest = 0; - List orphan = new ArrayList(); - for (int i = fromIndex; i < this.queuedContexts.size(); i++) { - QueuedContextStruct entry = this.queuedContexts.get(i); - if (checkResourcesAvailability(entry.getResources(), null, resourceNamesToUnLock) != null) { - try { - Run run = entry.getContext().get(Run.class); - if (run != null && run.getStartTimeInMillis() > newest) { - newest = run.getStartTimeInMillis(); - newestEntry = entry; - } - } catch (IOException | InterruptedException e) { - // skip this one, for some reason there is no Run object for this context - orphan.add(entry); - } - } - } - if (!orphan.isEmpty()) { - this.queuedContexts.removeAll(orphan); - } - } - - return newestEntry; - } - - /** - * Creates the resource if it does not exist. - */ - public synchronized boolean createResource(String name) { - if (name != null) { - LockableResource existent = fromName(name); - if (existent == null) { - getResources().add(new LockableResource(name)); - save(); - return true; - } - } - return false; - } - - public synchronized boolean createResourceWithLabel(String name, String label) { - if (name !=null && label !=null) { - LockableResource existent = fromName(name); - if (existent == null) { - getResources().add(new LockableResource(name, "", label, null)); - save(); - return true; - } - } - return false; - } - - public synchronized boolean reserve(List resources, - String userName) { - for (LockableResource r : resources) { - if (r.isReserved() || r.isLocked() || r.isQueued()) { - return false; - } - } - for (LockableResource r : resources) { - r.setReservedBy(userName); - } - save(); - return true; - } - - private void unreserveResources(@Nonnull List resources) { - for (LockableResource l : resources) { - l.unReserve(); - } - save(); - } - public synchronized void unreserve(List resources) { - // make sure there is a list of resources to unreserve - if (resources == null || (resources.size() == 0)) { - return; - } - List resourceNamesToUnreserve = new ArrayList<>(); - for (LockableResource r : resources) { - resourceNamesToUnreserve.add(r.getName()); - } - - // check if there are resources which can be unlocked (and shall not be unlocked) - Set requiredResourceForNextContext = null; - QueuedContextStruct nextContext = this.getNextQueuedContext(resourceNamesToUnreserve, false, null); - - // no context is queued which can be started once these resources are free'd. - if (nextContext == null) { - LOGGER.log(Level.FINER, "No context queued for resources " + StringUtils.join(resourceNamesToUnreserve, ", ") + " so unreserving and proceeding."); - unreserveResources(resources); - return; - } - - PrintStream nextContextLogger = null; - try { - TaskListener nextContextTaskListener = nextContext.getContext().get(TaskListener.class); - if (nextContextTaskListener != null) { - nextContextLogger = nextContextTaskListener.getLogger(); - } - } catch (IOException | InterruptedException e) { - LOGGER.log(Level.FINE, "Could not get logger for next context: " + e, e); - } - - // remove context from queue and process it - requiredResourceForNextContext = checkResourcesAvailability(nextContext.getResources(), - nextContextLogger, - resourceNamesToUnreserve); - this.queuedContexts.remove(nextContext); - - // resourceNamesToUnreserve contains the names of the previous resources. - // requiredResourceForNextContext contains the resource objects which are required for the next context. - // It is guaranteed that there is an overlap between the two - the resources which are to be reused. - boolean needToWait = false; - for (LockableResource requiredResource : requiredResourceForNextContext) { - if (!resourceNamesToUnreserve.contains(requiredResource.getName())) { - if (requiredResource.isReserved() || requiredResource.isLocked()) { - needToWait = true; - break; - } - } - } - - if (needToWait) { - unreserveResources(resources); - return; - } else { - unreserveResources(resources); - List resourceNamesToLock = new ArrayList(); - - // lock all (old and new resources) - for (LockableResource requiredResource : requiredResourceForNextContext) { - try { - requiredResource.setBuild(nextContext.getContext().get(Run.class)); - resourceNamesToLock.add(requiredResource.getName()); - } catch (Exception e) { - // skip this context, as the build cannot be retrieved (maybe it was deleted while running?) - LOGGER.log(Level.WARNING, "Skipping queued context for lock. Can not get the Run object from the context to proceed with lock, " + - "this could be a legitimate status if the build waiting for the lock was deleted or" + - " hard killed. More information at Level.FINE if debug is needed."); - LOGGER.log(Level.FINE, "Can not get the Run object from the context to proceed with lock", e); - return; - } - } - - // continue with next context - LockStepExecution.proceed(resourceNamesToLock, nextContext.getContext(), nextContext.getResourceDescription(), null, false); - } - save(); - } - - @Override - public String getDisplayName() { - return "External Resources"; - } - - public synchronized void reset(List resources) { - for (LockableResource r : resources) { - r.reset(); - } - save(); - } - - @Override - public boolean configure(StaplerRequest req, JSONObject json) - throws FormException { - try { - List newResouces = req.bindJSONToList( - LockableResource.class, json.get("resources")); - for (LockableResource r : newResouces) { - LockableResource old = fromName(r.getName()); - if (old != null) { - r.setBuild(old.getBuild()); - r.setQueued(r.getQueueItemId(), r.getQueueItemProject()); - } - } - resources = newResouces; - save(); - return true; - } catch (JSONException e) { - return false; - } - } - - /** - * Checks if there are enough resources available to satisfy the requirements specified - * within requiredResources and returns the necessary available resources. - * If not enough resources are available, returns null. - */ - public synchronized Set checkResourcesAvailability(List requiredResourcesList, - @Nullable PrintStream logger, @Nullable List lockedResourcesAboutToBeUnlocked) { - - List requiredResourcesCandidatesList = new ArrayList<>(); - - // Build possible resources for each requirement - for (LockableResourcesStruct requiredResources : requiredResourcesList) { - // get possible resources - int requiredAmount = 0; // 0 means all - List candidates = new ArrayList<>(); - if (requiredResources.label != null && requiredResources.label.isEmpty()) { - candidates.addAll(requiredResources.required); - } else { - candidates.addAll(getResourcesWithLabel(requiredResources.label, null)); - if (requiredResources.requiredNumber != null) { - try { - requiredAmount = Integer.parseInt(requiredResources.requiredNumber); - } catch (NumberFormatException e) { - requiredAmount = 0; - } - } - } - - if (requiredAmount == 0) { - requiredAmount = candidates.size(); - } - - requiredResourcesCandidatesList.add(new LockableResourcesCandidatesStruct(candidates, requiredAmount)); - } - - // Process freed resources - int totalSelected = 0; - - for (LockableResourcesCandidatesStruct requiredResources : requiredResourcesCandidatesList) { - // start with an empty set of selected resources - List selected = new ArrayList(); - - // some resources might be already locked, but will be freed. - // Determine if these resources can be reused - if (lockedResourcesAboutToBeUnlocked != null) { - for (LockableResource candidate : requiredResources.candidates) { - if (selected.size() >= requiredResources.requiredAmount) { - break; - } - if (lockedResourcesAboutToBeUnlocked.contains(candidate.getName())) { - selected.add(candidate); - } - } - } - - totalSelected += selected.size(); - requiredResources.selected = selected; - } - - // if none of the currently locked resources can be reused, - // this context is not suitable to be continued with - if (lockedResourcesAboutToBeUnlocked != null && totalSelected == 0) { - return null; - } - - // Find remaining resources - Set allSelected = new HashSet<>(); - - for (LockableResourcesCandidatesStruct requiredResources : requiredResourcesCandidatesList) { - List candidates = requiredResources.candidates; - List selected = requiredResources.selected; - int requiredAmount = requiredResources.requiredAmount; - - // Try and re-use as many previously selected resources first - List alreadySelectedCandidates = new ArrayList<>(candidates); - alreadySelectedCandidates.retainAll(allSelected); - for (LockableResource rs : alreadySelectedCandidates) { - if (selected.size() >= requiredAmount) { - break; - } - if (!rs.isReserved() && !rs.isLocked()) { - selected.add(rs); - } - } - - candidates.removeAll(alreadySelectedCandidates); - for (LockableResource rs : candidates) { - if (selected.size() >= requiredAmount) { - break; - } - if (!rs.isReserved() && !rs.isLocked()) { - selected.add(rs); - } - } - - if (selected.size() < requiredAmount) { - if (logger != null) { - logger.println("Found " + selected.size() + " available resource(s). Waiting for correct amount: " + requiredAmount + "."); - } - return null; - } - - allSelected.addAll(selected); - } - - return allSelected; - } - - /* - * Adds the given context and the required resources to the queue if - * this context is not yet queued. - */ - public synchronized void queueContext(StepContext context, List requiredResources, String resourceDescription) { - for (QueuedContextStruct entry : this.queuedContexts) { - if (entry.getContext() == context) { - return; - } - } - - this.queuedContexts.add(new QueuedContextStruct(context, requiredResources, resourceDescription)); - save(); - } - - public synchronized boolean unqueueContext(StepContext context) { - for (Iterator iter = this.queuedContexts.listIterator(); iter.hasNext(); ) { - if (iter.next().getContext() == context) { - iter.remove(); - save(); - return true; - } - } - return false; - } - - public static LockableResourcesManager get() { - return (LockableResourcesManager) Jenkins.getInstance() - .getDescriptorOrDie(LockableResourcesManager.class); - } - - public synchronized void save() { - if(BulkChange.contains(this)) + /** Object to synchronized operations over LRM */ + public static final Object syncResources = new Object(); + + private List resources; + private final transient Cache> cachedCandidates = + CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); + private static final Logger LOGGER = Logger.getLogger(LockableResourcesManager.class.getName()); + + private boolean allowEmptyOrNullValues; + + /** + * Only used when this lockable resource is tried to be locked by {@link LockStep}, otherwise + * (freestyle builds) regular Jenkins queue is used. + */ + private final List queuedContexts = new ArrayList<>(); + + // cache to enable / disable saving lockable-resources state + private int enableSave = -1; + + private static final int enabledBlockedCount = + SystemProperties.getInteger(Constants.SYSTEM_PROPERTY_PRINT_BLOCKED_RESOURCE, 2); + private static final int enabledCausesCount = + SystemProperties.getInteger(Constants.SYSTEM_PROPERTY_PRINT_QUEUE_INFO, 2); + + @DataBoundSetter + public void setAllowEmptyOrNullValues(boolean allowEmptyOrNullValues) { + this.allowEmptyOrNullValues = allowEmptyOrNullValues; + } + + public boolean isAllowEmptyOrNullValues() { + return allowEmptyOrNullValues; + } + + // --------------------------------------------------------------------------- + /** C-tor */ + @SuppressFBWarnings( + value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", + justification = "Common Jenkins pattern to call method that can be overridden") + public LockableResourcesManager() { + resources = new ArrayList<>(); + load(); + } + + // --------------------------------------------------------------------------- + /** Get all resources Includes declared, ephemeral and node resources */ + public List getResources() { + return this.resources; + } + + // --------------------------------------------------------------------------- + /** + * Get all resources - read only The same as getResources() but unmodifiable list. The + * getResources() is unsafe to use because of possible concurrent modification exception. + */ + @Restricted(NoExternalUse.class) + public List getReadOnlyResources() { + synchronized (syncResources) { + return new ArrayList<>(Collections.unmodifiableCollection(this.resources)); + } + } + + // --------------------------------------------------------------------------- + /** Get declared resources, means only defined in config file (xml or JCaC yaml). */ + @Restricted(NoExternalUse.class) + public List getDeclaredResources() { + ArrayList declaredResources = new ArrayList<>(); + for (LockableResource r : this.getResources()) { + if (!r.isEphemeral() && !r.isNodeResource()) { + declaredResources.add(r); + } + } + return declaredResources; + } + + // --------------------------------------------------------------------------- + /** Set all declared resources (do not include ephemeral and node resources). */ + @DataBoundSetter + public void setDeclaredResources(List declaredResources) { + synchronized (syncResources) { + Map lockedResources = new HashMap<>(); + for (LockableResource r : this.resources) { + if (!r.isLocked()) continue; + lockedResources.put(r.getName(), r); + } + + // Removed from configuration locks became ephemeral. + ArrayList mergedResources = new ArrayList<>(); + Set addedLocks = new HashSet<>(); + for (LockableResource r : declaredResources) { + if (!addedLocks.add(r.getName())) { + continue; + } + LockableResource locked = lockedResources.remove(r.getName()); + if (locked != null) { + // Merge already locked lock. + locked.setDescription(r.getDescription()); + locked.setLabels(r.getLabels()); + locked.setEphemeral(false); + locked.setNote(r.getNote()); + mergedResources.add(locked); + continue; + } + mergedResources.add(r); + } + + for (LockableResource r : lockedResources.values()) { + // Removed locks became ephemeral. + r.setDescription(""); + r.setLabels(""); + r.setNote(""); + r.setEphemeral(true); + mergedResources.add(r); + } + + // Copy reservations and unconfigurable properties from old instances. Clear unconfigurable + // properties for new resources: they should be empty anyway for new resources from UI + // configuration. For CasC configuration, we ignore those fields, so set them to empty. + for (LockableResource newResource : mergedResources) { + final LockableResource oldDeclaredResource = fromName(newResource.getName()); + if (oldDeclaredResource != null) { + newResource.copyUnconfigurableProperties(oldDeclaredResource); + } else { + newResource.resetUnconfigurableProperties(); + } + } + + this.resources = mergedResources; + save(); + } + } + + // --------------------------------------------------------------------------- + /** Get all resources used by project. */ + @Restricted(NoExternalUse.class) + public List getResourcesFromProject(String fullName) { + List matching = new ArrayList<>(); + for (LockableResource r : this.getResources()) { + String rName = r.getQueueItemProject(); + if (rName != null && rName.equals(fullName)) { + matching.add(r); + } + } + return matching; + } + + // --------------------------------------------------------------------------- + /** + * Check if the label is valid. Valid in this context means, if is configured on someone resource. + */ + @Restricted(NoExternalUse.class) + public Boolean isValidLabel(@Nullable String label) { + if (label == null || label.isEmpty()) { + return false; + } + + synchronized (syncResources) { + for (LockableResource r : this.getResources()) { + if (r != null && r.isValidLabel(label)) { + return true; + } + } + } + + return false; + } + + // --------------------------------------------------------------------------- + /** Returns all configured labels. */ + @NonNull + @Restricted(NoExternalUse.class) + public Set getAllLabels() { + Set labels = new HashSet<>(); + for (LockableResource r : this.getReadOnlyResources()) { + if (r == null) { + continue; + } + List toAdd = r.getLabelsAsList(); + if (toAdd.isEmpty()) { + continue; + } + labels.addAll(toAdd); + } + return labels; + } + + // --------------------------------------------------------------------------- + /** Get amount of free resources contained given *label* + * This method is deprecated (no where used) and is not tested. + */ + @Restricted(NoExternalUse.class) + @Deprecated + @ExcludeFromJacocoGeneratedReport + public int getFreeResourceAmount(String label) { + int free = 0; + label = Util.fixEmpty(label); + + if (label == null) { + return free; + } + + for (LockableResource r : this.getResourcesWithLabel(label)) { + if (r == null) { + continue; + } + if (r.isFree()) { + free++; + } + } + return free; + } + + // --------------------------------------------------------------------------- + /** + * @deprecated Use getResourcesWithLabel(String label) + * Note: The param *params* is not used (has no effect) + */ + @Deprecated + @Restricted(NoExternalUse.class) + @ExcludeFromJacocoGeneratedReport + public List getResourcesWithLabel(String label, Map params) { + return getResourcesWithLabel(label); + } + + // --------------------------------------------------------------------------- + /** + * Returns resources matching by given *label*. + */ + @NonNull + @Restricted(NoExternalUse.class) + public List getResourcesWithLabel(final String label) { + synchronized (syncResources) { + return _getResourcesWithLabel(label, this.getResources()); + } + } + + // --------------------------------------------------------------------------- + @NonNull + private static List _getResourcesWithLabel(String label, final List resources) { + List found = new ArrayList<>(); + label = Util.fixEmpty(label); + + if (label == null) { + return found; + } + + for (LockableResource r : resources) { + if (r != null && r.isValidLabel(label)) found.add(r); + } + return found; + } + + // --------------------------------------------------------------------------- + /** + * Returns a list of resources matching by given *script*. + * + * @param script Script + * @param params Additional parameters + * @return List of the matching resources + * @throws ExecutionException Script execution failed for one of the resources. It is considered + * as a fatal failure since the requirement list may be incomplete + * @since 2.0 + */ + @NonNull + @Restricted(NoExternalUse.class) + public List getResourcesMatchingScript( + @NonNull SecureGroovyScript script, @CheckForNull Map params) throws ExecutionException { + List found = new ArrayList<>(); + synchronized (syncResources) { + for (LockableResource r : this.resources) { + if (r.scriptMatches(script, params)) found.add(r); + } + } + return found; + } + + // --------------------------------------------------------------------------- + /** Returns resource matched by name. Returns null in case, the resource does not exist. */ + @CheckForNull + @Restricted(NoExternalUse.class) + public LockableResource fromName(@CheckForNull String resourceName) { + resourceName = Util.fixEmpty(resourceName); + + if (resourceName != null) { + + synchronized (syncResources) { + for (LockableResource r : this.getResources()) { + if (resourceName.equals(r.getName())) return r; + } + } + } else { + LOGGER.warning("Internal failure, fromName is empty or null:" + getStack()); + } + return null; + } + + // --------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public List fromNames(@Nullable final List names) { + if (names == null) { + return null; + } + return fromNames(names, false); + } + + // --------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public List fromNames(final List names, final boolean createResource) { + List list = new ArrayList<>(); + for (String name : names) { + // be sure it exists + if (createResource) this.createResource(name); + LockableResource r = this.fromName(name); + if (r != null) // this is probably bug, but nobody know + list.add(r); + } + return list; + } + + // --------------------------------------------------------------------------- + private String getStack() { + StringBuilder buf = new StringBuilder(); + for (StackTraceElement st : Thread.currentThread().getStackTrace()) { + buf.append("\n").append(st); + } + return buf.toString(); + } + + // --------------------------------------------------------------------------- + /** Checks if given resource exist. */ + @NonNull + @Restricted(NoExternalUse.class) + public Boolean resourceExist(@CheckForNull String resourceName) { + return this.fromName(resourceName) != null; + } + + // --------------------------------------------------------------------------- + public boolean queue(List resources, long queueItemId, String queueProjectName) { + for (LockableResource r : resources) { + if (r.isReserved() || r.isQueued(queueItemId) || r.isLocked()) { + return false; + } + } + for (LockableResource r : resources) { + r.setQueued(queueItemId, queueProjectName); + } + return true; + } + + // --------------------------------------------------------------------------- + /** + * @deprecated Use {@link + * #tryQueue(org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct, long, + * java.lang.String, int, java.util.Map, java.util.logging.Logger)} + */ + @Deprecated + @CheckForNull + @ExcludeFromJacocoGeneratedReport + @Restricted(NoExternalUse.class) + public List queue( + LockableResourcesStruct requiredResources, + long queueItemId, + String queueItemProject, + int number, // 0 means all + Map params, + Logger log) { + try { + return tryQueue(requiredResources, queueItemId, queueItemProject, number, params, log); + } catch (ExecutionException ex) { + if (LOGGER.isLoggable(Level.WARNING)) { + String itemName = queueItemProject + " (id=" + queueItemId + ")"; + LOGGER.log( + Level.WARNING, "Failed to queue item " + itemName, ex.getCause() != null ? ex.getCause() : ex); + } + return null; + } + } + + // --------------------------------------------------------------------------- + /** + * If the lockable resource availability was evaluated before and cached to avoid frequent + * re-evaluations under queued pressure when there are no resources to give, we should state that + * a resource is again instantly available for re-evaluation when we know it was busy and right + * now is being freed. Note that a resource may be (both or separately) locked by a build and/or + * reserved by a user (or stolen from build to user) so we only un-cache it here if it becomes + * completely available. Called as a helper from methods that unlock/unreserve/reset (or + * indirectly - recycle) stuff. + * + *

NOTE for people using LR or LRM methods directly to add some abilities in their pipelines + * that are not provided by plugin: the `cachedCandidates` is an LRM concept, so if you tell a + * resource (LR instance) directly to unlock/unreserve, it has no idea to clean itself from this + * cache, and may be considered busy in queuing for some time afterward. + */ + public boolean uncacheIfFreeing(LockableResource candidate, boolean unlocking, boolean unreserving) { + if (candidate.isLocked() && !unlocking) return false; + + // "stolen" state helps track that a resource is currently not + // reserved for the same entity as it was originally given to; + // this flag is cleared during un-reservation. + if ((candidate.isReserved() || candidate.isStolen()) && !unreserving) return false; + + if (cachedCandidates.size() == 0) return true; + + // Per https://guava.dev/releases/19.0/api/docs/com/google/common/cache/Cache.html + // "Modifications made to the map directly affect the cache." + // so it is both a way for us to iterate the cache and to edit + // the lists it stores per queue. + Map> cachedCandidatesMap = cachedCandidates.asMap(); + for (Map.Entry> entry : cachedCandidatesMap.entrySet()) { + Long queueItemId = entry.getKey(); + List candidates = entry.getValue(); + if (candidates != null && (candidates.isEmpty() || candidates.contains(candidate))) { + cachedCandidates.invalidate(queueItemId); + } + } + + return true; + } + + // --------------------------------------------------------------------------- + /** + * Try to acquire the resources required by the task. + * + * @param number Number of resources to acquire. {@code 0} means all + * @return List of the locked resources if the task has been accepted. {@code null} if the item is + * still waiting for the resources + * @throws ExecutionException Cannot queue the resource due to the execution failure. Carries info + * in the cause + * @since 2.0 + */ + @CheckForNull + @Restricted(NoExternalUse.class) + public List tryQueue( + LockableResourcesStruct requiredResources, + long queueItemId, + String queueItemProject, + int number, + Map params, + Logger log) + throws ExecutionException { + List selected = new ArrayList<>(); + synchronized (syncResources) { + if (!checkCurrentResourcesStatus(selected, queueItemProject, queueItemId, log)) { + // The project has another buildable item waiting -> bail out + log.log( + Level.FINEST, + "{0} has another build waiting resources." + " Waiting for it to proceed first.", + new Object[] {queueItemProject}); + return null; + } + + final SecureGroovyScript systemGroovyScript; + try { + systemGroovyScript = requiredResources.getResourceMatchScript(); + } catch (Descriptor.FormException x) { + throw new ExecutionException(x); + } + boolean candidatesByScript = (systemGroovyScript != null); + List candidates = requiredResources.required; // default candidates + + if (candidatesByScript || (requiredResources.label != null && !requiredResources.label.isEmpty())) { + + candidates = cachedCandidates.getIfPresent(queueItemId); + if (candidates != null) { + candidates.retainAll(this.resources); + } else { + candidates = (systemGroovyScript == null) + ? getResourcesWithLabel(requiredResources.label) + : getResourcesMatchingScript(systemGroovyScript, params); + cachedCandidates.put(queueItemId, candidates); + } + } + + for (LockableResource rs : candidates) { + if (number != 0 && (selected.size() >= number)) break; + if (!rs.isReserved() && !rs.isLocked() && !rs.isQueued()) selected.add(rs); + } + + // if did not get wanted amount or did not get all + final int required_amount = getRequiredAmount(number, candidatesByScript, candidates); + + if (selected.size() != required_amount) { + log.log( + Level.FINEST, + "{0} found {1} resource(s) to queue. Waiting for correct amount: {2}.", + new Object[] {queueItemProject, selected.size(), required_amount}); + // just to be sure, clean up + for (LockableResource x : this.resources) { + if (x.getQueueItemProject() != null + && x.getQueueItemProject().equals(queueItemProject)) x.unqueue(); + } + return null; + } + + for (LockableResource rsc : selected) { + rsc.setQueued(queueItemId, queueItemProject); + } + } + return selected; + } + + // --------------------------------------------------------------------------- + /** + * Returns the amount of resources required by the task. + * If the groovy script does not return any candidates, it means nothing is needed, even if a + * higher amount is specified. A valid use case is a Matrix job, when not all configurations need resources. + */ + private static int getRequiredAmount(int number, boolean candidatesByScript, List candidates) { + final int required_amount; + if (candidatesByScript && candidates.isEmpty()) { + required_amount = 0; + } else { + required_amount = number == 0 ? candidates.size() : number; + } + return required_amount; + } + + // --------------------------------------------------------------------------- + // Adds already selected (in previous queue round) resources to 'selected' + // Return false if another item queued for this project -> bail out + private boolean checkCurrentResourcesStatus( + List selected, String project, long taskId, Logger log) { + for (LockableResource r : this.resources) { + // This project might already have something in queue + String rProject = r.getQueueItemProject(); + if (rProject != null && rProject.equals(project)) { + if (r.isQueuedByTask(taskId)) { + // this item has queued the resource earlier + selected.add(r); + } else { + // The project has another buildable item waiting -> bail out + log.log( + Level.FINEST, + "{0} has another build that already queued resource {1}. Continue queueing.", + new Object[] {project, r}); + return false; + } + } + } + return true; + } + + // --------------------------------------------------------------------------- + @Deprecated + public boolean lock(List resources, Run build, @Nullable StepContext context) { + return this.lock(resources, build); + } + + // --------------------------------------------------------------------------- + @Deprecated + public boolean lock( + List resources, + Run build, + @Nullable StepContext context, + @Nullable String logmessage, + final String variable, + boolean inversePrecedence) { + return this.lock(resources, build); + } + + // --------------------------------------------------------------------------- + /** Try to lock the resource and return true if locked. */ + public boolean lock(List resourcesToLock, Run build) { + + LOGGER.fine("lock it: " + resourcesToLock + " for build " + build); + + if (build == null) { + LOGGER.warning("lock() will fails, because the build does not exits. " + resourcesToLock); + return false; // not locked + } + + String cause = getCauses(resourcesToLock); + if (!cause.isEmpty()) { + LOGGER.warning("lock() for build " + build + " will fails, because " + cause); + return false; // not locked + } + + for (LockableResource r : resourcesToLock) { + r.unqueue(); + r.setBuild(build); + } + + LockedResourcesBuildAction.findAndInitAction(build).addUsedResources(getResourcesNames(resourcesToLock)); + + save(); + + return true; + } + + // --------------------------------------------------------------------------- + private void freeResources(List unlockResources, Run build) { + + LOGGER.fine("free it: " + unlockResources); + + // make sure there is a list of resource names to unlock + if (unlockResources == null || unlockResources.isEmpty() || build == null) { + return; + } + + List toBeRemoved = new ArrayList<>(); + + for (LockableResource resource : unlockResources) { + // No more contexts, unlock resource + + // the resource has been currently unlocked (like by LRM page - button unlock, or by API) + if (!build.equals(resource.getBuild())) continue; + + resource.unqueue(); + resource.setBuild(null); + uncacheIfFreeing(resource, true, false); + + if (resource.isEphemeral()) { + LOGGER.fine("Remove ephemeral resource: " + resource); + toBeRemoved.add(resource); + } + } + + LockedResourcesBuildAction.findAndInitAction(build).removeUsedResources(getResourcesNames(unlockResources)); + + // remove all ephemeral resources + removeResources(toBeRemoved); + } + + public void unlockBuild(@Nullable Run build) { + + if (build == null) { + return; + } + + List resourcesInUse = + LockedResourcesBuildAction.findAndInitAction(build).getCurrentUsedResourceNames(); + + if (resourcesInUse.isEmpty()) { + return; + } + unlockNames(resourcesInUse, build); + } + + // --------------------------------------------------------------------------- + public void unlockNames(@Nullable List resourceNamesToUnLock, Run build) { + + // make sure there is a list of resource names to unlock + if (resourceNamesToUnLock == null || resourceNamesToUnLock.isEmpty()) { + return; + } + synchronized (syncResources) { + unlockResources(this.fromNames(resourceNamesToUnLock), build); + } + } + + // --------------------------------------------------------------------------- + public void unlockResources(List resourcesToUnLock) { + unlockResources(resourcesToUnLock, resourcesToUnLock.get(0).getBuild()); + } + + // --------------------------------------------------------------------------- + public void unlockResources(List resourcesToUnLock, Run build) { + if (resourcesToUnLock == null || resourcesToUnLock.isEmpty()) { + return; + } + synchronized (syncResources) { + this.freeResources(resourcesToUnLock, build); + + while (proceedNextContext()) { + // process as many contexts as possible + } + + save(); + } + } + + private boolean proceedNextContext() { + QueuedContextStruct nextContext = this.getNextQueuedContext(); + LOGGER.finest("nextContext: " + nextContext); + // no context is queued which can be started once these resources are free'd. + if (nextContext == null) { + LOGGER.fine("No context is queued which can be started once these resources are free'd."); + return false; + } + LOGGER.finest("nextContext candidates: " + nextContext.candidates); + List requiredResourceForNextContext = + this.fromNames(nextContext.candidates, /*create un-existent resources */ true); + LOGGER.finest("nextContext real candidates: " + requiredResourceForNextContext); + // remove context from queue and process it + + Run build = nextContext.getBuild(); + if (build == null) { + // this shall never happen + // skip this context, as the build cannot be retrieved (maybe it was deleted while + // running?) + LOGGER.warning("Skip this context, as the build cannot be retrieved"); + return true; + } + boolean locked = this.lock(requiredResourceForNextContext, build); + if (!locked) { + // defensive line, shall never happen + LOGGER.warning("Can not lock resources: " + requiredResourceForNextContext); + // to eliminate possible endless loop + return false; + } + + // build env vars + LinkedHashMap> resourcesToLock = new LinkedHashMap<>(); + for (LockableResource requiredResource : requiredResourceForNextContext) { + resourcesToLock.put(requiredResource.getName(), requiredResource.getProperties()); + } + + this.unqueueContext(nextContext.getContext()); + + // continue with next context + LOGGER.fine("Continue with next context: " + nextContext); + LockStepExecution.proceed( + resourcesToLock, + nextContext.getContext(), + nextContext.getResourceDescription(), + nextContext.getVariableName()); + return true; + } + + // --------------------------------------------------------------------------- + /** Returns names (IDs) of given *resources*. */ + @Restricted(NoExternalUse.class) + public static List getResourcesNames(final List resources) { + List resourceNames = new ArrayList<>(); + if (resources != null) { + for (LockableResource resource : resources) { + resourceNames.add(resource.getName()); + } + } + return resourceNames; + } + + // --------------------------------------------------------------------------- + /** Returns names (IDs) off all existing resources (inclusive ephemeral) */ + @Restricted(NoExternalUse.class) + public List getAllResourcesNames() { + synchronized (syncResources) { + return getResourcesNames(this.resources); + } + } + + // --------------------------------------------------------------------------- + /** + * Returns the next queued context with all its requirements satisfied. + * + */ + @CheckForNull + private QueuedContextStruct getNextQueuedContext() { + + LOGGER.fine("current queue size: " + this.queuedContexts.size()); + LOGGER.finest("current queue: " + this.queuedContexts); + List orphan = new ArrayList<>(); + QueuedContextStruct nextEntry = null; + + // the first one added lock is the oldest one, and this wins + + for (int idx = 0; idx < this.queuedContexts.size() && nextEntry == null; idx++) { + QueuedContextStruct entry = this.queuedContexts.get(idx); + // check queue list first + if (!entry.isValid()) { + LOGGER.fine("well be removed: " + idx + " " + entry); + orphan.add(entry); + continue; + } + LOGGER.finest("oldest win - index: " + idx + " " + entry); + + nextEntry = getNextQueuedContextEntry(entry); + } + + if (!orphan.isEmpty()) { + this.queuedContexts.removeAll(orphan); + } + + return nextEntry; + } + + // --------------------------------------------------------------------------- + QueuedContextStruct getNextQueuedContextEntry(QueuedContextStruct entry) { + List candidates = this.getAvailableResources(entry.getResources()); + if (candidates == null || candidates.isEmpty()) { + return null; + } + + entry.candidates = getResourcesNames(candidates); + LOGGER.fine("take this: " + entry); + return entry; + } + + // --------------------------------------------------------------------------- + /** Returns current queue */ + @Restricted(NoExternalUse.class) // used by jelly + public List getCurrentQueuedContext() { + synchronized (syncResources) { + return Collections.unmodifiableList(this.queuedContexts); + } + } + + // --------------------------------------------------------------------------- + /** Creates the resource if it does not exist. */ + public boolean createResource(@CheckForNull String name) { + name = Util.fixEmptyAndTrim(name); + LockableResource resource = new LockableResource(name); + resource.setEphemeral(true); + + return this.addResource(resource, /*doSave*/ true); + } + + // --------------------------------------------------------------------------- + public boolean createResourceWithLabel(@CheckForNull String name, @CheckForNull String label) { + name = Util.fixEmptyAndTrim(name); + label = Util.fixEmptyAndTrim(label); + LockableResource resource = new LockableResource(name); + resource.setLabels(label); + + return this.addResource(resource, /*doSave*/ true); + } + + // --------------------------------------------------------------------------- + public boolean createResourceWithLabelAndProperties( + @CheckForNull String name, @CheckForNull String label, final Map properties) { + if (properties == null) { + return false; + } + + name = Util.fixEmptyAndTrim(name); + label = Util.fixEmptyAndTrim(label); + LockableResource resource = new LockableResource(name); + resource.setLabels(label); + resource.setProperties(properties.entrySet().stream() + .map(e -> { + LockableResourceProperty p = new LockableResourceProperty(); + p.setName(e.getKey()); + p.setValue(e.getValue()); + return p; + }) + .collect(Collectors.toList())); + + return this.addResource(resource, /*doSave*/ true); + } + + // --------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public boolean addResource(@Nullable final LockableResource resource) { + return this.addResource(resource, /*doSave*/ false); + } + // --------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public boolean addResource(@Nullable final LockableResource resource, final boolean doSave) { + + if (resource == null || resource.getName() == null || resource.getName().isEmpty()) { + LOGGER.warning("Internal failure: We will add wrong resource: '" + resource + "' " + getStack()); + return false; + } + synchronized (syncResources) { + if (this.resourceExist(resource.getName())) { + LOGGER.finest("We will add existing resource: " + resource + getStack()); + return false; + } + this.resources.add(resource); + LOGGER.fine("Resource added : " + resource); + if (doSave) { + this.save(); + } + } + return true; + } + + // --------------------------------------------------------------------------- + /** + * Reserves an available resource for the userName indefinitely (until that person, or some + * explicit scripted action, decides to release the resource). + */ + public boolean reserve(List resources, String userName) { + synchronized (syncResources) { + for (LockableResource r : resources) { + if (!r.isFree()) { + return false; + } + } + for (LockableResource r : resources) { + r.reserve(userName); + } + save(); + } + return true; + } + + // --------------------------------------------------------------------------- + /** + * Reserves a resource that may be or not be locked by some job (or reserved by some user) + * already, giving it away to the userName indefinitely (until that person, or some explicit + * scripted action, later decides to release the resource). + */ + public boolean steal(List resources, String userName) { + synchronized (syncResources) { + for (LockableResource r : resources) { + r.setReservedBy(userName); + r.setStolen(); + } + unlockResources(resources); + Date date = new Date(); + for (LockableResource r : resources) { + r.setReservedTimestamp(date); + } + save(); + } + return true; + } + + // --------------------------------------------------------------------------- + /** + * Reserves a resource that may be or not be reserved by some person already, giving it away to + * the userName indefinitely (until that person, or some explicit scripted action, decides to + * release the resource). + */ + public void reassign(List resources, String userName) { + synchronized (syncResources) { + Date date = new Date(); + for (LockableResource r : resources) { + if (!r.isFree()) { + r.unReserve(); + } + r.setReservedBy(userName); + r.setReservedTimestamp(date); + } + save(); + } + } + + // --------------------------------------------------------------------------- + private void unreserveResources(@NonNull List resources) { + for (LockableResource l : resources) { + uncacheIfFreeing(l, false, true); + l.unReserve(); + } + save(); + } + + // --------------------------------------------------------------------------- + public void unreserve(List resources) { + // make sure there is a list of resources to unreserve + if (resources == null || resources.isEmpty()) { + return; + } + + synchronized (syncResources) { + LOGGER.fine("unreserve " + resources); + unreserveResources(resources); + + proceedNextContext(); + + save(); + } + } + + // --------------------------------------------------------------------------- + @NonNull + @Override + public String getDisplayName() { + return Messages.LockableResourcesManager_displayName(); + } + + // --------------------------------------------------------------------------- + public void reset(List resources) { + synchronized (syncResources) { + for (LockableResource r : resources) { + uncacheIfFreeing(r, true, true); + r.reset(); + } + save(); + } + } + + // --------------------------------------------------------------------------- + /** + * Make the lockable resource reusable and notify the queue(s), if any WARNING: Do not use this + * from inside the lock step closure which originally locked this resource, to avoid nasty + * surprises! Namely, this *might* let a second consumer use the resource quickly, but when the + * original closure ends and unlocks again that resource, a third consumer might then effectively + * hijack it from the second one. + */ + public void recycle(List resources) { + synchronized (syncResources) { + // Not calling reset() because that also un-queues the resource + // and we want to proclaim it is usable (if anyone is waiting) + this.unlockResources(resources); + this.unreserve(resources); + } + } + + // --------------------------------------------------------------------------- + /** Change the order (position) of the given item in the queue*/ + @Restricted(NoExternalUse.class) // used by jelly + public void changeQueueOrder(final String queueId, final int newPosition) throws IOException { + synchronized (syncResources) { + if (newPosition < 0 || newPosition >= this.queuedContexts.size()) { + throw new IOException( + Messages.error_queuePositionOutOfRange(newPosition + 1, this.queuedContexts.size())); + } + + int oldIndex = -1; + for (int i = 0; i < this.queuedContexts.size(); i++) { + QueuedContextStruct entry = this.queuedContexts.get(i); + if (entry.getId().equals(queueId)) { + oldIndex = i; + break; + } + } + + if (oldIndex < 0) { + // no more exists !? + throw new IOException(Messages.error_queueDoesNotExist(queueId)); + } + + Collections.swap(this.queuedContexts, oldIndex, newPosition); + } + } + + // --------------------------------------------------------------------------- + @Override + public boolean configure(StaplerRequest2 req, JSONObject json) { + synchronized (syncResources) { + try (BulkChange bc = new BulkChange(this)) { + req.bindJSON(this, json); + bc.commit(); + } catch (IOException exception) { + LOGGER.log(Level.WARNING, "Exception occurred while committing bulkchange operation.", exception); + return false; + } + } + return true; + } + + // --------------------------------------------------------------------------- + public List getAvailableResources(final List requiredResourcesList) { + return this.getAvailableResources(requiredResourcesList, null, null); + } + + // --------------------------------------------------------------------------- + /** Function removes all given resources */ + public void removeResources(List toBeRemoved) { + synchronized (syncResources) { + this.resources.removeAll(toBeRemoved); + } + } + + // --------------------------------------------------------------------------- + /** + * Checks if there are enough resources available to satisfy the requirements specified within + * requiredResources and returns the necessary available resources. If not enough resources are + * available, returns null. + */ + public List getAvailableResources( + final List requiredResourcesList, + final @Nullable PrintStream logger, + final @Nullable ResourceSelectStrategy selectStrategy) { + + LOGGER.finest("getAvailableResources, " + requiredResourcesList); + List candidates = new ArrayList<>(); + for (LockableResourcesStruct requiredResources : requiredResourcesList) { + List available = new ArrayList<>(); + // filter by labels + if (requiredResources.label != null && !requiredResources.label.isBlank()) { + // get required amount first + int requiredAmount = 0; + if (requiredResources.requiredNumber != null) { + try { + requiredAmount = Integer.parseInt(requiredResources.requiredNumber); + } catch (NumberFormatException ignored) { + } + } + + available = this.getFreeResourcesWithLabel( + requiredResources.label, requiredAmount, selectStrategy, logger, candidates); + } else if (requiredResources.required != null) { + // resource by name requested + + // this is a little hack. The 'requiredResources.required' is a copy, and we need to find + // all of them in LRM + // fromNames() also re-create the resource (ephemeral things) + available = fromNames( + getResourcesNames(requiredResources.required), /*create un-existent resources */ true); + + if (!this.areAllAvailable(available)) { + available = null; + } + } else { + LOGGER.warning("getAvailableResources, Not implemented: " + requiredResources); + } + + if (available == null || available.isEmpty()) { + LOGGER.finest("No available resources found " + requiredResourcesList); + return null; + } + + final boolean isPreReserved = !Collections.disjoint(candidates, available); + if (isPreReserved) { + // FIXME I think this is failure + // You use filter label1 and it lock resource1 and then in extra you will lock resource1 + // But when I allow this line, many tests will fails, and I am pretty sure it will throws + // exceptions on end-user pipelines + // So when we want to fix, it it might be braking-change + // Therefore keep it here as warning for now + printLogs("Extra filter tries to allocate pre-reserved resources.", logger, Level.WARNING); + available.removeAll(candidates); + } + + candidates.addAll(available); + } + + return candidates; + } + + // --------------------------------------------------------------------------- + private boolean areAllAvailable(List resources) { + for (LockableResource resource : resources) { + if (!resource.isFree()) { + return false; + } + } + return true; + } + + // --------------------------------------------------------------------------- + public static void printLogs(final String msg, final Level level, Logger L, final @Nullable PrintStream logger) { + L.log(level, msg); + + if (logger != null) { + if (level == Level.WARNING || level == Level.SEVERE) logger.println(level.getLocalizedName() + ": " + msg); + else logger.println(msg); + } + } + + // --------------------------------------------------------------------------- + private static void printLogs(final String msg, final @Nullable PrintStream logger, final Level level) { + printLogs(msg, level, LOGGER, logger); + } + + // --------------------------------------------------------------------------- + @CheckForNull + @Restricted(NoExternalUse.class) + private List getFreeResourcesWithLabel( + @NonNull String label, + long amount, + final @Nullable ResourceSelectStrategy selectStrategy, + final @Nullable PrintStream logger, + final List alreadySelected) { + List found = new ArrayList<>(); + + List candidates = _getResourcesWithLabel(label, alreadySelected); + candidates.addAll(this.getResourcesWithLabel(label)); + + if (amount <= 0) { + amount = candidates.size(); + } + + if (candidates.size() < amount) { + printLogs( + "Found " + + candidates.size() + + " possible resource(s). Waiting for correct amount: " + + amount + + "." + + "This may remain stuck, until you create enough resources", + logger, + Level.WARNING); + return null; // there are not enough resources + } + + if (selectStrategy != null && selectStrategy.equals(ResourceSelectStrategy.RANDOM)) { + Collections.shuffle(candidates); + } + + for (LockableResource r : candidates) { + // TODO: it shall be used isFree() here, but in that case we need to change the + // logic in parametrized builds and that is much more effort as I want to spend here now + if (!r.isReserved() && !r.isLocked()) { + found.add(r); + } + + if (amount > 0 && found.size() >= amount) { + return found; + } + } + + String msg = "Found " + found.size() + " available resource(s). Waiting for correct amount: " + amount + "."; + if (enabledBlockedCount != 0) { + msg += "\nBlocking causes: " + getCauses(candidates); + } + printLogs(msg, logger, Level.FINE); + + return null; + } + + // --------------------------------------------------------------------------- + // for debug purpose + private String getCauses(List resources) { + StringBuilder buf = new StringBuilder(); + int currentSize = 0; + for (LockableResource resource : resources) { + String cause = resource.getLockCauseDetail(); + if (cause == null) continue; // means it is free, not blocked + + currentSize++; + if (enabledBlockedCount > 0 && currentSize == enabledBlockedCount) { + buf.append("\n ..."); + break; + } + buf.append("\n ").append(cause); + + final String queueCause = getQueueCause(resource); + if (!queueCause.isEmpty()) { + buf.append(queueCause); + } + } + return buf.toString(); + } + + // --------------------------------------------------------------------------- + // for debug purpose + private String getQueueCause(final LockableResource resource) { + Map, Integer> usage = new HashMap<>(); + + for (QueuedContextStruct entry : this.queuedContexts) { + + Run build = entry.getBuild(); + if (build == null) { + LOGGER.warning("Why we don`t have the build? " + entry); + continue; + } + + int count = 0; + if (usage.containsKey(build)) { + count = usage.get(build); + } + + for (LockableResourcesStruct _struct : entry.getResources()) { + if (_struct.isResourceRequired(resource)) { + LOGGER.fine("found " + resource + " " + count); + count++; + break; + } + } + + usage.put(build, count); + } + + StringBuilder buf = new StringBuilder(); + int currentSize = 0; + for (Map.Entry, Integer> entry : usage.entrySet()) { + Run build = entry.getKey(); + int count = entry.getValue(); + + if (build != null && count > 0) { + currentSize++; + buf.append("\n Queued ") + .append(count) + .append(" time(s) by build ") + .append(build.getFullDisplayName()) + .append(" ") + .append(ModelHyperlinkNote.encodeTo(build)); + + if (currentSize >= enabledCausesCount) { + buf.append("\n ..."); + break; + } + } + } + return buf.toString(); + } + + /* + * Adds the given context and the required resources to the queue if + * this context is not yet queued. + */ + @Restricted(NoExternalUse.class) + public void queueContext( + StepContext context, + List requiredResources, + String resourceDescription, + String variableName, + boolean inversePrecedence, + int priority) { + synchronized (syncResources) { + for (QueuedContextStruct entry : this.queuedContexts) { + if (entry.getContext() == context) { + LOGGER.warning("queueContext, duplicated, " + requiredResources); return; + } + } + + int queueIndex = 0; + QueuedContextStruct newQueueItem = + new QueuedContextStruct(context, requiredResources, resourceDescription, variableName, priority); + + if (!inversePrecedence || priority != 0) { + queueIndex = this.queuedContexts.size() - 1; + for (; queueIndex >= 0; queueIndex--) { + QueuedContextStruct entry = this.queuedContexts.get(queueIndex); + final int rc = entry.compare(newQueueItem); + if (rc > 0) { + continue; + } + break; + } + queueIndex++; + } - try { - getConfigFile().write(this); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(),e); + this.queuedContexts.add(queueIndex, newQueueItem); + printLogs( + requiredResources + " added into queue at position " + queueIndex, + newQueueItem.getLogger(), + Level.FINE); + + save(); + } + } + + // --------------------------------------------------------------------------- + public boolean unqueueContext(StepContext context) { + synchronized (syncResources) { + for (Iterator iter = this.queuedContexts.listIterator(); iter.hasNext(); ) { + if (iter.next().getContext() == context) { + iter.remove(); + save(); + return true; } + } + } + return false; + } + + // --------------------------------------------------------------------------- + public static LockableResourcesManager get() { + return (LockableResourcesManager) Jenkins.get().getDescriptorOrDie(LockableResourcesManager.class); + } + + // --------------------------------------------------------------------------- + @Override + public void save() { + if (enableSave == -1) { + // read system property and cache it. + enableSave = SystemProperties.getBoolean(Constants.SYSTEM_PROPERTY_DISABLE_SAVE) ? 0 : 1; } - private static final Logger LOGGER = Logger.getLogger(LockableResourcesManager.class.getName()); + if (enableSave == 0) return; // saving is disabled + synchronized (syncResources) { + if (BulkChange.contains(this)) return; + + try { + getConfigFile().write(this); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(), e); + } + } + } + + // --------------------------------------------------------------------------- + /** For testing purpose. */ + @Restricted(NoExternalUse.class) + public LockableResource getFirst() { + return this.getResources().get(0); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java index 2da486021..ce995dddd 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java @@ -8,269 +8,312 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; import hudson.model.AbstractProject; import hudson.model.AutoCompletionCandidates; +import hudson.model.Descriptor; +import hudson.model.Item; +import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; -import hudson.model.Job; import hudson.util.FormValidation; - import java.util.ArrayList; import java.util.List; - +import jenkins.model.Jenkins; import net.sf.json.JSONObject; - import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; - -import javax.annotation.CheckForNull; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.interceptor.RequirePOST; public class RequiredResourcesProperty extends JobProperty> { - private final String resourceNames; - private final String resourceNamesVar; - private final String resourceNumber; - private final String labelName; - private final @CheckForNull SecureGroovyScript resourceMatchScript; - - @DataBoundConstructor - public RequiredResourcesProperty(String resourceNames, - String resourceNamesVar, String resourceNumber, - String labelName, @CheckForNull SecureGroovyScript resourceMatchScript) { - super(); - - if (resourceNames == null || resourceNames.trim().isEmpty()) { - this.resourceNames = null; - } else { - this.resourceNames = resourceNames.trim(); - } - if (resourceNamesVar == null || resourceNamesVar.trim().isEmpty()) { - this.resourceNamesVar = null; - } else { - this.resourceNamesVar = resourceNamesVar.trim(); - } - if (resourceNumber == null || resourceNumber.trim().isEmpty()) { - this.resourceNumber = null; - } else { - this.resourceNumber = resourceNumber.trim(); - } - String labelNamePreparation = (labelName == null || labelName.trim().isEmpty()) ? null : labelName.trim(); - if (resourceMatchScript != null) { - this.resourceMatchScript = resourceMatchScript.configuringWithKeyItem(); - this.labelName = labelNamePreparation; - } else if (labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) { - this.resourceMatchScript = new SecureGroovyScript(labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), - false, null).configuring(ApprovalContext.create()); - this.labelName = null; - } else { - this.resourceMatchScript = null; - this.labelName = labelNamePreparation; - } - } - - @Deprecated - public RequiredResourcesProperty(String resourceNames, - String resourceNamesVar, String resourceNumber, - String labelName) { - this(resourceNames, resourceNamesVar, resourceNumber, labelName, null); - } - - private Object readResolve() { - // SECURITY-368 migration logic - if (resourceMatchScript == null && labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) { - return new RequiredResourcesProperty(resourceNames, resourceNamesVar, resourceNumber, null, - new SecureGroovyScript(labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null) - .configuring(ApprovalContext.create())); - } - - return this; - } - - public String[] getResources() { - String names = Util.fixEmptyAndTrim(resourceNames); - if (names != null) - return names.split("\\s+"); - else - return new String[0]; - } - - public String getResourceNames() { - return resourceNames; - } - - public String getResourceNamesVar() { - return resourceNamesVar; - } - - public String getResourceNumber() { - return resourceNumber; - } - - public String getLabelName() { - return labelName; - } - - /** - * Gets a system Groovy script to be executed in order to determine if the {@link LockableResource} matches the condition. - * @return System Groovy Script if defined - * @since TODO - * @see LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript, java.util.Map) - */ - @CheckForNull - public SecureGroovyScript getResourceMatchScript() { - return resourceMatchScript; - } - - @Extension - public static class DescriptorImpl extends JobPropertyDescriptor { - - @Override - public String getDisplayName() { - return "Required Lockable Resources"; - } - - @Override - public boolean isApplicable(Class jobType) { - return AbstractProject.class.isAssignableFrom(jobType); - } - - @Override - public RequiredResourcesProperty newInstance(StaplerRequest req, JSONObject formData) throws FormException { - if (formData.containsKey("required-lockable-resources")) { - return (RequiredResourcesProperty) super.newInstance(req, formData.getJSONObject("required-lockable-resources")); - } - return null; - } - - public FormValidation doCheckResourceNames(@QueryParameter String value, - @QueryParameter String labelName, - @QueryParameter boolean script) { - String labelVal = Util.fixEmptyAndTrim(labelName); - String names = Util.fixEmptyAndTrim(value); - - if (names == null) { - return FormValidation.ok(); - } else if (labelVal != null || script) { - return FormValidation.error( - "Only label, groovy expression, or resources can be defined, not more than one."); - } else { - List wrongNames = new ArrayList(); - for (String name : names.split("\\s+")) { - boolean found = false; - for (LockableResource r : LockableResourcesManager.get() - .getResources()) { - if (r.getName().equals(name)) { - found = true; - break; - } - } - if (!found) - wrongNames.add(name); - } - if (wrongNames.isEmpty()) { - return FormValidation.ok(); - } else { - return FormValidation - .error("The following resources do not exist: " - + wrongNames); - } - } - } - - public FormValidation doCheckLabelName( - @QueryParameter String value, - @QueryParameter String resourceNames, - @QueryParameter boolean script) { - String label = Util.fixEmptyAndTrim(value); - String names = Util.fixEmptyAndTrim(resourceNames); - - if (label == null) { - return FormValidation.ok(); - } else if (names != null || script) { - return FormValidation.error( - "Only label, groovy expression, or resources can be defined, not more than one."); - } else { - if (LockableResourcesManager.get().isValidLabel(label)) { - return FormValidation.ok(); - } else { - return FormValidation.error( - "The label does not exist: " + label); - } - } - } - - public FormValidation doCheckResourceNumber(@QueryParameter String value, - @QueryParameter String resourceNames, + private final String resourceNames; + private final String resourceNamesVar; + private final String resourceNumber; + private final String labelName; + private final @CheckForNull SecureGroovyScript resourceMatchScript; + + @DataBoundConstructor + public RequiredResourcesProperty( + String resourceNames, + String resourceNamesVar, + String resourceNumber, + String labelName, + @CheckForNull SecureGroovyScript resourceMatchScript) + throws Descriptor.FormException { + super(); + + if (resourceNames == null || resourceNames.trim().isEmpty()) { + this.resourceNames = null; + } else { + this.resourceNames = resourceNames.trim(); + } + if (resourceNamesVar == null || resourceNamesVar.trim().isEmpty()) { + this.resourceNamesVar = null; + } else { + this.resourceNamesVar = resourceNamesVar.trim(); + } + if (resourceNumber == null || resourceNumber.trim().isEmpty()) { + this.resourceNumber = null; + } else { + this.resourceNumber = resourceNumber.trim(); + } + String labelNamePreparation = (labelName == null || labelName.trim().isEmpty()) ? null : labelName.trim(); + if (resourceMatchScript != null) { + this.resourceMatchScript = resourceMatchScript.configuringWithKeyItem(); + this.labelName = labelNamePreparation; + } else if (labelName != null && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) { + this.resourceMatchScript = new SecureGroovyScript( + labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null) + .configuring(ApprovalContext.create()); + this.labelName = null; + } else { + this.resourceMatchScript = null; + this.labelName = labelNamePreparation; + } + } + + /** + * @deprecated groovy script was added (since 2.0) + */ + @Deprecated + @ExcludeFromJacocoGeneratedReport + public RequiredResourcesProperty( + String resourceNames, String resourceNamesVar, String resourceNumber, String labelName) + throws Descriptor.FormException { + this(resourceNames, resourceNamesVar, resourceNumber, labelName, null); + } + + private Object readResolve() throws Descriptor.FormException { + // SECURITY-368 migration logic + if (resourceMatchScript == null + && labelName != null + && labelName.startsWith(LockableResource.GROOVY_LABEL_MARKER)) { + return new RequiredResourcesProperty( + resourceNames, + resourceNamesVar, + resourceNumber, + null, + new SecureGroovyScript( + labelName.substring(LockableResource.GROOVY_LABEL_MARKER.length()), false, null) + .configuring(ApprovalContext.create())); + } + + return this; + } + + public String[] getResources() { + String names = Util.fixEmptyAndTrim(resourceNames); + if (names != null) return names.split("\\s+"); + else return new String[0]; + } + + public String getResourceNames() { + return resourceNames; + } + + public String getResourceNamesVar() { + return resourceNamesVar; + } + + public String getResourceNumber() { + return resourceNumber; + } + + public String getLabelName() { + return labelName; + } + + /** + * Gets a system Groovy script to be executed in order to determine if the {@link + * LockableResource} matches the condition. + * + * @return System Groovy Script if defined + * @since 2.0 + * @see + * LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript, + * java.util.Map) + */ + @CheckForNull + public SecureGroovyScript getResourceMatchScript() { + return resourceMatchScript; + } + + @Extension + public static class DescriptorImpl extends JobPropertyDescriptor { + + @NonNull + @Override + public String getDisplayName() { + return Messages.RequiredResourcesProperty_displayName(); + } + + @Override + public boolean isApplicable(Class jobType) { + return AbstractProject.class.isAssignableFrom(jobType); + } + + @Override + public RequiredResourcesProperty newInstance(StaplerRequest2 req, JSONObject formData) throws FormException { + if (formData.containsKey("required-lockable-resources")) { + return (RequiredResourcesProperty) + super.newInstance(req, formData.getJSONObject("required-lockable-resources")); + } + return null; + } + + @RequirePOST + public FormValidation doCheckResourceNames( + @QueryParameter String value, @QueryParameter String labelName, - @QueryParameter String resourceMatchScript) - { + @QueryParameter boolean script, + @AncestorInPath Item item) { + // check permission, security first + checkPermission(item); + + String labelVal = Util.fixEmptyAndTrim(labelName); + String names = Util.fixEmptyAndTrim(value); + + if (names == null) { + return FormValidation.ok(); + } else if (labelVal != null || script) { + return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified()); + } else { + List wrongNames = new ArrayList<>(); + for (String name : names.split("\\s+")) { + boolean found = LockableResourcesManager.get().resourceExist(name); + if (!found) wrongNames.add(name); + } + if (wrongNames.isEmpty()) { + return FormValidation.ok(); + } else { + return FormValidation.error(Messages.error_resourceDoesNotExist(wrongNames)); + } + } + } + + @RequirePOST + public FormValidation doCheckLabelName( + @QueryParameter String value, + @QueryParameter String resourceNames, + @QueryParameter boolean script, + @AncestorInPath Item item) { + // check permission, security first + checkPermission(item); + + String label = Util.fixEmptyAndTrim(value); + String names = Util.fixEmptyAndTrim(resourceNames); + + if (label == null) { + return FormValidation.ok(); + } else if (names != null || script) { + return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified()); + } else { + if (LockableResourcesManager.get().isValidLabel(label)) { + return FormValidation.ok(); + } else { + return FormValidation.error(Messages.error_labelDoesNotExist(label)); + } + } + } - String number = Util.fixEmptyAndTrim(value); - String names = Util.fixEmptyAndTrim(resourceNames); - String label = Util.fixEmptyAndTrim(labelName); + @RequirePOST + public FormValidation doCheckResourceNumber( + @QueryParameter String value, + @QueryParameter String resourceNames, + @QueryParameter String labelName, + @QueryParameter String resourceMatchScript, + @AncestorInPath Item item) { + // check permission, security first + checkPermission(item); + + String number = Util.fixEmptyAndTrim(value); + String names = Util.fixEmptyAndTrim(resourceNames); + String label = Util.fixEmptyAndTrim(labelName); String script = Util.fixEmptyAndTrim(resourceMatchScript); - if (number == null || number.equals("") || number.trim().equals("0")) { - return FormValidation.ok(); - } - - int numAsInt; - try { - numAsInt = Integer.parseInt(number); - } catch(NumberFormatException e) { - return FormValidation.error( - "Could not parse the given value as integer."); - } - int numResources = 0; - if (names != null) { - numResources = names.split("\\s+").length; + if (number == null || number.isEmpty() || number.trim().equals("0")) { + return FormValidation.ok(); + } + + int numAsInt; + try { + numAsInt = Integer.parseInt(number); + } catch (NumberFormatException e) { + return FormValidation.error(Messages.error_couldNotParseToint()); + } + int numResources = 0; + if (names != null) { + numResources = names.split("\\s+").length; } else if (label != null || script != null) { - numResources = Integer.MAX_VALUE; + numResources = Integer.MAX_VALUE; } - if (numResources < numAsInt) { - return FormValidation.error(String.format( - "Given amount %d is greater than amount of resources: %d.", - numAsInt, - numResources)); - } - return FormValidation.ok(); - } - - public AutoCompletionCandidates doAutoCompleteLabelName( - @QueryParameter String value) { - AutoCompletionCandidates c = new AutoCompletionCandidates(); - - value = Util.fixEmptyAndTrim(value); - - for (String l : LockableResourcesManager.get().getAllLabels()) - if (value != null && l.startsWith(value)) - c.add(l); - - return c; - } - - public static AutoCompletionCandidates doAutoCompleteResourceNames( - @QueryParameter String value) { - AutoCompletionCandidates c = new AutoCompletionCandidates(); - - value = Util.fixEmptyAndTrim(value); - - if (value != null) { - for (LockableResource r : LockableResourcesManager.get() - .getResources()) { - if (r.getName().startsWith(value)) - c.add(r.getName()); - } - } - - return c; - } - } -} + if (numResources < numAsInt) { + return FormValidation.error(String.format( + Messages.error_givenAmountIsGreaterThatResourcesAmount(), numAsInt, numResources)); + } + return FormValidation.ok(); + } + @RequirePOST + public AutoCompletionCandidates doAutoCompleteLabelName( + @QueryParameter String value, @AncestorInPath Item item) { + // check permission, security first + checkPermission(item); + + AutoCompletionCandidates c = new AutoCompletionCandidates(); + + value = Util.fixEmptyAndTrim(value); + + if (value == null) { + return c; + } + for (String l : LockableResourcesManager.get().getAllLabels()) { + if (l.startsWith(value)) { + c.add(l); + } + } + + return c; + } + + @RequirePOST + public static AutoCompletionCandidates doAutoCompleteResourceNames( + @QueryParameter String value, @AncestorInPath Item item) { + // check permission, security first + checkPermission(item); + + AutoCompletionCandidates c = new AutoCompletionCandidates(); + + value = Util.fixEmptyAndTrim(value); + + if (value == null) { + return c; + } + List allNames = LockableResourcesManager.get().getAllResourcesNames(); + for (String name : allNames) { + if (name.startsWith(value)) { + c.add(name); + } + } + + return c; + } + + private static void checkPermission(Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java b/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java new file mode 100644 index 000000000..0b1152731 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/ResourceSelectStrategy.java @@ -0,0 +1,6 @@ +package org.jenkins.plugins.lockableresources; + +public enum ResourceSelectStrategy { + SEQUENTIAL, + RANDOM +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java index 08770d5ae..4c9a4969b 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java @@ -8,163 +8,710 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.actions; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import hudson.model.Api; +import hudson.model.Descriptor; import hudson.model.RootAction; -import hudson.model.User; -import hudson.security.AccessDeniedException2; +import hudson.model.Run; +import hudson.security.AccessDeniedException3; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.PermissionScope; - +import jakarta.servlet.ServletException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Set; - -import javax.servlet.ServletException; - +import java.util.logging.Logger; import jenkins.model.Jenkins; - import org.jenkins.plugins.lockableresources.LockableResource; import org.jenkins.plugins.lockableresources.LockableResourcesManager; import org.jenkins.plugins.lockableresources.Messages; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; +import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct; +import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.stapler.interceptor.RequirePOST; @Extension +@ExportedBean public class LockableResourcesRootAction implements RootAction { - public static final PermissionGroup PERMISSIONS_GROUP = new PermissionGroup( - LockableResourcesManager.class, Messages._LockableResourcesRootAction_PermissionGroup()); - public static final Permission UNLOCK = new Permission(PERMISSIONS_GROUP, - Messages.LockableResourcesRootAction_UnlockPermission(), - Messages._LockableResourcesRootAction_UnlockPermission_Description(), Jenkins.ADMINISTER, - PermissionScope.JENKINS); - public static final Permission RESERVE = new Permission(PERMISSIONS_GROUP, - Messages.LockableResourcesRootAction_ReservePermission(), - Messages._LockableResourcesRootAction_ReservePermission_Description(), Jenkins.ADMINISTER, - PermissionScope.JENKINS); - - public static final String ICON = "/plugin/lockable-resources/img/device-24x24.png"; - - public String getIconFileName() { - if (User.current() != null) { - // only show if logged in - return ICON; - } else { - return null; - } - } - - public String getUserName() { - User current = User.current(); - if (current != null) - return current.getFullName(); - else - return null; - } - - public String getDisplayName() { - return "Lockable Resources"; - } - - public String getUrlName() { - return "lockable-resources"; - } - - public List getResources() { - return LockableResourcesManager.get().getResources(); - } - - public int getFreeResourceAmount(String label) { - return LockableResourcesManager.get().getFreeResourceAmount(label); - } - - public Set getAllLabels() { - return LockableResourcesManager.get().getAllLabels(); - } - - public int getNumberOfAllLabels() { - return LockableResourcesManager.get().getAllLabels().size(); - } - - public void doUnlock(StaplerRequest req, StaplerResponse rsp) - throws IOException, ServletException { - Jenkins.getInstance().checkPermission(UNLOCK); - - String name = req.getParameter("resource"); - LockableResource r = LockableResourcesManager.get().fromName(name); - if (r == null) { - rsp.sendError(404, "Resource not found " + name); - return; - } - - List resources = new ArrayList(); - resources.add(r); - LockableResourcesManager.get().unlock(resources, null); - - rsp.forwardToPreviousPage(req); - } - - public void doReserve(StaplerRequest req, StaplerResponse rsp) - throws IOException, ServletException { - Jenkins.getInstance().checkPermission(RESERVE); - - String name = req.getParameter("resource"); - LockableResource r = LockableResourcesManager.get().fromName(name); - if (r == null) { - rsp.sendError(404, "Resource not found " + name); - return; - } - - List resources = new ArrayList(); - resources.add(r); - String userName = getUserName(); - if (userName != null) - LockableResourcesManager.get().reserve(resources, userName); - - rsp.forwardToPreviousPage(req); - } - - public void doUnreserve(StaplerRequest req, StaplerResponse rsp) - throws IOException, ServletException { - Jenkins.getInstance().checkPermission(RESERVE); - - String name = req.getParameter("resource"); - LockableResource r = LockableResourcesManager.get().fromName(name); - if (r == null) { - rsp.sendError(404, "Resource not found " + name); - return; - } - - String userName = getUserName(); - if ((userName == null || !userName.equals(r.getReservedBy())) - && !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) - throw new AccessDeniedException2(Jenkins.getAuthentication(), - RESERVE); - - List resources = new ArrayList(); - resources.add(r); - LockableResourcesManager.get().unreserve(resources); - - rsp.forwardToPreviousPage(req); - } - - public void doReset(StaplerRequest req, StaplerResponse rsp) - throws IOException, ServletException { - Jenkins.getInstance().checkPermission(UNLOCK); - - String name = req.getParameter("resource"); - LockableResource r = LockableResourcesManager.get().fromName(name); - if (r == null) { - rsp.sendError(404, "Resource not found " + name); - return; - } - - List resources = new ArrayList(); - resources.add(r); - LockableResourcesManager.get().reset(resources); - - rsp.forwardToPreviousPage(req); - } + private static final Logger LOGGER = Logger.getLogger(LockableResourcesRootAction.class.getName()); + + public static final PermissionGroup PERMISSIONS_GROUP = new PermissionGroup( + LockableResourcesManager.class, Messages._LockableResourcesRootAction_PermissionGroup()); + public static final Permission UNLOCK = new Permission( + PERMISSIONS_GROUP, + "Unlock", + Messages._LockableResourcesRootAction_UnlockPermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); + public static final Permission RESERVE = new Permission( + PERMISSIONS_GROUP, + "Reserve", + Messages._LockableResourcesRootAction_ReservePermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); + public static final Permission STEAL = new Permission( + PERMISSIONS_GROUP, + "Steal", + Messages._LockableResourcesRootAction_StealPermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); + public static final Permission VIEW = new Permission( + PERMISSIONS_GROUP, + "View", + Messages._LockableResourcesRootAction_ViewPermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); + public static final Permission QUEUE = new Permission( + PERMISSIONS_GROUP, + "Queue", + Messages._LockableResourcesRootAction_QueueChangeOrderPermission_Description(), + Jenkins.ADMINISTER, + PermissionScope.JENKINS); + + public static final String ICON = "symbol-lock-closed"; + + @Override + public String getIconFileName() { + return Jenkins.get().hasPermission(VIEW) ? ICON : null; + } + + public Api getApi() { + return new Api(this); + } + + @CheckForNull + public String getUserName() { + return LockableResource.getUserName(); + } + + @Override + public String getDisplayName() { + return Messages.LockableResourcesRootAction_PermissionGroup(); + } + + @Override + public String getUrlName() { + return Jenkins.get().hasPermission(VIEW) ? "lockable-resources" : ""; + } + + // --------------------------------------------------------------------------- + /** + * Get a list of resources + * + * @return All resources. + */ + @Exported + @Restricted(NoExternalUse.class) // used by jelly + public List getResources() { + return LockableResourcesManager.get().getReadOnlyResources(); + } + + // --------------------------------------------------------------------------- + /** + * Get a list of all labels + * + * @return All possible labels. + */ + @Restricted(NoExternalUse.class) // used by jelly + public LinkedHashMap getLabelsList() { + LinkedHashMap map = new LinkedHashMap<>(); + + for (LockableResource r : LockableResourcesManager.get().getReadOnlyResources()) { + if (r == null || r.getName().isEmpty()) { + continue; // defensive, shall never happens, but ... + } + List assignedLabels = r.getLabelsAsList(); + if (assignedLabels.isEmpty()) { + continue; + } + + for (String labelString : assignedLabels) { + if (labelString == null || labelString.isEmpty()) { + continue; // defensive, shall never happens, but ... + } + LockableResourcesLabel label = map.get(labelString); + if (label == null) { + label = new LockableResourcesLabel(labelString); + } + + label.update(r); + + map.put(labelString, label); + } + } + + return map; + } + + // --------------------------------------------------------------------------- + public static class LockableResourcesLabel { + String name; + int free; + int assigned; + + // ------------------------------------------------------------------------- + public LockableResourcesLabel(String _name) { + this.name = _name; + this.free = 0; + this.assigned = 0; + } + + // ------------------------------------------------------------------------- + public void update(LockableResource resource) { + this.assigned++; + if (resource.isFree()) free++; + } + + // ------------------------------------------------------------------------- + public String getName() { + return this.name; + } + + // ------------------------------------------------------------------------- + public int getFree() { + return this.free; + } + + // ------------------------------------------------------------------------- + public int getAssigned() { + return this.assigned; + } + + // ------------------------------------------------------------------------- + public int getPercentage() { + if (this.assigned == 0) { + return this.assigned; + } + return (int) ((double) this.free / (double) this.assigned * 100); + } + } + + // --------------------------------------------------------------------------- + // used by by + // src\main\resources\org\jenkins\plugins\lockableresources\actions\LockableResourcesRootAction\tableResources\table.jelly + @Restricted(NoExternalUse.class) + public LockableResource getResource(final String resourceName) { + return LockableResourcesManager.get().fromName(resourceName); + } + + // --------------------------------------------------------------------------- + /** + * Get amount of free resources assigned to given *labelString* + * + * @param labelString Label to search. + * @return Amount of free labels. + */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public int getFreeResourceAmount(final String labelString) { + this.informPerformanceIssue(); + LockableResourcesLabel label = this.getLabelsList().get(labelString); + return (label == null) ? 0 : label.getFree(); + } + + // --------------------------------------------------------------------------- + /** + * Get percentage (0-100) usage of resources assigned to given *labelString* + * + *

Used by {@code actions/LockableResourcesRootAction/index.jelly} + * + * @since 2.19 + * @param labelString Label to search. + * @return Percentage usages of *labelString* around all resources + */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public int getFreeResourcePercentage(final String labelString) { + this.informPerformanceIssue(); + LockableResourcesLabel label = this.getLabelsList().get(labelString); + return (label == null) ? 0 : label.getPercentage(); + } + + // --------------------------------------------------------------------------- + /** + * Get all existing labels as list. + * + * @return All possible labels. + */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public Set getAllLabels() { + this.informPerformanceIssue(); + return LockableResourcesManager.get().getAllLabels(); + } + + // --------------------------------------------------------------------------- + /** + * Get amount of all labels. + * + * @return Amount of all labels. + */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public int getNumberOfAllLabels() { + this.informPerformanceIssue(); + return this.getLabelsList().size(); + } + + // --------------------------------------------------------------------------- + /** + * Get amount of resources assigned to given *labelString* + * + *

Used by {@code actions/LockableResourcesRootAction/index.jelly} + * + * @param labelString Label to search. + * @return Amount of assigned resources. + */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public int getAssignedResourceAmount(String labelString) { + this.informPerformanceIssue(); + return LockableResourcesManager.get().getResourcesWithLabel(labelString).size(); + } + + // --------------------------------------------------------------------------- + private void informPerformanceIssue() { + String method = Thread.currentThread().getStackTrace()[2].getMethodName(); + StringBuilder buf = new StringBuilder(); + for (StackTraceElement st : Thread.currentThread().getStackTrace()) { + buf.append("\n").append(st); + } + LOGGER.warning("lockable-resources-plugin: The method " + + method + + " has been deprecated due performance issues. When you see this message, please inform plugin developers:" + + buf); + } + + // --------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public Queue getQueue() throws Descriptor.FormException { + List currentQueueContext = + List.copyOf(LockableResourcesManager.get().getCurrentQueuedContext()); + Queue queue = new Queue(); + + for (QueuedContextStruct context : currentQueueContext) { + for (LockableResourcesStruct resourceStruct : context.getResources()) { + queue.add(resourceStruct, context); + } + } + + return queue; + } + + // --------------------------------------------------------------------------- + public static class Queue { + + List queue; + QueueStruct oldest; + + // ------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public Queue() { + this.queue = new ArrayList<>(); + } + + // ------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public void add(final LockableResourcesStruct resourceStruct, final QueuedContextStruct context) + throws Descriptor.FormException { + QueueStruct queueStruct = new QueueStruct(resourceStruct, context); + queue.add(queueStruct); + if (resourceStruct.queuedAt == 0) { + // Older versions of this plugin might miss this information. + // Therefore skip it here. + return; + } + if (oldest == null || oldest.getQueuedAt() > queueStruct.getQueuedAt()) { + oldest = queueStruct; + } + } + + // ------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public List getAll() { + return Collections.unmodifiableList(this.queue); + } + + // ------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public QueueStruct getOldest() { + return this.oldest; + } + + // ------------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public static class QueueStruct { + List requiredResources; + String requiredLabel; + String groovyScript; + String requiredNumber; + long queuedAt = 0; + int priority = 0; + String id = null; + Run build; + + public QueueStruct(final LockableResourcesStruct resourceStruct, final QueuedContextStruct context) + throws Descriptor.FormException { + this.requiredResources = resourceStruct.required; + this.requiredLabel = resourceStruct.label; + this.requiredNumber = resourceStruct.requiredNumber; + this.queuedAt = resourceStruct.queuedAt; + this.build = context.getBuild(); + this.priority = context.getPriority(); + this.id = context.getId(); + + final SecureGroovyScript systemGroovyScript = resourceStruct.getResourceMatchScript(); + if (systemGroovyScript != null) { + this.groovyScript = systemGroovyScript.getScript(); + } + } + + // ----------------------------------------------------------------------- + /** */ + @Restricted(NoExternalUse.class) // used by jelly + public List getRequiredResources() { + return this.requiredResources; + } + + // ----------------------------------------------------------------------- + /** */ + @NonNull + @Restricted(NoExternalUse.class) // used by jelly + public String getRequiredLabel() { + return this.requiredLabel == null ? "N/A" : this.requiredLabel; + } + + // ----------------------------------------------------------------------- + /** */ + @NonNull + @Restricted(NoExternalUse.class) // used by jelly + public String getRequiredNumber() { + return this.requiredNumber == null ? "0" : this.requiredNumber; + } + + // ----------------------------------------------------------------------- + /** */ + @NonNull + @Restricted(NoExternalUse.class) // used by jelly + public String getGroovyScript() { + return this.groovyScript == null ? "N/A" : this.groovyScript; + } + + // ----------------------------------------------------------------------- + /** */ + @Restricted(NoExternalUse.class) // used by jelly + public Run getBuild() { + return this.build; + } + + // ----------------------------------------------------------------------- + /** */ + @Restricted(NoExternalUse.class) // used by jelly + public long getQueuedAt() { + return this.queuedAt; + } + + // ----------------------------------------------------------------------- + /** Check if the queue takes too long. At the moment "too long" means over 1 hour. */ + @Restricted(NoExternalUse.class) // used by jelly + public boolean takeTooLong() { + return (new Date().getTime() - this.queuedAt) > 3600000L; + } + + // ----------------------------------------------------------------------- + /** Returns timestamp when the resource has been added into queue. */ + @Restricted(NoExternalUse.class) // used by jelly + public Date getQueuedTimestamp() { + return new Date(this.queuedAt); + } + + // ----------------------------------------------------------------------- + /** Returns queue priority. */ + @Restricted(NoExternalUse.class) // used by jelly + public int getPriority() { + if (this.id == null) { + // defensive + // in case of jenkins update from older version and you have some queue + // might happens, that there are no priority set + return 0; + } + return this.priority; + } + + // ----------------------------------------------------------------------- + /** Returns queue ID. */ + @Restricted(NoExternalUse.class) + public String getId() { + if (this.id == null) { + // defensive + // in case of jenkins update from older version and you have some queue + // might happens, that there are no priority set + return "NN"; + } + return this.id; + } + + @Restricted(NoExternalUse.class) // used by jelly + public boolean resourcesMatch() { + return (requiredResources != null && requiredResources.size() > 0); + } + + // ----------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public boolean labelsMatch() { + return (requiredLabel != null); + } + + // ----------------------------------------------------------------------- + @Restricted(NoExternalUse.class) // used by jelly + public boolean scriptMatch() { + return (groovyScript != null && !groovyScript.isEmpty()); + } + } + } + + // --------------------------------------------------------------------------- + /** Returns current queue */ + @Restricted(NoExternalUse.class) // used by jelly + @Deprecated // slow down plugin execution due concurrent modification checks + public List getCurrentQueuedContext() { + return LockableResourcesManager.get().getCurrentQueuedContext(); + } + + // --------------------------------------------------------------------------- + /** Returns current queue */ + @Restricted(NoExternalUse.class) // used by jelly + @CheckForNull + @Deprecated // slow down plugin execution due concurrent modification checks + public LockableResourcesStruct getOldestQueue() { + LockableResourcesStruct oldest = null; + for (QueuedContextStruct context : this.getCurrentQueuedContext()) { + for (LockableResourcesStruct resourceStruct : context.getResources()) { + if (resourceStruct.queuedAt == 0) { + // Older versions of this plugin might miss this information. + // Therefore skip it here. + continue; + } + if (oldest == null || oldest.queuedAt > resourceStruct.queuedAt) { + oldest = resourceStruct; + } + } + } + return oldest; + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doUnlock(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(UNLOCK); + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + LockableResourcesManager.get().unlockResources(resources); + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doReserve(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(RESERVE); + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + String userName = getUserName(); + if (userName != null) { + if (!LockableResourcesManager.get().reserve(resources, userName)) { + rsp.sendError( + 423, + Messages.error_resourceAlreadyLocked(LockableResourcesManager.getResourcesNames(resources))); + return; + } + } + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doSteal(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(STEAL); + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + String userName = getUserName(); + if (userName != null) { + LockableResourcesManager.get().steal(resources, userName); + } + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doReassign(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(STEAL); + + String userName = getUserName(); + if (userName == null) { + // defensive: this can not happens because we check you permissions few lines before + // therefore you must be logged in + throw new AccessDeniedException3(Jenkins.getAuthentication2(), STEAL); + } + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + for (LockableResource resource : resources) { + if (userName.equals(resource.getReservedBy())) { + // Can not achieve much by re-assigning the + // resource I already hold to myself again, + // that would just burn the compute resources. + // ...unless something catches the event? (TODO?) + return; + } + } + + LockableResourcesManager.get().reassign(resources, userName); + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doUnreserve(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(RESERVE); + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + String userName = getUserName(); + for (LockableResource resource : resources) { + if ((userName == null || !userName.equals(resource.getReservedBy())) + && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + throw new AccessDeniedException3(Jenkins.getAuthentication2(), RESERVE); + } + } + + LockableResourcesManager.get().unreserve(resources); + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doReset(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(UNLOCK); + // Should this also be permitted by "STEAL"?.. + + List resources = this.getResourcesFromRequest(req, rsp); + if (resources == null) { + return; + } + + LockableResourcesManager.get().reset(resources); + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + @RequirePOST + public void doSaveNote(final StaplerRequest2 req, final StaplerResponse2 rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(RESERVE); + + String resourceName = req.getParameter("resource"); + if (resourceName == null) { + resourceName = req.getParameter("resourceName"); + } + + final LockableResource resource = getResource(resourceName); + if (resource == null) { + rsp.sendError(404, Messages.error_resourceDoesNotExist(resourceName)); + } else { + String resourceNote = req.getParameter("note"); + if (resourceNote == null) { + resourceNote = req.getParameter("resourceNote"); + } + resource.setNote(resourceNote); + LockableResourcesManager.get().save(); + + rsp.forwardToPreviousPage(req); + } + } + + // --------------------------------------------------------------------------- + /** Change queue order (item position) */ + @Restricted(NoExternalUse.class) // used by jelly + @RequirePOST + public void doChangeQueueOrder(final StaplerRequest2 req, final StaplerResponse2 rsp) + throws IOException, ServletException { + Jenkins.get().checkPermission(QUEUE); + + final String queueId = req.getParameter("id"); + final String newIndexStr = req.getParameter("index"); + + LOGGER.fine("doChangeQueueOrder, id: " + queueId + " newIndexStr: " + newIndexStr); + + final int newIndex; + try { + newIndex = Integer.parseInt(newIndexStr); + } catch (NumberFormatException e) { + rsp.sendError(423, Messages.error_isNotANumber(newIndexStr)); + return; + } + + try { + LockableResourcesManager.get().changeQueueOrder(queueId, newIndex - 1); + } catch (IOException e) { + rsp.sendError(423, e.toString().replace("java.io.IOException: ", "")); + return; + } + + rsp.forwardToPreviousPage(req); + } + + // --------------------------------------------------------------------------- + private List getResourcesFromRequest(final StaplerRequest2 req, final StaplerResponse2 rsp) + throws IOException, ServletException { + // todo, when you try to improve the API to use multiple resources (a list instead of single + // one) + // this will be the best place to change it. Probably it will be enough to add a code piece here + // like req.getParameter("resources"); And split the content by some delimiter like ' ' (space) + String name = req.getParameter("resource"); + LockableResource r = LockableResourcesManager.get().fromName(name); + if (r == null) { + rsp.sendError(404, Messages.error_resourceDoesNotExist(name)); + return null; + } + + List resources = new ArrayList<>(); + resources.add(r); + return resources; + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockedResourcesBuildAction.java b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockedResourcesBuildAction.java index 8d7a7d322..c673ef1db 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockedResourcesBuildAction.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockedResourcesBuildAction.java @@ -1,76 +1,149 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (c) 2013, 6WIND S.A. All rights reserved. * - * * - * This file is part of the Jenkins Lockable Resources Plugin and is * - * published under the MIT license. * - * * - * See the "LICENSE.txt" file for more information. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.actions; import hudson.model.Action; - +import hudson.model.Run; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; +import java.util.Date; import java.util.List; - -import org.jenkins.plugins.lockableresources.LockableResource; - +import org.jenkins.plugins.lockableresources.Messages; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +// ----------------------------------------------------------------------------- +/** BuildAction for lockable resources. + * Shows usage of resources in the build page. + * url: jobUrl/buildNr/locked-resources/ + */ +@Restricted(NoExternalUse.class) public class LockedResourcesBuildAction implements Action { - private final List lockedResources; - - public LockedResourcesBuildAction(List lockedResources) { - this.lockedResources = lockedResources; - } - - public List getLockedResources() { - return lockedResources; - } - - public String getIconFileName() { - return LockableResourcesRootAction.ICON; - } - - public String getDisplayName() { - return "Locked Resources"; - } - - public String getUrlName() { - return "locked-resources"; - } - - public static LockedResourcesBuildAction fromResources( - Collection resources) { - List resPojos = new ArrayList(); - for (LockableResource r : resources) - resPojos.add(new ResourcePOJO(r)); - return new LockedResourcesBuildAction(resPojos); - } - - public static class ResourcePOJO { - - private String name; - private String description; - - public ResourcePOJO(String name, String description) { - this.name = name; - this.description = description; - } - - public ResourcePOJO(LockableResource r) { - this.name = r.getName(); - this.description = r.getDescription(); - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - } - + private final List logs = new ArrayList<>(); + private final transient Object syncLogs = new Object(); + private final List resourcesInUse = new ArrayList<>(); + + public LockedResourcesBuildAction() {} + + // ------------------------------------------------------------------------- + @Override + public String getIconFileName() { + return LockableResourcesRootAction.ICON; + } + + // ------------------------------------------------------------------------- + @Override + public String getDisplayName() { + return Messages.LockedResourcesBuildAction_displayName(); + } + + // ------------------------------------------------------------------------- + @Override + public String getUrlName() { + return "locked-resources"; + } + + public List getCurrentUsedResourceNames() { + return resourcesInUse; + } + + public void addUsedResources(List resourceNames) { + synchronized (resourcesInUse) { + resourcesInUse.addAll(resourceNames); + } + } + + public void removeUsedResources(List resourceNames) { + synchronized (resourcesInUse) { + resourcesInUse.removeAll(resourceNames); + } + } + + public static LockedResourcesBuildAction findAndInitAction(final Run build) { + if (build == null) { + return null; + } + LockedResourcesBuildAction action; + final Object lock = build.getId(); + // It is very difficult to guarantee correct operation when synchronizing on a parameter. + // There is no control over the identity, visibility, or lifecycle of that object. + synchronized (lock) { + List actions = build.getActions(LockedResourcesBuildAction.class); + + if (actions.isEmpty()) { + action = new LockedResourcesBuildAction(); + build.addAction(action); + } else { + action = actions.get(0); + } + } + return action; + } + + public static void addLog( + final Run build, final List resourceNames, final String step, final String action) { + + for (String resourceName : resourceNames) addLog(build, resourceName, step, action); + } + + public static void addLog( + final Run build, final String resourceName, final String step, final String action) { + + LockedResourcesBuildAction buildAction = findAndInitAction(build); + + buildAction.addLog(resourceName, step, action); + } + + public void addLog(final String resourceName, final String step, final String action) { + + synchronized (this.logs) { + this.logs.add(new LogEntry(step, action, resourceName)); + } + } + + @Restricted(NoExternalUse.class) + public List getReadOnlyLogs() { + synchronized (this.logs) { + return new ArrayList<>(Collections.unmodifiableCollection(this.logs)); + } + } + + public static class LogEntry { + + private final String step; + private final String action; + private final String resourceName; + private final long timeStamp; + + @Restricted(NoExternalUse.class) + public LogEntry(final String step, final String action, final String resourceName) { + this.step = step; + this.action = action; + this.resourceName = resourceName; + this.timeStamp = new Date().getTime(); + } + + // --------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public String getName() { + return this.resourceName; + } + + // --------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public String getStep() { + return this.step; + } + + // --------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public String getAction() { + return this.action; + } + + // --------------------------------------------------------------------- + @Restricted(NoExternalUse.class) + public Date getTimeStamp() { + return new Date(this.timeStamp); + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/actions/ResourceVariableNameAction.java b/src/main/java/org/jenkins/plugins/lockableresources/actions/ResourceVariableNameAction.java index 3595cd928..9658161aa 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/actions/ResourceVariableNameAction.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/ResourceVariableNameAction.java @@ -1,10 +1,6 @@ package org.jenkins.plugins.lockableresources.actions; -import java.io.IOException; - -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; - +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.EnvVars; import hudson.Extension; import hudson.model.EnvironmentContributor; @@ -12,32 +8,34 @@ import hudson.model.Run; import hudson.model.StringParameterValue; import hudson.model.TaskListener; +import java.util.List; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public class ResourceVariableNameAction extends InvisibleAction { - private final StringParameterValue resourceNameParameter; - - public ResourceVariableNameAction(StringParameterValue r) { - this.resourceNameParameter = r; - } - - StringParameterValue getParameter() { - return resourceNameParameter; - } - - @Extension - public static final class ResourceVariableNameActionEnvironmentContributor extends EnvironmentContributor { - - @Override - public void buildEnvironmentFor(Run r, EnvVars envs, TaskListener listener) - throws IOException, InterruptedException { - ResourceVariableNameAction a = r.getAction(ResourceVariableNameAction.class); - if (a != null && a.getParameter() != null && a.getParameter().getValue() != null) { - envs.put(a.getParameter().getName(), String.valueOf(a.getParameter().getValue())); - } - } - - } - + private final List resourceNameParameter; + + public ResourceVariableNameAction(List r) { + this.resourceNameParameter = r; + } + + List getParameter() { + return resourceNameParameter; + } + + @Extension + public static final class ResourceVariableNameActionEnvironmentContributor extends EnvironmentContributor { + + @Override + public void buildEnvironmentFor(@NonNull Run r, @NonNull EnvVars envs, @NonNull TaskListener listener) { + ResourceVariableNameAction a = r.getAction(ResourceVariableNameAction.class); + if (a != null && a.getParameter() != null) { + for (StringParameterValue envToSet : a.getParameter()) { + envs.override(envToSet.getName(), envToSet.getValue()); + } + } + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/nodes/NodesMirror.java b/src/main/java/org/jenkins/plugins/lockableresources/nodes/NodesMirror.java new file mode 100644 index 000000000..d6d51906b --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/nodes/NodesMirror.java @@ -0,0 +1,106 @@ +package org.jenkins.plugins.lockableresources; + +import hudson.Extension; +import hudson.init.InitMilestone; +import hudson.init.Initializer; +import hudson.model.Node; +import hudson.slaves.ComputerListener; +import java.util.Iterator; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import org.jenkins.plugins.lockableresources.util.Constants; + +// ----------------------------------------------------------------------------- +/** Mirror Jenkins nodes to lockable-resources */ +@Extension +public class NodesMirror extends ComputerListener { + + private static final Logger LOGGER = Logger.getLogger(NodesMirror.class.getName()); + private static LockableResourcesManager lrm; + + // --------------------------------------------------------------------------- + private static boolean isNodeMirrorEnabled() { + return SystemProperties.getBoolean(Constants.SYSTEM_PROPERTY_ENABLE_NODE_MIRROR); + } + + // --------------------------------------------------------------------------- + @Initializer(after = InitMilestone.JOB_LOADED) + public static void createNodeResources() { + LOGGER.info("lockable-resources-plugin: configure node resources"); + mirrorNodes(); + } + + // --------------------------------------------------------------------------- + @Override + public final void onConfigurationChange() { + mirrorNodes(); + } + + // --------------------------------------------------------------------------- + private static void mirrorNodes() { + if (!isNodeMirrorEnabled()) { + return; + } + + LOGGER.info("lockable-resources-plugin: start nodes mirroring"); + lrm = LockableResourcesManager.get(); + synchronized (lrm.syncResources) { + for (Node n : Jenkins.get().getNodes()) { + mirrorNode(n); + } + // please do not remove it, From time to time is necessary for developer debugs + // thx + // lrm.printResources(); + deleteNotExistingNodes(); + // lrm.printResources(); + } + LOGGER.info("lockable-resources-plugin: nodes mirroring finished"); + } + + // --------------------------------------------------------------------------- + private static void deleteNotExistingNodes() { + Iterator resourceIterator = lrm.getResources().iterator(); + while (resourceIterator.hasNext()) { + LockableResource resource = resourceIterator.next(); + if (!resource.isNodeResource() || (Jenkins.get().getNode(resource.getName()) != null)) { + continue; + } + if (resource.isFree()) { + // we can remove this resource. Is newer used currently + LOGGER.config("lockable-resources-plugin: remove node resource '" + resource.getName() + "'."); + resourceIterator.remove(); + } else { + LOGGER.warning("lockable-resources-plugin: can not remove node-resource '" + + resource.getName() + + "'. The resource is currently used (not free)."); + } + } + } + + // --------------------------------------------------------------------------- + private static void mirrorNode(Node node) { + if (node == null) { + return; + } + + LockableResource nodeResource = lrm.fromName(node.getNodeName()); + boolean exist = nodeResource != null; + if (!exist) { + nodeResource = new LockableResource(node.getNodeName()); + LOGGER.config("lockable-resources-plugin: Node-resource '" + nodeResource.getName() + "' will be added."); + } else { + LOGGER.fine("lockable-resources-plugin: Node-resource '" + nodeResource.getName() + "' will be updated."); + } + nodeResource.setLabels( + node.getAssignedLabels().stream().map(Object::toString).collect(Collectors.joining(" "))); + nodeResource.setNodeResource(true); + nodeResource.setEphemeral(false); + nodeResource.setDescription(node.getNodeDescription()); + + if (!exist) { + lrm.addResource(nodeResource); + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java index 88f1fc075..c010dbc9c 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java @@ -8,113 +8,110 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.queue; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; -import hudson.matrix.MatrixBuild; import hudson.model.AbstractBuild; import hudson.model.Job; import hudson.model.Run; +import hudson.model.StringParameterValue; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; -import hudson.model.StringParameterValue; - import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.logging.Logger; - -import org.jenkins.plugins.lockableresources.LockableResourcesManager; +import java.util.stream.Collectors; import org.jenkins.plugins.lockableresources.LockableResource; -import org.jenkins.plugins.lockableresources.actions.LockedResourcesBuildAction; +import org.jenkins.plugins.lockableresources.LockableResourceProperty; +import org.jenkins.plugins.lockableresources.LockableResourcesManager; import org.jenkins.plugins.lockableresources.actions.ResourceVariableNameAction; @Extension public class LockRunListener extends RunListener> { - static final String LOG_PREFIX = "[lockable-resources]"; - static final Logger LOGGER = Logger.getLogger(LockRunListener.class - .getName()); - - @Override - public void onStarted(Run build, TaskListener listener) { - // Skip locking for multiple configuration projects, - // only the child jobs will actually lock resources. - if (build instanceof MatrixBuild) - return; - - if (build instanceof AbstractBuild) { - Job proj = Utils.getProject(build); - Set required = new HashSet(); - if (proj != null) { - LockableResourcesStruct resources = Utils.requiredResources(proj); - - if (resources != null) { - if (resources.requiredNumber != null || !resources.label.isEmpty() || resources.getResourceMatchScript() != null) { - required.addAll(LockableResourcesManager.get(). - getResourcesFromProject(proj.getFullName())); - } else { - required.addAll(resources.required); - } - - if (LockableResourcesManager.get().lock(required, build, null)) { - build.addAction(LockedResourcesBuildAction - .fromResources(required)); - listener.getLogger().printf("%s acquired lock on %s%n", - LOG_PREFIX, required); - LOGGER.fine(build.getFullDisplayName() - + " acquired lock on " + required); - if (resources.requiredVar != null) { - build.addAction(new ResourceVariableNameAction(new StringParameterValue( - resources.requiredVar, - required.toString().replaceAll("[\\]\\[]", "")))); - } - } else { - listener.getLogger().printf("%s failed to lock %s%n", - LOG_PREFIX, required); - LOGGER.fine(build.getFullDisplayName() + " failed to lock " - + required); - } - } - } - } - - return; - } - - @Override - public void onCompleted(Run build, TaskListener listener) { - // Skip unlocking for multiple configuration projects, - // only the child jobs will actually unlock resources. - if (build instanceof MatrixBuild) - return; - - // obviously project name cannot be obtained here - List required = LockableResourcesManager.get() - .getResourcesFromBuild(build); - if (required.size() > 0) { - LockableResourcesManager.get().unlock(required, build); - listener.getLogger().printf("%s released lock on %s%n", - LOG_PREFIX, required); - LOGGER.fine(build.getFullDisplayName() + " released lock on " - + required); - } - - } - - @Override - public void onDeleted(Run build) { - // Skip unlocking for multiple configuration projects, - // only the child jobs will actually unlock resources. - if (build instanceof MatrixBuild) - return; - - List required = LockableResourcesManager.get() - .getResourcesFromBuild(build); - if (required.size() > 0) { - LockableResourcesManager.get().unlock(required, build); - LOGGER.fine(build.getFullDisplayName() + " released lock on " - + required); - } - } - + static final String LOG_PREFIX = "[lockable-resources]"; + static final Logger LOGGER = Logger.getLogger(LockRunListener.class.getName()); + + @Override + public void onStarted(Run build, TaskListener listener) { + // Skip locking for multiple configuration projects, + // only the child jobs will actually lock resources. + if (build.getClass().getName().equals("hudson.matrix.MatrixBuild")) { + return; + } + + if (build instanceof AbstractBuild) { + LockableResourcesManager lrm = LockableResourcesManager.get(); + synchronized (lrm.syncResources) { + Job proj = Utils.getProject(build); + List required = new ArrayList<>(); + + LockableResourcesStruct resources = Utils.requiredResources(proj); + + if (resources != null) { + if (resources.requiredNumber != null + || !resources.label.isEmpty() + || resources.getResourceMatchScriptText() != null) { + required.addAll(lrm.getResourcesFromProject(proj.getFullName())); + } else { + required.addAll(resources.required); + } + + if (lrm.lock(required, build)) { + // build.addAction(LockedResourcesBuildAction.fromResources(required)); + listener.getLogger().printf("%s acquired lock on %s%n", LOG_PREFIX, required); + LOGGER.info(build.getFullDisplayName() + " acquired lock on " + required); + if (resources.requiredVar != null) { + List envsToSet = new ArrayList<>(); + + // add the comma separated list of names acquired + envsToSet.add(new StringParameterValue( + resources.requiredVar, + required.stream() + .map(LockableResource::getName) + .collect(Collectors.joining(",")))); + + // also add a numbered variable for each acquired lock along with properties of the lock + int index = 0; + for (LockableResource lr : required) { + String lockEnvName = resources.requiredVar + index; + envsToSet.add(new StringParameterValue(lockEnvName, lr.getName())); + for (LockableResourceProperty lockProperty : lr.getProperties()) { + String propEnvName = lockEnvName + "_" + lockProperty.getName(); + envsToSet.add(new StringParameterValue(propEnvName, lockProperty.getValue())); + } + ++index; + } + + build.addAction(new ResourceVariableNameAction(envsToSet)); + } + } else { + listener.getLogger().printf("%s failed to lock %s%n", LOG_PREFIX, required); + LOGGER.warning(build.getFullDisplayName() + " failed to lock " + required); + } + } + } + } + } + + @Override + public void onCompleted(Run build, @NonNull TaskListener listener) { + // Skip unlocking for multiple configuration projects, + // only the child jobs will actually unlock resources. + if (build.getClass().getName().equals("hudson.matrix.MatrixBuild")) { + return; + } + LOGGER.info(build.getFullDisplayName()); + LockableResourcesManager.get().unlockBuild(build); + } + + @Override + public void onDeleted(Run build) { + // Skip unlocking for multiple configuration projects, + // only the child jobs will actually unlock resources. + if (build.getClass().getName().equals("hudson.matrix.MatrixBuild")) { + return; + } + LOGGER.info(build.getFullDisplayName()); + LockableResourcesManager.get().unlockBuild(build); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesCandidatesStruct.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesCandidatesStruct.java deleted file mode 100644 index b59e070c1..000000000 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesCandidatesStruct.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.jenkins.plugins.lockableresources.queue; - -import java.util.List; - -import org.jenkins.plugins.lockableresources.LockableResource; - -public class LockableResourcesCandidatesStruct { - - public List candidates; - public int requiredAmount; - public List selected; - - public LockableResourcesCandidatesStruct(List candidates, int requiredAmount) { - this.candidates = candidates; - this.requiredAmount = requiredAmount; - } - - @Override - public String toString() - { - return "LockableResourcesCandidatesStruct [candidates=" + candidates + ", requiredAmount=" + requiredAmount - + ", selected=" + selected + "]"; - } - -} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java index 8fb414dee..d9fc8650f 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java @@ -8,20 +8,17 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.queue; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; -import hudson.matrix.MatrixConfiguration; -import hudson.matrix.MatrixProject; -import hudson.model.AbstractProject; +import hudson.ExtensionList; import hudson.model.Job; +import hudson.model.ParameterValue; +import hudson.model.ParametersAction; import hudson.model.Queue; -import hudson.model.queue.QueueTaskDispatcher; import hudson.model.queue.CauseOfBlockage; -import hudson.model.ParametersAction; -import hudson.model.ParameterValue; - +import hudson.model.queue.QueueTaskDispatcher; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -30,8 +27,6 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; - -import org.apache.commons.lang.time.DateUtils; import org.jenkins.plugins.lockableresources.LockableResource; import org.jenkins.plugins.lockableresources.LockableResourcesManager; import org.kohsuke.accmod.Restricted; @@ -40,153 +35,164 @@ @Extension public class LockableResourcesQueueTaskDispatcher extends QueueTaskDispatcher { - private transient Cache lastLogged = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build(); - - static final Logger LOGGER = Logger - .getLogger(LockableResourcesQueueTaskDispatcher.class.getName()); - - @Override - public CauseOfBlockage canRun(Queue.Item item) { - // Skip locking for multiple configuration projects, - // only the child jobs will actually lock resources. - if (item.task instanceof MatrixProject) - return null; - - Job project = Utils.getProject(item); - if (project == null) - return null; - - LockableResourcesStruct resources = Utils.requiredResources(project); - if (resources == null || - (resources.required.isEmpty() && resources.label.isEmpty() && resources.getResourceMatchScript() == null)) { - return null; - } - - int resourceNumber; - try { - resourceNumber = Integer.parseInt(resources.requiredNumber); - } catch (NumberFormatException e) { - resourceNumber = 0; - } - - LOGGER.finest(project.getName() + - " trying to get resources with these details: " + resources); - - if (resourceNumber > 0 || !resources.label.isEmpty() || resources.getResourceMatchScript() != null) { - Map params = new HashMap(); - - // Inject Build Parameters, if possible and applicable to the "item" type - try { - List itemparams = item.getActions(ParametersAction.class); - if (itemparams != null) { - for ( ParametersAction actparam : itemparams) { - if (actparam == null) continue; - for ( ParameterValue p : actparam.getParameters() ) { - if (p == null) continue; - params.put(p.getName(), p.getValue()); - } - } - } - } catch(Exception ex) { - // Report the error and go on with the build - - // perhaps this item is not a build with args, etc. - // Note this is likely to fail a bit later in such case. - if (LOGGER.isLoggable(Level.WARNING)) { - if (lastLogged.getIfPresent(item.getId()) == null) { - lastLogged.put(item.getId(), new Date()); - String itemName = project.getFullName() + " (id=" + item.getId() + ")"; - LOGGER.log(Level.WARNING, "Failed to get build params from item " + itemName, ex); - } - } - } - - if (item.task instanceof MatrixConfiguration) { - MatrixConfiguration matrix = (MatrixConfiguration) item.task; - params.putAll(matrix.getCombination()); - } - - final List selected ; - try { - selected = LockableResourcesManager.get().tryQueue( - resources, - item.getId(), - project.getFullName(), - resourceNumber, - params, - LOGGER); - } catch(ExecutionException ex) { - Throwable toReport = ex.getCause(); - if (toReport == null) { // We care about the cause only - toReport = ex; - } - if (LOGGER.isLoggable(Level.WARNING)) { - if (lastLogged.getIfPresent(item.getId()) == null) { - lastLogged.put(item.getId(), new Date()); - - String itemName = project.getFullName() + " (id=" + item.getId() + ")"; - LOGGER.log(Level.WARNING, "Failed to queue item " + itemName, toReport.getMessage()); - } - } - - return new BecauseResourcesQueueFailed(resources, toReport); - } - - if (selected != null) { - LOGGER.finest(project.getName() + " reserved resources " + selected); - return null; - } else { - LOGGER.finest(project.getName() + " waiting for resources"); - return new BecauseResourcesLocked(resources); - } - - } else { - if (LockableResourcesManager.get().queue(resources.required, item.getId(), project.getFullDisplayName())) { - LOGGER.finest(project.getName() + " reserved resources " + resources.required); - return null; - } else { - LOGGER.finest(project.getName() + " waiting for resources " - + resources.required); - return new BecauseResourcesLocked(resources); - } - } - } - - public static class BecauseResourcesLocked extends CauseOfBlockage { - - private final LockableResourcesStruct rscStruct; - - public BecauseResourcesLocked(LockableResourcesStruct r) { - this.rscStruct = r; - } - - @Override - public String getShortDescription() { - if (this.rscStruct.label.isEmpty()) - return "Waiting for resources " + rscStruct.required.toString(); - else - return "Waiting for resources with label " + rscStruct.label; - } - } - - // Only for UI - @Restricted(NoExternalUse.class) - public static class BecauseResourcesQueueFailed extends CauseOfBlockage { - - @NonNull - private final LockableResourcesStruct resources; - @NonNull - private final Throwable cause; - - public BecauseResourcesQueueFailed(@NonNull LockableResourcesStruct resources, @NonNull Throwable cause) { - this.cause = cause; - this.resources = resources; - } - - @Override - public String getShortDescription() { - //TODO: Just a copy-paste from BecauseResourcesLocked, seems strange - String resourceInfo = (resources.label.isEmpty()) ? resources.required.toString() : "with label " + resources.label; - return "Execution failed while acquiring the resource " + resourceInfo + ". " + cause.getMessage(); - } - } + private transient Cache lastLogged = + Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build(); + + static final Logger LOGGER = Logger.getLogger(LockableResourcesQueueTaskDispatcher.class.getName()); + + @Override + public CauseOfBlockage canRun(Queue.Item item) { + // Skip locking for multiple configuration projects, + // only the child jobs will actually lock resources. + if (item.task.getClass().getName().equals("hudson.matrix.MatrixProject")) { + return null; + } + + Job project = Utils.getProject(item); + if (project == null) return null; + + LockableResourcesStruct resources = Utils.requiredResources(project); + if (resources == null + || (resources.required.isEmpty() + && resources.label.isEmpty() + && resources.getResourceMatchScriptText() == null)) { + return null; + } + + int resourceNumber; + try { + resourceNumber = Integer.parseInt(resources.requiredNumber); + } catch (NumberFormatException e) { + resourceNumber = 0; + } + + LOGGER.finest(project.getName() + " trying to get resources with these details: " + resources); + + if (resourceNumber > 0 || !resources.label.isEmpty() || resources.getResourceMatchScriptText() != null) { + Map params = new HashMap<>(); + + // Inject Build Parameters, if possible and applicable to the "item" type + try { + List itemparams = item.getActions(ParametersAction.class); + for (ParametersAction actparam : itemparams) { + if (actparam == null) continue; + for (ParameterValue p : actparam.getParameters()) { + if (p == null) continue; + params.put(p.getName(), p.getValue()); + } + } + } catch (Exception ex) { + // Report the error and go on with the build - + // perhaps this item is not a build with args, etc. + // Note this is likely to fail a bit later in such case. + if (LOGGER.isLoggable(Level.WARNING)) { + if (lastLogged.getIfPresent(item.getId()) == null) { + lastLogged.put(item.getId(), new Date()); + String itemName = project.getFullName() + " (id=" + item.getId() + ")"; + LOGGER.log(Level.WARNING, "Failed to get build params from item " + itemName, ex); + } + } + } + + for (var ma : ExtensionList.lookup(Utils.MatrixAssist.class)) { + params.putAll(ma.getCombination(project)); + } + + final List selected; + try { + selected = LockableResourcesManager.get() + .tryQueue(resources, item.getId(), project.getFullName(), resourceNumber, params, LOGGER); + } catch (ExecutionException ex) { + Throwable toReport = ex.getCause(); + if (toReport == null) { // We care about the cause only + toReport = ex; + } + if (LOGGER.isLoggable(Level.WARNING)) { + if (lastLogged.getIfPresent(item.getId()) == null) { + lastLogged.put(item.getId(), new Date()); + + String itemName = project.getFullName() + " (id=" + item.getId() + ")"; + LOGGER.log(Level.WARNING, "Failed to queue item " + itemName, toReport.getMessage()); + } + } + + return new BecauseResourcesQueueFailed(resources, toReport); + } + + if (selected != null) { + LOGGER.finest(project.getName() + " reserved resources " + selected); + return null; + } else { + LOGGER.finest(project.getName() + " waiting for resources"); + return new BecauseResourcesLocked(resources); + } + + } else { + if (LockableResourcesManager.get().queue(resources.required, item.getId(), project.getFullDisplayName())) { + LOGGER.finest(project.getName() + " reserved resources " + resources.required); + return null; + } else { + LOGGER.finest(project.getName() + " waiting for resources " + resources.required); + return new BecauseResourcesLocked(resources); + } + } + } + + public static class BecauseResourcesLocked extends CauseOfBlockage { + + private final LockableResourcesStruct rscStruct; + + public BecauseResourcesLocked(LockableResourcesStruct r) { + this.rscStruct = r; + } + + @Override + public String getShortDescription() { + if (this.rscStruct.label.isEmpty()) { + if (!this.rscStruct.required.isEmpty()) { + return "Waiting for resource instances " + rscStruct.required; + } else { + final String systemGroovyScript = this.rscStruct.getResourceMatchScriptText(); + if (systemGroovyScript != null) { + // Empty or not... just keep the logic in sync + // with tryQueue() in LockableResourcesManager + if (systemGroovyScript.isEmpty()) { + return "Waiting for resources identified by custom script (which is empty)"; + } else { + return "Waiting for resources identified by custom script"; + } + } + // TODO: Developers should extend here if LockableResourcesStruct is extended + LOGGER.log(Level.WARNING, "Failed to classify reason of waiting for resource: " + this.rscStruct); + return "Waiting for lockable resources"; + } + } else { + return "Waiting for resources with label " + rscStruct.label; + } + } + } + + // Only for UI + @Restricted(NoExternalUse.class) + public static class BecauseResourcesQueueFailed extends CauseOfBlockage { + + @NonNull + private final LockableResourcesStruct resources; + + @NonNull + private final Throwable cause; + + public BecauseResourcesQueueFailed(@NonNull LockableResourcesStruct resources, @NonNull Throwable cause) { + this.cause = cause; + this.resources = resources; + } + + @Override + public String getShortDescription() { + // TODO: Just a copy-paste from BecauseResourcesLocked, seems strange + String resourceInfo = + resources.label.isEmpty() ? resources.required.toString() : "with label " + resources.label; + return "Execution failed while acquiring the resource " + resourceInfo + ". " + cause.getMessage(); + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java index 27b991c1b..b200ad150 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java @@ -8,121 +8,165 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.queue; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; -import java.io.IOException; -import java.io.ObjectOutputStream; - +import hudson.model.Descriptor; import java.io.Serializable; import java.util.ArrayList; +import java.util.Date; import java.util.List; -import javax.annotation.CheckForNull; - import org.jenkins.plugins.lockableresources.LockableResource; import org.jenkins.plugins.lockableresources.LockableResourcesManager; import org.jenkins.plugins.lockableresources.RequiredResourcesProperty; import org.jenkins.plugins.lockableresources.util.SerializableSecureGroovyScript; import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; - -import edu.umd.cs.findbugs.annotations.Nullable; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; public class LockableResourcesStruct implements Serializable { - public List required; - public String label; - public String requiredVar; - public String requiredNumber; - - @CheckForNull - private final SerializableSecureGroovyScript serializableResourceMatchScript; - - @CheckForNull - private transient SecureGroovyScript resourceMatchScript; - - public LockableResourcesStruct(RequiredResourcesProperty property, - EnvVars env) { - required = new ArrayList(); - for (String name : property.getResources()) { - LockableResource r = LockableResourcesManager.get().fromName( - env.expand(name)); - if (r != null) { - this.required.add(r); - } - } - - label = env.expand(property.getLabelName()); - if (label == null) - label = ""; - - resourceMatchScript = property.getResourceMatchScript(); - serializableResourceMatchScript = new SerializableSecureGroovyScript(resourceMatchScript); - - requiredVar = property.getResourceNamesVar(); - - requiredNumber = property.getResourceNumber(); - if (requiredNumber != null && requiredNumber.equals("0")) - requiredNumber = null; - } - - /** - * Light-weight constructor for declaring a resource only. - * @param resources Resources to be required - */ - public LockableResourcesStruct(@Nullable List resources) { - this(resources, null, 0); - } - - public LockableResourcesStruct(@Nullable List resources, @Nullable String label, int quantity, String variable) { - this(resources, label, quantity); - requiredVar = variable; - } - - public LockableResourcesStruct(@Nullable List resources, @Nullable String label, int quantity) { - required = new ArrayList(); - if (resources != null) { - for (String resource : resources) { - LockableResource r = LockableResourcesManager.get().fromName(resource); - if (r != null) { - this.required.add(r); - } - } - } - - this.label = label; - if (this.label == null) { - this.label = ""; - } - - this.requiredNumber = null; - if (quantity > 0) { - this.requiredNumber = String.valueOf(quantity); - } - - // We do not support - this.serializableResourceMatchScript = null; - this.resourceMatchScript = null; - } - - /** - * Gets a system Groovy script to be executed in order to determine if the {@link LockableResource} matches the condition. - * @return System Groovy Script if defined - * @since TODO - * @see LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript, java.util.Map) - */ - @CheckForNull - public SecureGroovyScript getResourceMatchScript() { - if (resourceMatchScript == null && serializableResourceMatchScript != null) { - resourceMatchScript = serializableResourceMatchScript.rehydrate(); - } - return resourceMatchScript; - } - - public String toString() { - return "Required resources: " + this.required + - ", Required label: " + this.label + - ", Required label script: " + (this.resourceMatchScript != null ? this.resourceMatchScript.getScript() : "") + - ", Variable name: " + this.requiredVar + - ", Number of resources: " + this.requiredNumber; - } - - private static final long serialVersionUID = 1L; + // Note to developers: if the set of selection criteria variables evolves, + // do not forget to update LockableResourcesQueueTaskDispatcher.java with + // class BecauseResourcesLocked method getShortDescription() for user info. + public List required; + public String label; + public String requiredVar; + public String requiredNumber; + public long queuedAt = 0; + + @CheckForNull + private final SerializableSecureGroovyScript serializableResourceMatchScript; + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + @CheckForNull + private transient SecureGroovyScript resourceMatchScript; + + private static final long serialVersionUID = 1L; + + public LockableResourcesStruct(RequiredResourcesProperty property, EnvVars env) { + queuedAt = new Date().getTime(); + required = new ArrayList<>(); + + List names = new ArrayList<>(); + for (String name : property.getResources()) { + String resourceName = env.expand(name); + if (resourceName == null) { + continue; + } + names.add(resourceName); + } + + LockableResourcesManager lrm = LockableResourcesManager.get(); + this.required = lrm.fromNames(names, /*create un-existent resources */ true); + + label = env.expand(property.getLabelName()); + if (label == null) label = ""; + + resourceMatchScript = property.getResourceMatchScript(); + serializableResourceMatchScript = new SerializableSecureGroovyScript(resourceMatchScript); + + requiredVar = property.getResourceNamesVar(); + + requiredNumber = property.getResourceNumber(); + if (requiredNumber != null && requiredNumber.equals("0")) requiredNumber = null; + } + + /** + * Light-weight constructor for declaring a resource only. + * + * @param resources Resources to be required + */ + public LockableResourcesStruct(@Nullable List resources) { + this(resources, null, 0); + } + + public LockableResourcesStruct( + @Nullable List resources, @Nullable String label, int quantity, String variable) { + this(resources, label, quantity); + requiredVar = variable; + } + + public LockableResourcesStruct(@Nullable List resources, @Nullable String label, int quantity) { + queuedAt = new Date().getTime(); + required = new ArrayList<>(); + if (resources != null) { + /// FIXME do we shall check here if resources.size() >= quantity + for (String resource : resources) { + LockableResource r = LockableResourcesManager.get().fromName(resource); + if (r != null) { + this.required.add(r); + } + } + } + + this.label = label; + if (this.label == null) { + this.label = ""; + } + + this.requiredNumber = null; + if (quantity > 0) { + this.requiredNumber = String.valueOf(quantity); + } + + // We do not support + this.serializableResourceMatchScript = null; + this.resourceMatchScript = null; + } + + /** + * Gets a system Groovy script to be executed in order to determine if the {@link + * LockableResource} matches the condition. + * + * @return System Groovy Script if defined + * @see + * LockableResource#scriptMatches(org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript, + * java.util.Map) + * @since 2.1 + */ + @CheckForNull + public SecureGroovyScript getResourceMatchScript() throws Descriptor.FormException { + if (resourceMatchScript == null && serializableResourceMatchScript != null) { + // this is probably high defensive code, because + resourceMatchScript = serializableResourceMatchScript.rehydrate(); + } + return resourceMatchScript; + } + + @CheckForNull + public String getResourceMatchScriptText() { + return serializableResourceMatchScript != null ? serializableResourceMatchScript.getScript() : null; + } + + @Override + public String toString() { + String str = ""; + if (this.required != null && !this.required.isEmpty()) { + str += "Required resources: " + this.required; + } + if (this.label != null && !this.label.isEmpty()) { + str += "Required label: " + this.label; + } + if (this.resourceMatchScript != null) { + str += "Required label script: " + this.resourceMatchScript.getScript(); + } + if (this.requiredVar != null) { + str += ", Variable name: " + this.requiredVar; + } + if (this.requiredNumber != null) { + str += ", Number of resources: " + this.requiredNumber; + } + return str; + } + + /** Check if the *resource* is required by this struct / queue */ + @Restricted(NoExternalUse.class) + public boolean isResourceRequired(final LockableResource resource) { + if (resource == null) { + return false; + } + return LockableResourcesManager.getResourcesNames(this.required).contains(resource.getName()); + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java index 8d573ae28..a131de21c 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java @@ -8,13 +8,19 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.queue; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.model.Run; +import hudson.model.TaskListener; +import java.io.IOException; +import java.io.PrintStream; import java.io.Serializable; import java.util.List; - +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct; - -import edu.umd.cs.findbugs.annotations.Nullable; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /* * This class is used to queue pipeline contexts @@ -23,50 +29,160 @@ */ public class QueuedContextStruct implements Serializable { - /* - * Reference to the pipeline step context. - */ - private StepContext context; - - /* - * Reference to the resources required by the step context. - */ - private List lockableResourcesStruct; - - /* - * Description of the required resources used within logging messages. - */ - private String resourceDescription; - - /* - * Constructor for the QueuedContextStruct class. - */ - public QueuedContextStruct(StepContext context, List lockableResourcesStruct, String resourceDescription) { - this.context = context; - this.lockableResourcesStruct = lockableResourcesStruct; - this.resourceDescription = resourceDescription; - } - - /* - * Gets the pipeline step context. - */ - public StepContext getContext() { - return this.context; - } - - /* - * Gets the required resources. - */ - public List getResources() { - return this.lockableResourcesStruct; - } - - /* - * Gets the resource description for logging messages. - */ - public String getResourceDescription() { - return this.resourceDescription; - } - - private static final long serialVersionUID = 1L; + /* + * Reference to the pipeline step context. + */ + private StepContext context; + + /* + * Reference to the resources required by the step context. + */ + private List lockableResourcesStruct; + + /* + * Description of the required resources used within logging messages. + */ + private String resourceDescription; + + /* + * Name of the variable to save the locks taken. + */ + private String variableName; + + private int priority = 0; + + // cached candidates + public transient List candidates = null; + + private static final Logger LOGGER = Logger.getLogger(QueuedContextStruct.class.getName()); + + private String id = null; + + /* + * Constructor for the QueuedContextStruct class. + */ + @Restricted(NoExternalUse.class) + public QueuedContextStruct( + StepContext context, + List lockableResourcesStruct, + String resourceDescription, + String variableName, + int priority) { + this.context = context; + this.lockableResourcesStruct = lockableResourcesStruct; + this.resourceDescription = resourceDescription; + this.variableName = variableName; + this.priority = priority; + this.id = UUID.randomUUID().toString(); + } + + @Restricted(NoExternalUse.class) + public int compare(QueuedContextStruct other) { + if (this.priority > other.getPriority()) return -1; + else if (this.priority == other.getPriority()) return 0; + else return 1; + } + + @Restricted(NoExternalUse.class) + public int getPriority() { + return this.priority; + } + + @Restricted(NoExternalUse.class) + public String getId() { + if (this.id == null) { + this.id = UUID.randomUUID().toString(); + } + return this.id; + } + + /* + * Gets the pipeline step context. + */ + @Restricted(NoExternalUse.class) + public StepContext getContext() { + return this.context; + } + + /** Return build, where is the resource used. */ + @CheckForNull + @Restricted(NoExternalUse.class) + public Run getBuild() { + try { + if (this.getContext() == null) { + return null; + } + return this.getContext().get(Run.class); + } catch (Exception e) { + // for some reason there is no Run object for this context + LOGGER.log( + Level.WARNING, + "Cannot get the build object from the context to proceed with lock. The build probably does not exists (deleted?)", + e); + return null; + } + } + + @Restricted(NoExternalUse.class) + public boolean isValid() { + Run run = this.getBuild(); + if (run == null || run.isBuilding() == false) { + // skip this one, for some reason there is no Run object for this context + LOGGER.warning("The queue " + this + " will be removed, because the build does not exists"); + return false; + } + return true; + } + + @Restricted(NoExternalUse.class) + /* + * Gets the required resources. + */ + public List getResources() { + return this.lockableResourcesStruct; + } + + @Restricted(NoExternalUse.class) + /* + * Gets the resource description for logging messages. + */ + public String getResourceDescription() { + return this.resourceDescription; + } + + @Restricted(NoExternalUse.class) + /* + * Gets the variable name to save the locks taken. + */ + public String getVariableName() { + return this.variableName; + } + + @Restricted(NoExternalUse.class) + public String toString() { + return "build: " + + this.getBuild() + + " resources: " + + this.getResourceDescription() + + " priority: " + + this.priority + + " id: " + + this.getId(); + } + + @Restricted(NoExternalUse.class) + public PrintStream getLogger() { + PrintStream logger = null; + try { + TaskListener taskListener = this.getContext().get(TaskListener.class); + if (taskListener != null) { + logger = taskListener.getLogger(); + } + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.FINE, "Could not get logger for next context: " + e, e); + } + return logger; + } + + private static final long serialVersionUID = 1L; } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java index ccbf9040b..b1896c705 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java @@ -8,41 +8,65 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ package org.jenkins.plugins.lockableresources.queue; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.EnvVars; +import hudson.ExtensionList; import hudson.matrix.MatrixConfiguration; import hudson.model.Job; import hudson.model.Queue; - import hudson.model.Run; +import java.util.Map; import org.jenkins.plugins.lockableresources.RequiredResourcesProperty; +import org.jenkinsci.plugins.variant.OptionalExtension; + +public final class Utils { + private Utils() {} + + @CheckForNull + public static Job getProject(@NonNull Queue.Item item) { + if (item.task instanceof Job) return (Job) item.task; + return null; + } + + @NonNull + public static Job getProject(@NonNull Run build) { + return build.getParent(); + } + + @CheckForNull + public static LockableResourcesStruct requiredResources(@NonNull Job project) { + EnvVars env = new EnvVars(); -public class Utils { + for (var ma : ExtensionList.lookup(MatrixAssist.class)) { + env.putAll(ma.getCombination(project)); + project = ma.getMainProject(project); + } - public static Job getProject(Queue.Item item) { - if (item.task instanceof Job) - return (Job) item.task; - return null; - } + RequiredResourcesProperty property = project.getProperty(RequiredResourcesProperty.class); + if (property != null) return new LockableResourcesStruct(property, env); - public static Job getProject(Run build) { - Object p = build.getParent(); - return (Job) p; - } + return null; + } - public static LockableResourcesStruct requiredResources( - Job project) { - RequiredResourcesProperty property = null; - EnvVars env = new EnvVars(); + public interface MatrixAssist { + @NonNull + Map getCombination(@NonNull Job project); - if (project instanceof MatrixConfiguration) { - env.putAll(((MatrixConfiguration) project).getCombination()); - project = (Job) project.getParent(); - } + @NonNull + Job getMainProject(@NonNull Job project); + } - property = project.getProperty(RequiredResourcesProperty.class); - if (property != null) - return new LockableResourcesStruct(property, env); + @OptionalExtension(requirePlugins = "matrix-project") + public static final class MatrixImpl implements MatrixAssist { + @Override + public Map getCombination(Job project) { + return project instanceof MatrixConfiguration mc ? mc.getCombination() : Map.of(); + } - return null; - } + @Override + public Job getMainProject(Job project) { + return project instanceof MatrixConfiguration mc ? mc.getParent() : project; + } + } } diff --git a/src/main/java/org/jenkins/plugins/lockableresources/util/Constants.java b/src/main/java/org/jenkins/plugins/lockableresources/util/Constants.java new file mode 100644 index 000000000..ed6c086fb --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/util/Constants.java @@ -0,0 +1,18 @@ +package org.jenkins.plugins.lockableresources.util; + +public class Constants { + /// Enable mirror nodes to lockable-resources + public static final String SYSTEM_PROPERTY_ENABLE_NODE_MIRROR = + "org.jenkins.plugins.lockableresources.ENABLE_NODE_MIRROR"; + /// Disable saving lockable resources states, properties ... into local file system. + /// This option makes the plugin much faster (everything is in cache) but + /// **Keep in mind, that you will lost all your manual changed properties** + /// The best way is to use it with JCaC plugin. + public static final String SYSTEM_PROPERTY_DISABLE_SAVE = "org.jenkins.plugins.lockableresources.DISABLE_SAVE"; + /// Enable to print lock causes. Keep in mind, that the log output may grove depends on count of + /// blocked resources. + public static final String SYSTEM_PROPERTY_PRINT_BLOCKED_RESOURCE = + "org.jenkins.plugins.lockableresources.PRINT_BLOCKED_RESOURCE"; + public static final String SYSTEM_PROPERTY_PRINT_QUEUE_INFO = + "org.jenkins.plugins.lockableresources.PRINT_QUEUE_INFO"; +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java b/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java index e1ad77345..2603a850e 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java @@ -23,17 +23,17 @@ */ package org.jenkins.plugins.lockableresources.util; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.Descriptor; +import hudson.util.FormValidation; import java.io.Serializable; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import hudson.util.FormValidation; import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript; import org.jenkinsci.plugins.scriptsecurity.scripts.ClasspathEntry; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; @@ -42,83 +42,87 @@ /** * Wrapper for a {@link SecureGroovyScript}. + * * @author Oleg Nenashev */ @Restricted(NoExternalUse.class) public class SerializableSecureGroovyScript implements Serializable { - private static final long serialVersionUID = 1L; - - @CheckForNull - private final String script; - private final boolean sandbox; - /** - * {@code null} if and only if the {@link #script is null}. - */ - @Nullable - private final ArrayList classPathEntries; - - private static final Logger LOGGER = Logger.getLogger(SerializableSecureGroovyScript.class.getName()); - - public SerializableSecureGroovyScript(@CheckForNull SecureGroovyScript secureScript) { - if (secureScript == null) { - script = null; - sandbox = false; - classPathEntries = null; - } else { - this.script = secureScript.getScript(); - this.sandbox = secureScript.isSandbox(); - - List classpath = secureScript.getClasspath(); - classPathEntries = new ArrayList<>(classpath.size()); - for (ClasspathEntry e : classpath) { - classPathEntries.add(new SerializableClassPathEntry(e)); - } - } - } - - @CheckForNull - public SecureGroovyScript rehydrate() { - if (script == null) { - return null; - } - - ArrayList p = new ArrayList<>(classPathEntries.size()); - for (SerializableClassPathEntry e : classPathEntries) { - ClasspathEntry entry = e.rehydrate(); - if (entry != null) { - p.add(entry); - } - } - - return new SecureGroovyScript(script, sandbox, p); - } - - private static class SerializableClassPathEntry implements Serializable { - - private static final long serialVersionUID = 1L; - - private final String url; - - private SerializableClassPathEntry(@Nonnull ClasspathEntry entry) { - this.url = entry.getPath(); - } - - @CheckForNull - private ClasspathEntry rehydrate(){ - try { - ClasspathEntry entry = new ClasspathEntry(url); - if (ScriptApproval.get().checking(entry).kind.equals(FormValidation.Kind.OK)) { - return entry; - } else { - return null; - } - } catch (MalformedURLException ex) { - // Unrealistic - LOGGER.log(Level.SEVERE, "Failed to rehydrate the URL " + url + ". It will be skipped", ex); - return null; - } - } - - } + private static final long serialVersionUID = 1L; + + @CheckForNull + private final String script; + + private final boolean sandbox; + /** {@code null} if and only if the {@link #script is null}. */ + @Nullable + private final ArrayList classPathEntries; + + private static final Logger LOGGER = Logger.getLogger(SerializableSecureGroovyScript.class.getName()); + + public SerializableSecureGroovyScript(@CheckForNull SecureGroovyScript secureScript) { + if (secureScript == null) { + script = null; + sandbox = false; + classPathEntries = null; + } else { + this.script = secureScript.getScript(); + this.sandbox = secureScript.isSandbox(); + + List classpath = secureScript.getClasspath(); + classPathEntries = new ArrayList<>(classpath.size()); + for (ClasspathEntry e : classpath) { + classPathEntries.add(new SerializableClassPathEntry(e)); + } + } + } + + @CheckForNull + public String getScript() { + return script; + } + + @CheckForNull + public SecureGroovyScript rehydrate() throws Descriptor.FormException { + if (script == null) { + return null; + } + + ArrayList p = new ArrayList<>(classPathEntries.size()); + for (SerializableClassPathEntry e : classPathEntries) { + ClasspathEntry entry = e.rehydrate(); + if (entry != null) { + p.add(entry); + } + } + + return new SecureGroovyScript(script, sandbox, p); + } + + private static class SerializableClassPathEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String url; + + private SerializableClassPathEntry(@NonNull ClasspathEntry entry) { + this.url = entry.getPath(); + } + + @CheckForNull + private ClasspathEntry rehydrate() { + try { + ClasspathEntry entry = new ClasspathEntry(url); + if (ScriptApproval.get().checking(entry).kind.equals(FormValidation.Kind.OK)) { + return entry; + } else { + return null; + } + } catch (MalformedURLException ex) { + // Unrealistic + LOGGER.log(Level.SEVERE, "Failed to rehydrate the URL " + url + ". It will be skipped", ex); + return null; + } + } + } } diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly index 90affe3e7..b39d2bfcc 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.jelly @@ -1,29 +1,38 @@ - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
+ xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties new file mode 100644 index 000000000..3a11c8217 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties @@ -0,0 +1,32 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Resource +entry.label.title=Label +entry.quantity.title=Quantity +entry.variable.title=Result variable +entry.inversePrecedence.checkbox.title=Inverse precedence +entry.skipIfLocked.title=Skip if locked +entry.priority.title=Queue priority +entry.resourceSelectStrategy.title=Strategy for resource selection +entry.extra.title=Extra resources +entry.extra.add=Add Resource diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties new file mode 100644 index 000000000..e8c783f38 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_cs.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Zdroj +entry.label.title=Popisek +entry.quantity.title=Mno\u017estv\u00ed +entry.variable.title=Prom\u011bnn\u00e1 v\u00fdsledk\u016f +entry.inversePrecedence.checkbox.title=Obr\u00e1cen\u00e9 po\u0159ad\u00ed +entry.skipIfLocked.title=P\u0159esko\u010dit frontu +entry.resourceSelectStrategy.title=Strategie v\u00fdb\u011bru zdroj\u016f +entry.extra.title=Dodate\u010dn\u00e9 zdroje +entry.extra.add=P\u0159idat zdroj diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties new file mode 100644 index 000000000..6294b40c8 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_de.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Ressource +entry.label.title=Label +entry.quantity.title=Anzahl +entry.variable.title=Ergebnisvariable +entry.inversePrecedence.checkbox.title=Umgekehrter Vorrang +entry.skipIfLocked.title=Warteschlange \u00fcberspringen +entry.resourceSelectStrategy.title=Strategie f\u00fcr Ressourcenauswahl +entry.extra.title=Zus\u00e4tzliche Ressourcen +entry.extra.add=Ressource hinzuf\u00fcgen diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties new file mode 100644 index 000000000..2268459e1 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_fr.properties @@ -0,0 +1,30 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Ressource +entry.label.title=Libell\u00e9 +entry.quantity.title=Quantit\u00e9 +entry.variable.title=Variable r\u00e9sultat +entry.inversePrecedence.checkbox.title=Priorit\u00e9 invers\u00e9e +entry.skipIfLocked.title=Sauter la file d'attente +entry.extra.title=Ressources suppl\u00e9mentaires +entry.extra.add=Ajouter une ressource \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties new file mode 100644 index 000000000..3ba850a01 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config_sk.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Zdroj +entry.label.title=\u0160t\u00edtok +entry.quantity.title=Po\u010det +entry.variable.title=Pramenn\u00e1 s v\u00fdsledkami +entry.inversePrecedence.checkbox.title=Opa\u010dn\u00e9 poradie +entry.skipIfLocked.title=Predbehn\u00fa\u0165 rad +entry.resourceSelectStrategy.title=Strat\u00e9gia v\u00fdberu zdrojov +entry.extra.title=Dodato\u010dn\u00e9 zdroje +entry.extra.add=Prida\u0165 zdroj diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-inversePrecedence.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-inversePrecedence.html index a182da914..1a14e6ee6 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-inversePrecedence.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-inversePrecedence.html @@ -1,8 +1,13 @@

-

- By default waiting builds get the lock in the same order they requested to acquire it. -

-

- By checking this option the newest build in the waiting queue will get the lock first. -

+

+ By default waiting builds get the lock in the same order they requested to acquire it. +

+

+ By checking this option the newest build in the waiting queue will get the lock first. +

+

+ See also + examples + . +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-label.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-label.html index e02149d02..d89e6a47c 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-label.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-label.html @@ -1,6 +1,14 @@
-

- The label of the resources to be locked as defined in Global settings. - Either a resource or a label need to be specified. -

+

+ The label of the resources to be locked as defined in Global settings. +

+

+ Either a resource or a label need to be specified. +

+

+ See also + examples and + Scripted vs declarative pipeline + . +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html new file mode 100644 index 000000000..9289be778 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-priority.html @@ -0,0 +1,14 @@ +
+

+ The priority of the lock, +

+

+ which takes an integer number that defines the order in which concurrent jobs waiting for the same resource are served. + The job with the highest number would get the resource first. If the priority is equal, the current precedence (first comes first) would be applied. +

+

+ See also + examples + . +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-quantity.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-quantity.html index 7646eb821..95f5f3bfe 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-quantity.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-quantity.html @@ -1,6 +1,9 @@
-

- The quantity of resources with the specified label to be locked as defined in Global settings. - Either a resource or a label need to be specified. -

+

+ The quantity of resources with the specified label to be locked as defined in Global settings. +

+

+ Either a resource or a label need to be specified. + Empty value or 0 means lock all matching resources. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resource.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resource.html index 6c5ec5177..fc9e44264 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resource.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resource.html @@ -1,7 +1,9 @@
-

- The resource name to lock as defined in Global settings. - If the resource does not exist in Global Settings it will be automatically created on build execution. - Either a resource or a label need to be specified. -

+

+ The resource name to lock as defined in Global settings. +

+

+ If the resource does not exist in Global Settings it will be automatically created on build execution. + Either a resource or a label need to be specified. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resourceSelectStrategy.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resourceSelectStrategy.html new file mode 100644 index 000000000..c1b9a4098 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-resourceSelectStrategy.html @@ -0,0 +1,7 @@ +
+

+ The strategy used to chose which available resources get locked. + By default, the strategy will be "sequential" and resources are locked following the order in the lockable resources list. + Set the strategy to "random" to randomize the order in which resources are locked. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-skipIfLocked.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-skipIfLocked.html new file mode 100644 index 000000000..cff1c30d0 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-skipIfLocked.html @@ -0,0 +1,14 @@ +
+

+ By default waiting builds get the lock. +

+

+ By checking this option the body will not be executed if there is a queue. + It will only take the lock if it can be taken immediately. +

+

+ See also + examples + . +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-variable.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-variable.html index 6331541bc..9ff45eae6 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-variable.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-variable.html @@ -1,13 +1,11 @@
-

- Name of an environment variable that will receive the comma separated list of the names of the locked resources while the block executes. -

-

- e.g.: -

-lock(abel: 'label', variable: 'var') {
-    echo "Resource locked: ${env.var}"
-}
-		
-

+

+ Name of an environment variable that will receive the comma separated list of the names of the locked resources while the block executes. +

+

+ See also + examples and + Scripted vs declarative pipeline + . +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.jelly index 9a30bd2be..ebc07d4ba 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.jelly @@ -1,13 +1,13 @@ - - - - - - - - - + xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> + + + + + + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.properties new file mode 100644 index 000000000..90154a2db --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Resource +entry.label.title=Label +entry.quantity.title=Quantity \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_cs.properties new file mode 100644 index 000000000..7b4d77160 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_cs.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Zdroj +entry.label.title=Popisek +entry.quantity.title=Po\u010det \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_de.properties new file mode 100644 index 000000000..e51d7c592 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_de.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Ressource +entry.label.title=Label +entry.quantity.title=Menge \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_fr.properties new file mode 100644 index 000000000..ba06595b9 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_fr.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Ressource +entry.label.title=Libell\u00e9 +entry.quantity.title=Quantit\u00e9 \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_sk.properties new file mode 100644 index 000000000..5d2ef943d --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config_sk.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.resource.title=Zdroj +entry.label.title=\u0160t\u00edtok +entry.quantity.title=Po\u010det \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-label.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-label.html index e02149d02..d89e6a47c 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-label.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-label.html @@ -1,6 +1,14 @@
-

- The label of the resources to be locked as defined in Global settings. - Either a resource or a label need to be specified. -

+

+ The label of the resources to be locked as defined in Global settings. +

+

+ Either a resource or a label need to be specified. +

+

+ See also + examples and + Scripted vs declarative pipeline + . +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-quantity.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-quantity.html index 7646eb821..95f5f3bfe 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-quantity.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-quantity.html @@ -1,6 +1,9 @@
-

- The quantity of resources with the specified label to be locked as defined in Global settings. - Either a resource or a label need to be specified. -

+

+ The quantity of resources with the specified label to be locked as defined in Global settings. +

+

+ Either a resource or a label need to be specified. + Empty value or 0 means lock all matching resources. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-resource.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-resource.html index 6c5ec5177..fc9e44264 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-resource.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-resource.html @@ -1,7 +1,9 @@
-

- The resource name to lock as defined in Global settings. - If the resource does not exist in Global Settings it will be automatically created on build execution. - Either a resource or a label need to be specified. -

+

+ The resource name to lock as defined in Global settings. +

+

+ If the resource does not exist in Global Settings it will be automatically created on build execution. + Either a resource or a label need to be specified. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.jelly index 53eec81b1..1f7e00314 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.jelly @@ -10,17 +10,23 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> - - - - - - - - - - - - + xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> + + + + + + + + + + + + +
+ +
+
+
+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.properties new file mode 100644 index 000000000..bd6fe2fc4 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Name +entry.description.title=Description +entry.labels.title=Labels +entry.properties.title=Properties +entry.properties.add=Add Property +entry.properties.delete=Delete Property \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_cs.properties new file mode 100644 index 000000000..0fd70285e --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_cs.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Jm\u00e9no +entry.description.title=Popis +entry.labels.title=Popisky \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_de.properties new file mode 100644 index 000000000..cdd76fa46 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_de.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Name +entry.description.title=Beschreibung +entry.labels.title=Labels \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_fr.properties new file mode 100644 index 000000000..5e798e099 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_fr.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Nom +entry.description.title=Description +entry.labels.title=Libell\u00e9s +entry.properties.title=Propri\u00e9t\u00e9s +entry.properties.add=Ajouter une propri\u00e9t\u00e9 +entry.properties.delete=Supprimer une propri\u00e9t\u00e9 \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_sk.properties new file mode 100644 index 000000000..f369e6755 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResource/config_sk.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Meno +entry.description.title=Popis +entry.labels.title=\u0160t\u00edtky \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.jelly new file mode 100644 index 000000000..561b3f24d --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.jelly @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.properties new file mode 100644 index 000000000..3034bf1f9 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Name +entry.value.title=Value \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config_fr.properties new file mode 100644 index 000000000..e6bfee239 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourceProperty/config_fr.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +entry.name.title=Nom +entry.value.title=Valeur \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.jelly index 658b7595f..2ff01cc13 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.jelly @@ -10,17 +10,24 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> - - - - - - -
-
-
-
-
-
+ xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.properties new file mode 100644 index 000000000..4053ad618 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +section.title=Lockable Resources Manager +configuration.title=Configuration +configuration.allowEmptyOrNullValues.title=Allow empty or null values +entry.title=Lockable Resources +field.header=Resource +field.add=Add Lockable Resource diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_cs.properties new file mode 100644 index 000000000..9a13642fe --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_cs.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +section.title=Spr\u00e1vce uzamykateln\u00fdch zdroj\u00fa +entry.title=Uzamykateln\u00e9 zdroje +field.header=Zdroj +field.add=P\u0159idat uzamykateln\u00fd zdroj \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_de.properties new file mode 100644 index 000000000..bc6600e47 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_de.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +section.title=Sperrbare Ressourcen verwalten +entry.title=Sperrbare Ressourcen +field.header=Ressource +field.add=Sperrbare Ressource hinzuf\u00fcgen \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_fr.properties new file mode 100644 index 000000000..73d5c9017 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_fr.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +section.title=Gestionnaire des ressources verrouillables +entry.title=Ressources verrouillables +field.header=Ressource +field.add=Ajouter une ressource verrouillable \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_sk.properties new file mode 100644 index 000000000..dd4817dd3 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config_sk.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +section.title=Spr\u00e1vca uzamykate\u013en\u00fdch zdrojov +entry.title=Uzamykate\u013en\u00e9 zdroje +field.header=Zdroj +field.add=Prida\u0165 uzamykate\u013en\u00fd zdroj \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/help-allowEmptyOrNullValues.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/help-allowEmptyOrNullValues.html new file mode 100644 index 000000000..7b121ca58 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/help-allowEmptyOrNullValues.html @@ -0,0 +1,9 @@ +
+ Usage at own risk. Enabling this flag allows calling the lock step +
    +
  • with null or empty string
  • +
  • with label property with null or empty value
  • +
  • with resource property with null or empty value
  • +
  • with extra property with null or empty list
  • +
+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties index f634f92cb..cc293d0b1 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties @@ -8,9 +8,34 @@ # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # LockableResourcesRootAction.PermissionGroup=Lockable Resources -LockableResourcesRootAction.UnlockPermission=Unlock LockableResourcesRootAction.UnlockPermission.Description=This permission grants the ability to manually \ unlock resources that have been locked by builds. -LockableResourcesRootAction.ReservePermission=Reserve LockableResourcesRootAction.ReservePermission.Description=This permission grants the ability to manually \ reserve lockable resources outside of a build. +LockableResourcesRootAction.StealPermission.Description=This permission grants the ability to manually \ + "steal" resources that have been locked by builds or "reassign" those reserved by users. +LockableResourcesRootAction.ViewPermission.Description=This permission grants the ability to view \ + lockable resources. +LockableResourcesRootAction.QueueChangeOrderPermission.Description=This permission grants the ability to \ + manually manipulate the lockable resources queue.. +LockedResourcesBuildAction.displayName=Lockable resources +# Java errors +error.labelDoesNotExist=The resource label does not exist: {0}. +error.resourceDoesNotExist=The resource does not exist: {0}. +error.labelOrNameMustBeSpecified=Either resource label or resource name must be specified. +error.labelAndNameSpecified=Resource label and resource name cannot be specified simultaneously. +error.labelAndNameOrGroovySpecified=Only resource label, groovy expression, or resource names can be defined, not more than one. +error.couldNotParseToint=Could not parse the given value as integer. +error.givenAmountIsGreaterThatResourcesAmount=Given amount %d is greater than amount of resources: %d. +error.resourceAlreadyLocked=Resource {0} already reserved or locked! +error.invalidResourceSelectionStrategy=The strategy "{0}" is not supported. Valid options are {1}. +error.isNotANumber=The queue position must be a number. Given: {0} +error.queuePositionOutOfRange=The queue position {0} is out of range (1 - {1})! +error.queueDoesNotExist=The queue {0} does not (anymore) exist. +error.inversePrecedenceAndPriorityAreSet=The "inverse precedence" option is not compatible with "queue priority" option! +# display-names +LockStep.displayName=Lock shared resource +LockStepResource.displayName=Resource +LockableResource.displayName=Resource +LockableResourcesManager.displayName=External Resources +RequiredResourcesProperty.displayName=Required Lockable Resources diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_cs.properties new file mode 100644 index 000000000..7b1feb2ad --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_cs.properties @@ -0,0 +1,15 @@ +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # +# Copyright (c) 2014, 6WIND S.A. All rights reserved. # +# # +# This file is part of the Jenkins Lockable Resources Plugin and is # +# published under the MIT license. # +# # +# See the "LICENSE.txt" file for more information. # +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # + +LockableResourcesRootAction.PermissionGroup=Uzamykateln\u00e9 zdroje +LockableResourcesRootAction.UnlockPermission.Description=Toto opr\u00e1vn\u011bn\u00ed ud\u011bluje mo\u017enost ru\u010dn\u011b odemknout zdroje, kter\u00e9 byly uzam\u010deny sestaven\u00edmi. +LockableResourcesRootAction.ReservePermission.Description=Toto opr\u00e1vn\u011bn\u00ed poskytuje mo\u017enost ru\u010dn\u011b rezervovat uzamykateln\u00e9 zdroje mimo sestaven\u00ed. +LockableResourcesRootAction.StealPermission.Description=Toto opr\u00e1vn\u011bn\u00ed ud\u011bluje mo\u017enost manu\u00e1ln\u011b "ukradnout" zdroje, kter\u00e9 byly uzam\u010deny sestaven\u00edm nebo "p\u0159e\u0159adit" polo\u017eky rezervovan\u00e9 u\u017eivateli. +LockableResourcesRootAction.ViewPermission.Description=Toto opr\u00e1vn\u011bn\u00ed ud\u011bluje mo\u017enost zobrazit uzamykateln\u00e9 zdroje. + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_de.properties new file mode 100644 index 000000000..44144b07f --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_de.properties @@ -0,0 +1,15 @@ +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # +# Copyright (c) 2014, 6WIND S.A. All rights reserved. # +# # +# This file is part of the Jenkins Lockable Resources Plugin and is # +# published under the MIT license. # +# # +# See the "LICENSE.txt" file for more information. # +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # + +LockableResourcesRootAction.PermissionGroup=Sperrbare Ressourcen +LockableResourcesRootAction.UnlockPermission.Description=Diese Berechtigung gew\u00e4hrt die M\u00f6glichkeit, Ressourcen manuell freizuschalten, die durch Builds gesperrt wurden. +LockableResourcesRootAction.ReservePermission.Description=Diese Berechtigung gew\u00e4hrt die M\u00f6glichkeit, verschlie\u00dfbare Ressourcen au\u00dferhalb eines Build manuell zu reservieren. +LockableResourcesRootAction.StealPermission.Description=Diese Berechtigung gew\u00e4hrt die M\u00f6glichkeit, Ressourcen manuell zu stehlen, die durch Builds gesperrt oder von Benutzern reserviert wurden. +LockableResourcesRootAction.ViewPermission.Description=Diese Berechtigung gew\u00e4hrt die M\u00f6glichkeit, gesperrte Ressourcen zu sehen. + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_fr.properties new file mode 100644 index 000000000..b24abe94f --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_fr.properties @@ -0,0 +1,15 @@ +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # +# Copyright (c) 2014, 6WIND S.A. All rights reserved. # +# # +# This file is part of the Jenkins Lockable Resources Plugin and is # +# published under the MIT license. # +# # +# See the "LICENSE.txt" file for more information. # +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # + +LockableResourcesRootAction.PermissionGroup=Ressources verrouillables +LockableResourcesRootAction.UnlockPermission.Description=Cette permission accorde la possibilit\u00e9 de d\u00e9verrouiller manuellement les ressources qui ont \u00e9t\u00e9 verrouill\u00e9es par les builds. +LockableResourcesRootAction.ReservePermission.Description=Cette permission accorde la possibilit\u00e9 de r\u00e9server manuellement les ressources verrouillables \u00e0 l'ext\u00e9rieur d'un build. +LockableResourcesRootAction.StealPermission.Description=Cette permission permet de "voler" manuellement les ressources qui ont \u00e9t\u00e9 verrouill\u00e9es par les builds ou de "r\u00e9assigner" celles r\u00e9serv\u00e9es par les utilisateurs. +LockableResourcesRootAction.ViewPermission.Description=Cette permission accorde la possibilit\u00e9 de voir les ressources verrouillables. + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_sk.properties new file mode 100644 index 000000000..18870b3ab --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages_sk.properties @@ -0,0 +1,15 @@ +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # +# Copyright (c) 2014, 6WIND S.A. All rights reserved. # +# # +# This file is part of the Jenkins Lockable Resources Plugin and is # +# published under the MIT license. # +# # +# See the "LICENSE.txt" file for more information. # +# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # + +LockableResourcesRootAction.PermissionGroup=Uzamykate\u013en\u00e9 zdroje +LockableResourcesRootAction.UnlockPermission.Description=Toto opr\u00e1vnenie ude\u013euje mo\u017enos\u0165 ru\u010dne odomkn\u00fa\u0165 zdroje, ktor\u00e9 boli uzamknut\u00e9 zostaveniami. +LockableResourcesRootAction.ReservePermission.Description=Toto opr\u00e1vnenie poskytuje mo\u017enos\u0165 ru\u010dne rezervova\u0165 uzamykate\u013en\u00e9 zdroje mimo zostavenia. +LockableResourcesRootAction.StealPermission.Description=Toto opr\u00e1vnenie umo\u017e\u0148uje manu\u00e1lne "ukradn\u00fa\u0165" zdroj, ktor\u00fd bol uzamknut\u00fd zostaven\u00edm alebo "preradi\u0165" zdroje rezervovan\u00e9 u\u017e\u00edvate\u013eom. +LockableResourcesRootAction.ViewPermission.Description=Toto opr\u00e1vnenie ude\u013euje mo\u017enos\u0165 zobrazi\u0165 zamykate\u013en\u00e9 zdroje. + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.jelly index dbffcae59..589279dd4 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.jelly @@ -10,24 +10,24 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> - - - - - - - - - - - - - - - - - + xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.properties new file mode 100644 index 000000000..d745faf83 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +optionalBlock.title=This build requires lockable resources +entry.resourceNames.title=Resources +entry.labelName.title=Label +optionalProperty.resourceMatchScript.title=Groovy Expression +entry.resourceNamesVar.title=Reserved resources variable name +entry.resourceNumber.title=Number of resources to request \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_cs.properties b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_cs.properties new file mode 100644 index 000000000..2d2fec0b0 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_cs.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +optionalBlock.title=Toto sestaven\u00ed vy\u017eaduje uzamykateln\u00e9 zdroje +entry.resourceNames.title=Zdroje +entry.labelName.title=Popisek +optionalProperty.resourceMatchScript.title=Groovy k\u00f3d +entry.resourceNamesVar.title=N\u00e1zev prom\u011bnn\u00e9 pro rezervovan\u00e9 zdroje +entry.resourceNumber.title=Po\u010det zdroj\u016f na vy\u017e\u00e1d\u00e1n\u00ed \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_de.properties b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_de.properties new file mode 100644 index 000000000..b85e27d8f --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_de.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +optionalBlock.title=Dieses Build erfordert sperrbare Ressourcen +entry.resourceNames.title=Ressourcen +entry.labelName.title=Label +optionalProperty.resourceMatchScript.title=Groovy Expression +entry.resourceNamesVar.title=Variablenname mit reservierten Ressourcen +entry.resourceNumber.title=Anzahl der anzufordernden Ressourcen \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_fr.properties b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_fr.properties new file mode 100644 index 000000000..f2d69e716 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_fr.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +optionalBlock.title=Ce job n\u00e9cessite des ressources verrouillables +entry.resourceNames.title=Ressources +entry.labelName.title=Libell\u00e9 +optionalProperty.resourceMatchScript.title=Expression Groovy +entry.resourceNamesVar.title=Nom de la variable des ressources r\u00e9serv\u00e9es +entry.resourceNumber.title=Nombre de ressources \u00e0 demander \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_sk.properties b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_sk.properties new file mode 100644 index 000000000..1b82c8fd0 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config_sk.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2022 Martin Pokorny. +# +# 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. + +optionalBlock.title=Toto zostavenie vy\u017eaduje uzamykate\u013en\u00e9 zdroje +entry.resourceNames.title=Zdroje +entry.labelName.title=\u0160t\u00edtok +optionalProperty.resourceMatchScript.title=Groovy k\u00f3d +entry.resourceNamesVar.title=N\u00e1zov pramennej obsahuj\u00facej rezervovan\u00e9 zdroje +entry.resourceNumber.title=Po\u010det zdrojov na vy\u017eiadanie \ No newline at end of file diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-labelName.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-labelName.html index ceb651ff1..50750ccc0 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-labelName.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-labelName.html @@ -1,8 +1,8 @@
-

-If you have created a pool of resources, i.e. a label, you can take it into use -here. The build will select the resource(s) from the pool that includes all -resources sharing the given label. -Only one of Label, Groovy Expression or Resources fields may be specified. -

+

+ If you have created a pool of resources, i.e. a label, you can take it into use + here. The build will select the resource(s) from the pool that includes all + resources sharing the given label. + Only one of Label, Groovy Expression or Resources fields may be specified. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceMatchScript.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceMatchScript.html index b60dee9a1..38918d027 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceMatchScript.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceMatchScript.html @@ -1,28 +1,28 @@
-

- You can specify a groovy expression to be evaluated each time a resource is checked - to be appropriate for a build. The expression must result into a boolean value. The - following variables are available, in addition to optional arguments of the currently - evaluated build: -

-
-
resourceName
-
as per resource configuration
-
resourceDescription
-
as per resource configuration
-
resourceLabels
-
java.util.List<String> of labels as per resource configuration
-
-

- For matrix jobs, axis names and axis values can be referenced as well. Examples: -

-
    -
  • resourceLabels.contains("hardcoded")
  • -
  • resourceLabels.contains(axisName)
  • -
  • resourceName == axisName
  • -
+

+ You can specify a groovy expression to be evaluated each time a resource is checked + to be appropriate for a build. The expression must result into a boolean value. The + following variables are available, in addition to optional arguments of the currently + evaluated build: +

+
+
resourceName
+
as per resource configuration
+
resourceDescription
+
as per resource configuration
+
resourceLabels
+
java.util.List<String> of labels as per resource configuration
+
+

+ For matrix jobs, axis names and axis values can be referenced as well. Examples: +

+
    +
  • resourceLabels.contains("hardcoded")
  • +
  • resourceLabels.contains(axisName)
  • +
  • resourceName == axisName
  • +
-

- The script's contents need to pass approval by the Script Security Plugin. -

-
\ No newline at end of file +

+ The script's contents need to pass approval by the Script Security Plugin. +

+ diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNames.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNames.html index ae57f1d5f..33f04268e 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNames.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNames.html @@ -1,8 +1,8 @@
-

-When a build is scheduled, it will attempt to lock the specified resources. If -some (or all) the resources are already locked by another build, the build will -be queued until they are released. It is possible to specify an amount for -requested resources below. -

+

+ When a build is scheduled, it will attempt to lock the specified resources. If + some (or all) the resources are already locked by another build, the build will + be queued until they are released. It is possible to specify an amount for + requested resources below. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNamesVar.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNamesVar.html index e85e61ee5..290c934fb 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNamesVar.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNamesVar.html @@ -1,6 +1,6 @@
-

-Name for the Jenkins variable to store the reserved resources in. Leave empty -to disable. -

+

+ Name for the Jenkins variable to store the reserved resources in. Leave empty + to disable. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNumber.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNumber.html index 17fbfa8b5..43cb881b5 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNumber.html +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-resourceNumber.html @@ -1,8 +1,8 @@
-

-Number of resources to request, empty value or 0 means all. -
-This is useful, if you have a pool of similar resources, from which you want -one or more to be reserved. -

+

+ Number of resources to request, empty value or 0 means all. +
+ This is useful, if you have a pool of similar resources, from which you want + one or more to be reserved. +

diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/_api.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/_api.jelly new file mode 100644 index 000000000..b0321f03a --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/_api.jelly @@ -0,0 +1,14 @@ + + +
+ This /api adds remote API access to read current resources. +
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly index 0acf38d6d..a279eb09f 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly +++ b/src/main/resources/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction/index.jelly @@ -1,147 +1,75 @@ + - - + + + + + - - -

${%Lockable Resources}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ResourceStatusLabelsAction
- ${resource.name}
- ${resource.description} -
- LOCKED by - - ${resource.build.fullDisplayName} - - ${resource.labels} - - - - - RESERVED by ${resource.reservedBy} - ${resource.labels} - - - - - - - QUEUED by "${resource.queueItemProject} ${resource.queueItemId}" - ${resource.labels} - - - - - FREE - ${resource.labels} - - - -
- -

Labels

- - - - - - - - - - - - - - - - - - - - - - - - - -
LabelFree resources
${label}0${label}1${label}${it.getFreeResourceAmount(label)}
-
+ + + +

+ ${%resources.not_configured}
+ + ${%resources.configure.here(rootURL + "/configure")} + +

+
+ +
+ + + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
-
-
+