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/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..98f334645 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,114 @@ +# Copilot Instructions — Lockable Resources Plugin + +This file tells GitHub Copilot (and other AI assistants) about the project's +coding standards, PR requirements, and review checklist so they are applied +automatically when generating code, commit messages, or PR descriptions. + +## Project overview + +Jenkins plugin that lets builds declare **lockable resources** (printers, +phones, lab machines, etc.). If a resource is already locked, the build waits +until it is free. + +- Language: **Java 17+** (Maven build, Jenkins HPI packaging) +- Localization: managed via [Crowdin](https://crowdin.jenkins.io/lockable-resources-plugin); all strings live in `*.properties` files +- UI examples: documented under `src/doc/examples/` + +## Issue linking + +- Reference a Jira issue as `JENKINS-XXXXX` with a link to `https://issues.jenkins.io/browse/JENKINS-XXXXX`. +- Reference a GitHub issue as `#XXXXX`. +- Use closing keywords (`Fixes #XXXXX`) when the PR fully resolves an issue. +- Minor improvements do not require a tracking issue. +- Bug fixes **should** have a tracking issue to facilitate backporting. +- Major new features **must** have a tracking issue. + +## Commit & PR title conventions + +- The PR title is used as the **changelog entry** — write it in imperative mood + (e.g. "Add timeout parameter to lock step"). +- Follow the style of https://github.com/jenkins-infra/jenkins.io/blob/main/content/_data/changelogs/weekly.yml + +## Coding rules + +| Area | Rule | +|------|------| +| Internal-only public API | Annotate with `@NoExternalUse`; if called from Jelly, add `Used by {@code .jelly}` Javadoc | +| New public classes/fields/methods | Annotate with `@Restricted` or add `@since TODO` Javadoc | +| Deprecations | Use `@Deprecated(since = "TODO")` or `@Deprecated(forRemoval = true, since = "TODO")` | +| JavaScript | No inline scripts, no `eval()` — support future CSP directives (see [docs](https://www.jenkins.io/doc/developer/security/csp/)) | +| Localizations | Always use `*.properties` files; English strings are mandatory for every new key | + +## Testing requirements + +- Every change **must** have automated tests, or the PR must explain why tests + are not feasible. +- A green CI build alone does not prove the changed lines were executed — + describe the test scenario if coverage is missing. +- Frontend changes: include before/after screenshots. +- Refactoring: exercise the code before and after and confirm identical behavior. + +## PR labels (automatic) + +PRs are auto-labeled by two workflows: + +### File-based labels (`actions/labeler`) + +| Label | Paths | +|-------|-------| +| `java` | `src/main/java/**` | +| `tests` | `src/test/**` | +| `ci` | `.github/workflows/**`, `Jenkinsfile` | +| `dependencies` | `pom.xml` | +| `documentation` | `*.md`, `src/doc/**` | +| `localization` | `src/main/resources/**/*.properties` | +| `frontend` | `src/main/webapp/**`, `src/main/resources/**/*.jelly` | +| `chore` | `.github/**`, `.gitignore`, `crowdin.yml` | + +### Release-drafter labels (`auto-label-pr.yml`) + +Added based on PR title/branch (for changelog generation): + +| Label | Trigger (title/branch) | +|-------|------------------------| +| `breaking` | Title contains "breaking" or "!:" | +| `major-enhancement` | Title contains "major" + "feat/enhancement" | +| `major-bug` | Title contains "major" + "fix/bug" | +| `deprecated` | Title contains "deprecat" | +| `removed` | Title contains "remove" | +| `enhancement` | Title starts with "feat"/"add" or branch `feature/*` | +| `bug` | Title starts with "fix" or branch `fix/*` | +| `documentation` | Title starts with "docs" or branch `docs/*` | +| `chore` | Title starts with "chore"/"ci:"/"build:" | +| `refactor` | Title starts with "refactor" | +| `security` | Title contains "security"/"cve-" | + +### Auto-approve countdown (owner/member PRs) + +For PRs by OWNER, MEMBER, or COLLABORATOR (non-draft), the label `merge-in-3-days-without-review` +is automatically added to start the auto-approve countdown. + +## Dependency updates + +- Include links to external changelogs and, if possible, full diffs. +- For new APIs or extension points, link to at least one consumer. + +## Upgrade guidelines + +- Only needed for breaking changes or changes requiring manual user action. +- When applicable, set the `upgrade-guide-needed` label. + +## Maintainer merge checklist + +Before marking `ready-for-merge`: + +1. At least **one approval** with no outstanding change requests. +2. All conversations resolved (or reviewer explicitly not blocking). +3. PR title is an accurate, imperative-mood changelog entry. +4. Correct release-drafter labels are set (see [label config](https://github.com/jenkinsci/.github/blob/ce466227c534c42820a597cb8e9cac2f2334920a/.github/release-drafter.yml#L9-L50)). +5. Java code changes are covered by automated tests. + +## Interface changes + +Document any UI or pipeline-DSL changes as examples under +[src/doc/examples/](src/doc/examples/readme.md). diff --git a/.github/copilot-pr-review-rules.md b/.github/copilot-pr-review-rules.md new file mode 100644 index 000000000..d0af7eb31 --- /dev/null +++ b/.github/copilot-pr-review-rules.md @@ -0,0 +1,134 @@ +# Copilot Instructions — PR Review & Cleanup Rules + +This file defines the mandatory checklist Copilot must follow when reviewing, +cleaning up, or triaging pull requests in this repository. + +> **Context:** This plugin is large, old, and highly complex. Regressions and +> performance degradation are real risks. Every PR review must account for +> testing depth and runtime impact — not just correctness. + +## PR review workflow + +When asked to review or clean up a PR, always follow these steps **in order**: + +### 1. Validate PR description + +- Read the PR title and body. +- Verify the description accurately reflects the actual code changes. +- Flag mismatches: wrong description, copy-paste errors, or stale text from + a template that was not filled in. + +### 2. Validate linked items + +- Check linked issues (Jira `JENKINS-XXXXX` or GitHub `#XXXXX`). +- Confirm the linked issue matches the intent of the PR. +- Flag PRs that reference an unrelated or wrong issue. +- Minor improvements may omit an issue; bug fixes and features must have one. + +### 3. Validate the implementation + +- Review the diff: does the code change actually solve the described problem? +- Look for incomplete changes, dead code, debug leftovers, or unrelated edits + that got mixed in. +- Ensure new code follows the project coding rules (see `copilot-instructions.md`). + +### 4. Review all comments — open AND closed + +- **Open comments** must be resolved or explicitly accepted with a meaningful + response before the PR can be merged. No open comment may be ignored. +- **Closed / resolved comments** should be read for context — they reveal + prior reviewer concerns, rejected approaches, and design decisions. + +### 5. Propose a continuation plan + +After reviewing the PR state, always provide **at least one concrete proposal** +for how to move the PR forward. This should include: + +- What is missing or broken and what needs to change. +- Specific next steps (code changes, rebases, comment resolutions). +- Whether the PR should be continued, split, or closed in favour of a fresh PR. +- If the PR has been idle for a long time, assess whether the approach is still + viable given the current state of `master`. + +### 6. Define testing — manual AND automated + +Testing is **mandatory** for every non-trivial change. The plugin is complex +and regressions are costly. Always provide: + +#### Manual testing steps + +- Describe concrete steps to verify the change locally: + ``` + mvn hpi:run + # then in Jenkins UI → Lockable Resources → ... + ``` +- Include expected outcomes (what the user should see / not see). +- For lock/unlock logic changes, describe multi-job concurrency scenarios. +- For UI changes, list which pages to check and what to look for. + +#### Automated testing requirements + +- **Unit tests** (`src/test/java/...`) for any new or changed logic. +- **Integration tests** using `JenkinsRule` for pipeline-level behaviour. +- **Configuration-as-Code tests** (`ConfigurationAsCodeTest`) when config + structures change. +- If the PR lacks tests, write them or explain precisely why they cannot be + added — "no tests" without justification is not acceptable. +- Run the full test suite before approving: + ``` + mvn clean verify + ``` +- If tests are flaky or slow, note it — do not silently skip them. + +### 7. Evaluate performance impact + +This plugin manages shared resources under contention. Performance matters. + +- **Lock/unlock hot paths:** Changes to `LockableResourcesManager`, + `LockStepExecution`, or queue handling must be evaluated for: + - Lock contention and synchronization overhead + - Iteration over all resources (O(n) scans) + - Unnecessary save/persist calls (`save()` triggers disk I/O) +- **Memory:** Watch for resource leaks, unbounded collections, or retained + references to `Run` / `FlowNode` objects. +- **Scalability:** Consider behaviour with 100+ resources and 50+ concurrent + builds waiting for locks. +- If a change affects a hot path, request or write a benchmark scenario: + ```java + // Example: time lock acquisition with N resources and M waiters + ``` +- Flag any change that adds `synchronized` blocks, new `save()` calls, or + full-collection scans without justification. + +### 8. Update the source branch + +- The source branch **must** be up-to-date with the target before merge. +- If the branch is behind, rebase or merge the target into the source branch + first. +- Never merge a PR with an outdated source branch. + +### 9. Target branch rules + +- The default target branch is **`master`**. +- A different target branch is only acceptable for long-running feature + development branches (explicitly agreed upon beforehand). +- If the target branch looks wrong, flag it immediately. + +## Summary checklist + +Use this quick checklist when processing any PR: + +``` +[ ] Description matches the actual changes +[ ] Linked issue (if any) is correct and relevant +[ ] Implementation solves the described problem +[ ] No unrelated changes mixed in +[ ] All open comments resolved or explicitly accepted +[ ] Closed comments reviewed for context +[ ] Continuation plan proposed (next steps / split / close) +[ ] Manual testing steps documented +[ ] Automated tests present or justified absence +[ ] Performance impact evaluated (hot paths, sync, save, memory) +[ ] Source branch is up-to-date with target +[ ] Target branch is master (or justified exception) +``` 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/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..ff80e06f3 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,44 @@ +# .github/labeler.yml — actions/labeler v5 schema +# Labels PRs based on changed files. +# For release-drafter compatibility, see also: auto-label-pr.yml + +# ═══════════════════════════════════════════════════════════════════ +# File-based labels (auto-applied by actions/labeler) +# ═══════════════════════════════════════════════════════════════════ + +java: + - changed-files: + - any-glob-to-any-file: 'src/main/java/**' + +tests: + - changed-files: + - any-glob-to-any-file: 'src/test/**' + +ci: + - changed-files: + - any-glob-to-any-file: '.github/workflows/**' + - any-glob-to-any-file: 'Jenkinsfile' + +dependencies: + - changed-files: + - any-glob-to-any-file: 'pom.xml' + +documentation: + - changed-files: + - any-glob-to-any-file: '*.md' + - any-glob-to-any-file: 'src/doc/**' + +localization: + - changed-files: + - any-glob-to-any-file: 'src/main/resources/**/*.properties' + +frontend: + - changed-files: + - any-glob-to-any-file: 'src/main/webapp/**' + - any-glob-to-any-file: 'src/main/resources/**/*.jelly' + +chore: + - changed-files: + - any-glob-to-any-file: '.github/**' + - any-glob-to-any-file: '.gitignore' + - any-glob-to-any-file: 'crowdin.yml' diff --git a/.github/workflows/auto-approve-owner-prs.yml b/.github/workflows/auto-approve-owner-prs.yml new file mode 100644 index 000000000..0ff593355 --- /dev/null +++ b/.github/workflows/auto-approve-owner-prs.yml @@ -0,0 +1,32 @@ +# Auto-approve owner PRs that receive no review within 3 days. +# +# Uses the reusable action: https://github.com/mPokornyETM/auto-approve-stale-prs +# +# Countdown labels give reviewers clear visibility: +# Day 0 → merge-in-3-days-without-review +# Day 1 → merge-in-2-days-without-review +# Day 2 → merge-in-1-day-without-review +# Day 3 → approved + merged-without-review (auto-merge takes over) +# +# Only PRs authored by a repo OWNER or MEMBER are processed. +# PRs that already have at least one approval are skipped. +name: Auto-approve owner PRs + +on: + schedule: + - cron: "0 8 * * *" # daily at 08:00 UTC + workflow_dispatch: # allow manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + countdown: + runs-on: ubuntu-latest + steps: + - uses: mPokornyETM/auto-approve-stale-prs@v1 + with: + days-until-approve: '3' + author-associations: 'OWNER' + merge-method: 'squash' diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml new file mode 100644 index 000000000..bff50b7fe --- /dev/null +++ b/.github/workflows/auto-label-pr.yml @@ -0,0 +1,146 @@ +# Auto-label PRs based on title, branch, and author association. +# Adds release-drafter compatible labels for changelog generation. +# Also adds auto-approve countdown label for owner/member PRs. +name: Auto-label PR + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + auto-label: + runs-on: ubuntu-latest + steps: + - name: Auto-label based on title and branch + uses: actions/github-script@v9 + with: + script: | + const pr = context.payload.pull_request; + const title = pr.title.toLowerCase(); + const branch = pr.head.ref.toLowerCase(); + const author = pr.user.login; + const authorAssoc = pr.author_association; + const isDraft = pr.draft; + + const labelsToAdd = []; + const labelsToRemove = []; + + // Get current labels + const currentLabels = pr.labels.map(l => l.name); + + console.log(`PR #${pr.number}: "${pr.title}"`); + console.log(` Branch: ${branch}`); + console.log(` Author: ${author} (${authorAssoc})`); + console.log(` Draft: ${isDraft}`); + console.log(` Current labels: ${currentLabels.join(', ') || 'none'}`); + + // ═══════════════════════════════════════════════════════════════ + // Release-drafter compatible labels based on title/branch + // ═══════════════════════════════════════════════════════════════ + + // Breaking changes + if (title.includes('breaking') || title.includes('!:') || branch.startsWith('breaking/')) { + labelsToAdd.push('breaking'); + } + + // Major enhancements + if (title.includes('major') && (title.includes('feat') || title.includes('enhancement'))) { + labelsToAdd.push('major-enhancement'); + } + + // Major bugs + if (title.includes('major') && (title.includes('fix') || title.includes('bug'))) { + labelsToAdd.push('major-bug'); + } + + // Removed/Deprecated + if (title.includes('remove') || title.includes('deprecat')) { + if (title.includes('deprecat')) { + labelsToAdd.push('deprecated'); + } else { + labelsToAdd.push('removed'); + } + } + + // Regular features/enhancements + if ((title.startsWith('feat') || title.includes('feature') || title.includes('enhancement') || + title.startsWith('add ') || title.startsWith('add:') || + branch.startsWith('feature/') || branch.startsWith('feat/')) && + !currentLabels.includes('major-enhancement')) { + labelsToAdd.push('enhancement'); + } + + // Bug fixes + if ((title.startsWith('fix') || title.includes('bugfix') || title.includes('bug fix') || + title.includes('regression') || branch.startsWith('fix/') || branch.startsWith('bugfix/')) && + !currentLabels.includes('major-bug')) { + labelsToAdd.push('bug'); + } + + // Documentation + if (title.startsWith('docs') || title.startsWith('doc:') || + branch.startsWith('docs/') || branch.startsWith('doc/')) { + labelsToAdd.push('documentation'); + } + + // Chore/Maintenance + if (title.startsWith('chore') || title.startsWith('maint') || + title.startsWith('ci:') || title.startsWith('build:') || + branch.startsWith('chore/') || branch.startsWith('maint/')) { + labelsToAdd.push('chore'); + } + + // Refactoring + if (title.startsWith('refactor') || title.includes('refactoring') || + branch.startsWith('refactor/')) { + labelsToAdd.push('refactor'); + } + + // Tests + if (title.startsWith('test') || branch.startsWith('test/')) { + labelsToAdd.push('tests'); + } + + // Security + if (title.includes('security') || title.includes('cve-') || + title.includes('vulnerability') || branch.startsWith('security/')) { + labelsToAdd.push('security'); + } + + // ═══════════════════════════════════════════════════════════════ + // Auto-approve countdown for owner/member PRs + // ═══════════════════════════════════════════════════════════════ + + const countdownLabel = 'merge-in-3-days-without-review'; + const hasCountdownLabel = currentLabels.some(l => l.startsWith('merge-in-')); + + // Add countdown label for trusted authors (non-draft only) + // OWNER = repo owner, MEMBER = org member, COLLABORATOR = added as collaborator + const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; + if (trustedAssociations.includes(authorAssoc) && !isDraft && !hasCountdownLabel) { + labelsToAdd.push(countdownLabel); + console.log(` → Adding auto-approve countdown for ${authorAssoc}`); + } + + // ═══════════════════════════════════════════════════════════════ + // Apply labels + // ═══════════════════════════════════════════════════════════════ + + // Filter out labels that already exist + const newLabels = labelsToAdd.filter(l => !currentLabels.includes(l)); + + if (newLabels.length > 0) { + console.log(` → Adding labels: ${newLabels.join(', ')}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: newLabels + }); + } else { + console.log(' → No new labels to add'); + } 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..70b5bc459 --- /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.16.2 + 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/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 000000000..33b3e35e8 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,24 @@ +# Auto-enable squash merge for Dependabot PRs +# GitHub will automatically merge once all required checks pass +# +# Uses pull_request_target so the workflow runs in the base-branch context +# and the default GITHUB_TOKEN has write permissions (Dependabot-triggered +# pull_request events only expose a read-only token). +name: Dependabot Auto-merge + +on: pull_request_target + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Enable auto-merge + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ github.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/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 000000000..01efdb112 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,41 @@ +name: PR Labels + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + label: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + steps: + - name: Check out + uses: actions/checkout@v6 + + - name: Run labeler + uses: actions/labeler@v6 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/labeler.yml + + required: + name: PR Labels - Required + runs-on: ubuntu-latest + if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }} + needs: + - label + steps: + - name: Verify required jobs + shell: bash + run: | + set -euo pipefail + echo "label: ${{ needs.label.result }}" + if [ "${{ needs.label.result }}" != "success" ]; then + echo "::error::PR Labels failed" + exit 1 + fi diff --git a/.github/workflows/rebase-open-prs.yml b/.github/workflows/rebase-open-prs.yml new file mode 100644 index 000000000..8275286f7 --- /dev/null +++ b/.github/workflows/rebase-open-prs.yml @@ -0,0 +1,22 @@ +name: Rebase open PRs + +on: + push: + branches: + - master + +permissions: + contents: write + pull-requests: write + +jobs: + rebase-prs: + runs-on: ubuntu-latest + steps: + - uses: mPokornyETM/rebase-open-prs-action@v1 + env: + # Use PAT to trigger subsequent workflows (security scan, etc.) + # Falls back to github.token if GH_TOKEN secret is not set + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + with: + github-token: ${{ secrets.GH_TOKEN || github.token }} 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..22ace8034 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,9 @@ The MIT License -Copyright (c) 2013, 6WIND S.A. All rights reserved. +Copyright 2013-2015, 6WIND S.A. +Copyright 2016-2018, Antonio Muñiz +Copyright 2019-2021, TobiX +Copyright 2022-2026, Martin Pokorny and contributors 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..87cf11c20 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +### What does this PR do? + + + +### Testing done + + + +### Checklist + +- [ ] Automated tests added or existing tests cover the change +- [ ] PR title is a clear, imperative-mood changelog entry +- [ ] Breaking changes or upgrade steps are documented below + +### Upgrade guidelines + + + +N/A diff --git a/README.md b/README.md index b8072fbda..f12b6913c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,492 @@ # 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/2026.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' + +``` + +#### Lock with a reason + +You can specify a reason why the resource is being locked. This is displayed +in the lockable resources UI while the resource is locked: + +```groovy +lock(resource: 'staging-server', reason: 'Running integration tests') { + echo 'Deploying to staging' +} +``` + +The reason helps other users understand why a resource is unavailable. + +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!' +} +``` + +#### Update resource properties + +The `updateLock` step allows pipelines to dynamically manage lockable resources without using the Jenkins UI. + +**Create a new resource:** + +```groovy +updateLock(resource: 'my-resource', createResource: true, setLabels: 'env-test team-a') +``` + +**Modify labels on an existing resource:** + +```groovy +// Replace all labels +updateLock(resource: 'my-resource', setLabels: 'new-label1 new-label2') + +// Add labels (keeps existing) +updateLock(resource: 'my-resource', addLabels: 'additional-label') + +// Remove specific labels +updateLock(resource: 'my-resource', removeLabels: 'old-label') + +// Add and remove in one step +updateLock(resource: 'my-resource', addLabels: 'new', removeLabels: 'old') +``` + +**Set a note on a resource:** + +```groovy +updateLock(resource: 'my-resource', setNote: 'Updated by build #${BUILD_NUMBER}') +``` + +**Delete a resource:** + +```groovy +updateLock(resource: 'my-resource', deleteResource: true) +``` + +> **Note:** Resources cannot be deleted while locked, queued, or reserved. + +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). + +---- + +## Dynamic resource behavior + +When new resources are added to the system, waiting jobs can automatically pick them up: + +- **Pipeline jobs** waiting in the lock step queue are re-evaluated immediately +- **Freestyle jobs** waiting in the Jenkins build queue are re-evaluated via queue maintenance + +This allows you to dynamically add resources to your resource pool without requiring waiting jobs to be restarted. + +### Example + +If a job is waiting for a resource with label `printer` and all existing printer resources are locked, adding a new resource with the label `printer` will allow the waiting job to acquire it immediately. + +### Limitations + +- **Modifying labels on existing resources does NOT trigger re-evaluation.** Only adding new resources triggers waiting jobs to re-evaluate. If you change labels on an existing resource, you can manually call `LockableResourcesManager.get().refreshQueue()` via Script Console to notify waiting jobs. +- **Removing resources** does not affect waiting jobs (they continue waiting for other resources with matching criteria). + +---- + +## 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..712268763 100644 --- a/pom.xml +++ b/pom.xml @@ -1,175 +1,196 @@ - + - 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 + 6.2153.vcf31911d10c4 + + - - - 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.541 + ${jenkins.baseline}.3 + jenkinsci/${project.artifactId}-plugin + Max + Low + false + false + 2.1.8 + + - - - - 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 + 6364.v16b_76a_4023c7 + 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 + - - - repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ - - + + + 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 + + + org.jenkins-ci.plugins.workflow + workflow-support + tests + test + + + org.jenkinsci.plugins + pipeline-model-definition + test + + + org.mockito + mockito-junit-jupiter + test + + + + uk.org.webcompere + system-stubs-jupiter + ${system-stubs.version} + + 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/dynamic-resource-pool-expansion.md b/src/doc/examples/dynamic-resource-pool-expansion.md new file mode 100644 index 000000000..70052bff0 --- /dev/null +++ b/src/doc/examples/dynamic-resource-pool-expansion.md @@ -0,0 +1,75 @@ +# Dynamic Resource Pool Expansion + +This example demonstrates how waiting jobs can automatically acquire newly added resources. + +## Use Case + +You have a pool of resources (e.g., test devices) labeled `test-device`. All devices are currently in use by running jobs. A new job starts and waits for a `test-device`. When you add a new device to the pool, the waiting job should automatically acquire it without needing to be restarted. + +## Pipeline Example + +### Job waiting for a resource + +```groovy +pipeline { + agent any + stages { + stage('Acquire Device') { + steps { + lock(label: 'test-device', quantity: 1, variable: 'DEVICE') { + echo "Acquired device: ${env.DEVICE}" + // Use the device + sh 'run-tests.sh' + } + } + } + } +} +``` + +### Adding a new resource via pipeline step + +While the job is waiting, you can add a new resource via a management job or the Script Console using the `updateLock` step: + +```groovy +// In a pipeline job +updateLock(resource: 'new-test-device-5', addLabels: 'test-device') +``` + +Or via Script Console: + +```groovy +import org.jenkins.plugins.lockableresources.LockableResourcesManager + +def manager = LockableResourcesManager.get() +manager.createResourceWithLabel('new-test-device-5', 'test-device') +``` + +The waiting job will automatically acquire `new-test-device-5` once it is added. + +## Freestyle Job Example + +For freestyle jobs configured with **Required Resources** (label: `test-device`), the same behavior applies. When a new resource with the matching label is added, the Jenkins queue is notified and the waiting freestyle job will be dispatched. + +## Limitations + +> **Important:** Modifying labels on existing resources does NOT trigger re-evaluation. + +Only **adding new resources** triggers waiting jobs to re-evaluate their resource requirements. If you: +- Change labels on an existing resource (e.g., add `test-device` label to an existing resource) +- The waiting jobs will **not** be notified + +To work around this limitation, you can manually trigger queue refresh via Script Console: + +```groovy +import org.jenkins.plugins.lockableresources.LockableResourcesManager + +LockableResourcesManager.get().refreshQueue() +``` + +This will invalidate the cached candidates and notify both pipeline and freestyle jobs to re-evaluate available resources. + +## Related + +- [JENKINS-46744](https://issues.jenkins.io/browse/JENKINS-46744) - Original issue requesting this behavior +- [GitHub #892](https://github.com/jenkinsci/lockable-resources-plugin/issues/892) - Implementation tracking issue 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/lock-specific-stages.md b/src/doc/examples/lock-specific-stages.md new file mode 100644 index 000000000..7cb85b046 --- /dev/null +++ b/src/doc/examples/lock-specific-stages.md @@ -0,0 +1,94 @@ +# Lock Specific Stages + +For long-running builds where a resource is only needed for part of the pipeline, +you can lock individual stages instead of the entire build. This allows other +jobs to use the resource while your build performs tasks that don't require it. + +## Lock a single stage + +Use the `options` block to lock a resource for just one stage: + +```groovy +pipeline { + agent any + stages { + stage('Build') { + steps { + echo 'Building for 27 minutes...' + echo 'Resource foo is not locked' + } + } + stage('Deploy') { + options { + lock(label: 'foo', quantity: 1) + } + steps { + echo 'Deploying for 3 minutes...' + echo 'Resource foo is locked' + } + } + stage('Verify') { + steps { + echo 'Verifying...' + echo 'Resource foo is not locked anymore' + } + } + } +} +``` + +## Lock multiple consecutive stages + +To lock a resource across multiple stages, nest them inside a parent stage +with the lock option: + +```groovy +pipeline { + agent any + stages { + stage('Build') { + steps { + echo 'Building...' + echo 'Resource foo is not locked' + } + } + stage('Deploy and Test') { + options { + lock(label: 'foo', quantity: 1) + } + stages { + stage('Deploy') { + steps { + echo 'Deploying...' + echo 'Resource foo is locked' + } + } + stage('Integration Test') { + steps { + echo 'Testing...' + echo 'Resource foo is still locked' + } + } + } + } + stage('Cleanup') { + steps { + echo 'Cleaning up...' + echo 'Resource foo is not locked anymore' + } + } + } +} +``` + +## When to use this pattern + +This pattern is useful when: + +- Your build has a long preparation phase that doesn't need the locked resource +- You want to maximize resource utilization across multiple jobs +- Only specific stages (like deployment or testing) require exclusive access + +## See also + +- [Locking multiple stages in declarative pipeline](locking-multiple-stages-in-declarative-pipeline.md) diff --git a/src/doc/examples/lock-with-timeout.md b/src/doc/examples/lock-with-timeout.md new file mode 100644 index 000000000..ce07f5dff --- /dev/null +++ b/src/doc/examples/lock-with-timeout.md @@ -0,0 +1,77 @@ +## Lock with allocation timeout + +By default, the `lock` step waits indefinitely until the requested resource becomes available. +With `timeoutForAllocateResource` you can set a maximum wait time — if the resource is not +acquired within that period, the build fails immediately instead of blocking the queue forever. + +This is useful when: +- You prefer a fast failure over an indefinitely blocked pipeline +- You want to detect resource starvation early +- Your CI/CD has SLAs that cap how long a job may wait + +### Pipeline (scripted) + +```groovy +node { + // Wait up to 5 minutes for the resource, then fail + lock(resource: 'my-printer', timeoutForAllocateResource: 5, timeoutUnit: 'MINUTES') { + echo "Printer locked, printing ..." + } +} +``` + +### Pipeline (declarative) + +```groovy +pipeline { + agent any + stages { + stage('Deploy') { + options { + lock(resource: 'staging-env', timeoutForAllocateResource: 10, timeoutUnit: 'MINUTES') + } + steps { + echo "Deploying to staging ..." + } + } + } +} +``` + +### Label-based locking with timeout + +```groovy +pipeline { + agent any + stages { + stage('Test') { + steps { + lock(label: 'phone', quantity: 1, variable: 'PHONE', + timeoutForAllocateResource: 2, timeoutUnit: 'MINUTES') { + echo "Running tests on ${env.PHONE}" + } + } + } + } +} +``` + +### Freestyle jobs + +In a freestyle job configuration, go to **This build requires lockable resources** and set: +- **Lock wait timeout**: the maximum time to wait (e.g. `5`) +- **Timeout unit**: `SECONDS`, `MINUTES`, or `HOURS` + +If the resource is not available within the configured timeout, the build is automatically +removed from the Jenkins queue. + +### Timeout values + +| `timeoutUnit` | Description | +|---------------|-------------| +| `SECONDS` | Timeout in seconds | +| `MINUTES` | Timeout in minutes (default) | +| `HOURS` | Timeout in hours | + +Setting `timeoutForAllocateResource: 0` (the default) disables the timeout — the build +waits indefinitely, which preserves the original behaviour. 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..7a9136f34 --- /dev/null +++ b/src/doc/examples/readme.md @@ -0,0 +1,16 @@ +# 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) +- [Lock specific stages](lock-specific-stages.md) +- [Locking multiple stages in declarative pipeline](locking-multiple-stages-in-declarative-pipeline.md) +- [Locking a random free resource](locking-random-free-resource.md) +- [Resource properties](resource-properties.md) +- [Scripted vs declarative pipeline](scripted-vs-declarative-pipeline.md) +- [Timeout inside lock](timeout-inside-lock.md) +- [Dynamic resource pool expansion](dynamic-resource-pool-expansion.md) +- [Lock with allocation timeout](lock-with-timeout.md) diff --git a/src/doc/examples/resource-properties.md b/src/doc/examples/resource-properties.md new file mode 100644 index 000000000..2f5ec0818 --- /dev/null +++ b/src/doc/examples/resource-properties.md @@ -0,0 +1,110 @@ +# Resource Properties + +Resources can have custom **properties** (name:value pairs) that are exposed +as environment variables when the resource is locked. + +## Defining properties + +Properties can be added to a resource via: + +- **Web UI** — Manage Jenkins → Lockable Resources → edit a resource → add properties +- **JCasC** (Jenkins Configuration as Code): + +```yaml +unclassified: + lockableResourcesManager: + resources: + - name: "staging-server" + properties: + - name: "HOST" + value: "192.168.1.10" + - name: "PORT" + value: "8080" +``` + +## Accessing properties in a pipeline + +Properties are exposed as environment variables **only when the `variable` +parameter is specified** in the `lock()` step. + +### Naming pattern + +| Variable | Value | +|----------|-------| +| `{variable}` | Comma-separated list of all locked resource names | +| `{variable}0` | Name of the first locked resource | +| `{variable}0_{PROPERTY_NAME}` | Value of that resource's property | +| `{variable}1` | Name of the second locked resource (if any) | +| `{variable}1_{PROPERTY_NAME}` | Value of the second resource's property | + +### Example: Read properties after locking by name + +```groovy +pipeline { + agent any + stages { + stage('Deploy') { + options { + lock(resource: 'staging-server', variable: 'LOCKED') + } + steps { + echo "Resource: ${env.LOCKED0}" // staging-server + echo "Host: ${env.LOCKED0_HOST}" // 192.168.1.10 + echo "Port: ${env.LOCKED0_PORT}" // 8080 + } + } + } +} +``` + +### Example: Lock by label and read properties + +```groovy +pipeline { + agent any + stages { + stage('Test') { + options { + lock(label: 'gpu', quantity: 1, variable: 'GPU') + } + steps { + echo "Got: ${env.GPU0}" + echo "GPU model: ${env.GPU0_MODEL}" + } + } + } +} +``` + +## Filtering resources by properties + +Use a `resourceMatchScript` to lock only resources whose properties match +specific criteria: + +```groovy +lock(extra: [ + [$class: 'LockableResourcesStruct', + resourceMatchScript: [ + $class: 'SecureGroovyScript', + script: ''' + resourceInstance.properties.any { + it.name == "ENV" && it.value == "staging" + } + ''', + sandbox: true + ], + resourceNumber: '1' + ] +]) { + echo "Got a staging resource: ${env.LOCKED_RESOURCE0}" +} +``` + +## Common pitfalls + +1. **Missing `variable` parameter** — without it, no environment variables are + created. This is the most common reason properties appear to be `null`. +2. **Property name is case-sensitive** — if the property is named `host`, the + env var is `LOCKED0_host`, not `LOCKED0_HOST`. +3. **Properties are only available inside the lock block** — they cannot be + accessed after the lock is released. 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/doc/examples/timeout-inside-lock.md b/src/doc/examples/timeout-inside-lock.md new file mode 100644 index 000000000..fa68003ac --- /dev/null +++ b/src/doc/examples/timeout-inside-lock.md @@ -0,0 +1,76 @@ +# Timeout Inside Lock + +When a pipeline uses both `timeout` and `lock`, the placement of `timeout` +determines whether queue wait time counts against the deadline. + +## Problem + +If `timeout` wraps the entire pipeline or stage, the clock starts before the +lock is acquired. A job that waits a long time in the queue may time out +before it gets a chance to run: + +```groovy +pipeline { + agent any + options { + // Clock starts immediately — includes queue wait time! + timeout(time: 5, unit: 'HOURS') + } + stages { + stage('Deploy') { + steps { + lock('my-resource') { + echo 'Deploying...' + } + } + } + } +} +``` + +## Solution + +Place `timeout` **inside** `lock` so the countdown begins only after the +resource has been acquired: + +```groovy +pipeline { + agent any + stages { + stage('Deploy') { + steps { + lock('my-resource') { + timeout(time: 5, unit: 'HOURS') { + echo 'Deploying...' + } + } + } + } + } +} +``` + +This way a job can wait in the queue as long as necessary without the +timeout expiring prematurely. + +## Stage-level variant + +The same pattern works with `options` at the stage level: + +```groovy +pipeline { + agent any + stages { + stage('Deploy') { + options { + lock('my-resource') + } + steps { + timeout(time: 5, unit: 'HOURS') { + echo 'Deploying...' + } + } + } + } +} +``` diff --git a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java index 12b7af7c4..3693236c5 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/BackwardCompatibility.java @@ -1,48 +1,60 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ package org.jenkins.plugins.lockableresources; 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..ae4a6f1c6 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/FreeDeadJobs.java @@ -0,0 +1,54 @@ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license 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(); + boolean freedAny = false; + 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(); + freedAny = true; + } + } + if (freedAny) { + LockableResourcesManager.scheduleQueueMaintenance(); + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java index 739a0b043..f6998ac66 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStep.java @@ -1,145 +1,325 @@ 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 hudson.util.ListBoxModel; 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 org.kohsuke.stapler.interceptor.RequirePOST; -import hudson.Extension; -import hudson.model.AutoCompletionCandidates; -import hudson.util.FormValidation; -import hudson.Util; +public class LockStep extends Step implements Serializable { + private static final Logger LOG = Logger.getLogger(LockStep.class.getName()); -import edu.umd.cs.findbugs.annotations.Nullable; -import edu.umd.cs.findbugs.annotations.CheckForNull; + 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; + + /** The reason why this resource is being locked, displayed in the UI while locked. */ + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String reason = 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; + + /** + * Timeout in the specified {@link #timeoutUnit} for waiting to acquire the resource. + * 0 means no timeout (wait indefinitely). When the timeout expires, the step fails + * with an exception instead of waiting forever. + */ + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public long timeoutForAllocateResource = 0; + + /** + * Time unit for {@link #timeoutForAllocateResource}. Defaults to MINUTES. + */ + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String timeoutUnit = "MINUTES"; + + // 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) { + if (resourceSelectStrategy != null && !resourceSelectStrategy.isEmpty()) { + // Validate the strategy is valid + 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(s -> s.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.joining(", ")))); + } + 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 setReason(String reason) { + if (reason != null && !reason.trim().isEmpty()) { + this.reason = reason.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; + } + + @DataBoundSetter + public void setTimeoutForAllocateResource(long timeoutForAllocateResource) { + this.timeoutForAllocateResource = Math.max(0, timeoutForAllocateResource); + } + + @DataBoundSetter + public void setTimeoutUnit(String timeoutUnit) { + if (timeoutUnit != null && !timeoutUnit.trim().isEmpty()) { + // Validate it is a valid TimeUnit name + try { + java.util.concurrent.TimeUnit.valueOf(timeoutUnit.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid timeoutUnit: " + timeoutUnit); + } + this.timeoutUnit = timeoutUnit.toUpperCase(Locale.ENGLISH); + } + } + + @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 ListBoxModel doFillResourceSelectStrategyItems(@AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + ListBoxModel items = new ListBoxModel(); + for (ResourceSelectStrategy resSelStrategy : ResourceSelectStrategy.values()) { + items.add(resSelStrategy.name()); + } + return items; + } + + @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(); + } + + @RequirePOST + public ListBoxModel doFillTimeoutUnitItems(@AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + ListBoxModel items = new ListBoxModel(); + items.add("Seconds", "SECONDS"); + items.add("Minutes", "MINUTES"); + items.add("Hours", "HOURS"); + return items; + } + + @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, reason); + 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, reason)); + } -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; + 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..04b5c6cd4 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,260 @@ 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, step.reason)) { + // 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); + String waitMsg = "[" + step + "] is not free, waiting for execution ..."; + if (step.timeoutForAllocateResource > 0) { + waitMsg += " (timeout: " + step.timeoutForAllocateResource + " " + + step.timeoutUnit.toLowerCase(java.util.Locale.ENGLISH) + ")"; + } + LockableResourcesManager.printLogs(waitMsg, Level.FINE, LOGGER, logger); + LockableResourcesManager lrm = LockableResourcesManager.get(); + lrm.queueContext( + getContext(), + resourceHolderList, + step.toString(), + step.variable, + step.inversePrecedence, + step.priority, + step.reason, + step.timeoutForAllocateResource, + step.timeoutUnit); + } + } + + 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..bc59d7c2a 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockStepResource.java @@ -1,122 +1,222 @@ 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; + + /** The reason why this resource is being locked, displayed in the UI while locked. */ + @CheckForNull + @SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility.") + public String reason = null; + + LockStepResource(@Nullable String resource, @Nullable String label, int quantity) { + this(resource, label, quantity, null); + } + + LockStepResource(@Nullable String resource, @Nullable String label, int quantity, @Nullable String reason) { + this.resource = Util.fixEmptyAndTrim(resource); + this.label = Util.fixEmptyAndTrim(label); + this.quantity = quantity; + this.reason = Util.fixEmptyAndTrim(reason); + } + + @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; + } + + @DataBoundSetter + public void setReason(String reason) { + this.reason = Util.fixEmptyAndTrim(reason); + } + + @Override + public String toString() { + return toString(resource, label, quantity, reason); + } + + public static String toString(String resource, String label, int quantity) { + return toString(resource, label, quantity, null); + } + + public static String toString(String resource, String label, int quantity, String reason) { + // a label takes always priority + StringBuilder sb = new StringBuilder(); + if (label != null) { + sb.append("Label: ").append(label); + if (quantity > 0) { + sb.append(", Quantity: ").append(quantity); + } + } else if (resource != null) { + sb.append("Resource: ").append(resource); + } else { + return "[no resource/label specified - probably a bug]"; + } + if (reason != null && !reason.isEmpty()) { + sb.append(", Reason: ").append(reason); + } + return sb.toString(); + } + + // ------------------------------------------------------------------------- + /** 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..34d4a1235 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResource.java @@ -1,364 +1,780 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; - import jenkins.model.Jenkins; - +import jenkins.util.SystemProperties; +import org.jenkins.plugins.lockableresources.util.Constants; 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 = ""; + + /** + * The reason why this resource is currently locked. Set via the lock() step's reason parameter. + * Cleared when the resource is unlocked. + */ + private String lockReason = ""; + + /** + * 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 static final long SCRIPT_CACHE_TTL_MS = + SystemProperties.getLong(Constants.SYSTEM_PROPERTY_SCRIPT_CACHE_TTL_MS, 30_000L); + private static final long LABEL_CACHE_TTL_MS = + SystemProperties.getLong(Constants.SYSTEM_PROPERTY_LABEL_CACHE_TTL_MS, 30_000L); + + /** Per-resource cache: Groovy script text -> (result, timestamp). */ + private transient volatile ConcurrentHashMap scriptCache; + /** Per-resource cache: label expression -> (result, timestamp). */ + private transient volatile ConcurrentHashMap labelCache; + + private static final class CachedResult { + final boolean value; + final long timestamp; + + CachedResult(boolean value) { + this.value = value; + this.timestamp = System.currentTimeMillis(); + } + + boolean isExpired(long ttlMs) { + return (System.currentTimeMillis() - timestamp) > ttlMs; + } + } + + private ConcurrentHashMap getScriptCache() { + ConcurrentHashMap c = scriptCache; + if (c == null) { + synchronized (this) { + c = scriptCache; + if (c == null) { + scriptCache = c = new ConcurrentHashMap<>(); + } + } + } + return c; + } + + private ConcurrentHashMap getLabelCache() { + ConcurrentHashMap c = labelCache; + if (c == null) { + synchronized (this) { + c = labelCache; + if (c == null) { + labelCache = c = new ConcurrentHashMap<>(); + } + } + } + return c; + } + + void invalidateCaches() { + ConcurrentHashMap sc = scriptCache; + if (sc != null) sc.clear(); + ConcurrentHashMap lc = labelCache; + if (lc != null) lc.clear(); + } + + 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); + } + + /** + * Returns the reason why this resource is currently locked. + * + * @return The lock reason, or empty string if not set. + */ + @Exported + public String getLockReason() { + return this.lockReason; + } + + /** + * Sets the reason why this resource is being locked. This is typically set via the lock() step's + * reason parameter and cleared when the resource is unlocked. + * + * @param lockReason The reason for locking, or null to clear. + */ + public void setLockReason(@Nullable String lockReason) { + this.lockReason = Util.fixNull(lockReason); + } + + @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); + } + invalidateCaches(); + } + + /** + * 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; + } + + if (LABEL_CACHE_TTL_MS > 0) { + ConcurrentHashMap cache = getLabelCache(); + CachedResult cached = cache.get(candidate); + if (cached != null && !cached.isExpired(LABEL_CACHE_TTL_MS)) { + return cached.value; + } + boolean result = evaluateLabelExpression(candidate); + cache.put(candidate, new CachedResult(result)); + return result; + } + return evaluateLabelExpression(candidate); + } + + private boolean evaluateLabelExpression(@NonNull String candidate) { + 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 { + if (SCRIPT_CACHE_TTL_MS > 0) { + String cacheKey = script.getScript(); + ConcurrentHashMap cache = getScriptCache(); + CachedResult cached = cache.get(cacheKey); + if (cached != null && !cached.isExpired(SCRIPT_CACHE_TTL_MS)) { + return cached.value; + } + boolean result = evaluateScript(script, params); + cache.put(cacheKey, new CachedResult(result)); + return result; + } + return evaluateScript(script, params); + } + + private boolean evaluateScript(@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); + binding.setVariable("resourceLockReason", lockReason); + 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) { + reserve(userName, null); + } + + public void reserve(String userName, String reason) { + setReservedBy(userName); + setReservedTimestamp(new Date()); + setLockReason(reason); + } + + public void unReserve() { + this.setReservedBy(null); + this.setReservedTimestamp(null); + this.setLockReason(null); + this.stolen = false; + } + + public void reset() { + this.unReserve(); + this.unqueue(); + this.setBuild(null); + this.setLockReason(null); + invalidateCaches(); + } + + /** + * 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()); + setLockReason(sourceResource.getLockReason()); + } + } + + /** + * 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(""); + setLockReason(""); + } + + /** + * 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..7aee8812f 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResources.java @@ -1,33 +1,25 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (c) 2015, 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ package org.jenkins.plugins.lockableresources; 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..9d16e36af 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java @@ -1,802 +1,1784 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.init.Terminator; +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.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; 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; + + /** + * Controls whether ephemeral resources can be created automatically. + * When enabled (default), locking a non-existent resource creates it dynamically. + * When disabled, locking a non-existent resource will block until it is manually created. + */ + private boolean allowEphemeralResources = true; + + /** + * 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); + + private static final boolean asyncSaveEnabled = + SystemProperties.getBoolean(Constants.SYSTEM_PROPERTY_ASYNC_SAVE, true); + private static final long saveCoalesceMs = + SystemProperties.getLong(Constants.SYSTEM_PROPERTY_SAVE_COALESCE_MS, 1000L); + + private transient volatile AtomicBoolean savePending; + private transient volatile ScheduledExecutorService saveExecutor; + + /** Single scheduled timeout task. Guarded by {@link #syncResources}. */ + private transient java.util.concurrent.ScheduledFuture nextTimeoutTask; + + /** Deadline (epoch ms) the current {@link #nextTimeoutTask} targets. 0 = none. */ + private transient long nextTimeoutDeadline; + + @DataBoundSetter + public void setAllowEmptyOrNullValues(boolean allowEmptyOrNullValues) { + this.allowEmptyOrNullValues = allowEmptyOrNullValues; + } + + public boolean isAllowEmptyOrNullValues() { + return allowEmptyOrNullValues; + } + + /** + * Sets whether ephemeral resources can be created automatically. + * + * @param allowEphemeralResources true to allow automatic creation of ephemeral resources + */ + @DataBoundSetter + public void setAllowEphemeralResources(boolean allowEphemeralResources) { + this.allowEphemeralResources = allowEphemeralResources; + } + + /** + * Returns whether ephemeral resources are allowed. + * + * @return true if ephemeral resources can be created automatically + */ + public boolean isAllowEphemeralResources() { + return allowEphemeralResources; + } + + // --------------------------------------------------------------------------- + /** 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() { + synchronized (syncResources) { + ArrayList declaredResources = new ArrayList<>(); + for (LockableResource r : this.resources) { + 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) { + synchronized (syncResources) { + List matching = new ArrayList<>(); + for (LockableResource r : this.resources) { + 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 == null) return false; + 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; + + // Take a snapshot of keys to avoid ConcurrentModificationException. + // Only invalidate entries that actually contain the freed resource, + // preserving cache for other queue items (important at scale with 1000+ items). + Set keys = new HashSet<>(cachedCandidates.asMap().keySet()); + for (Long queueItemId : keys) { + List candidates = cachedCandidates.getIfPresent(queueItemId); + 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 { + + final SecureGroovyScript systemGroovyScript; + try { + systemGroovyScript = requiredResources.getResourceMatchScript(); + } catch (Descriptor.FormException x) { + throw new ExecutionException(x); + } + boolean candidatesByScript = (systemGroovyScript != null); + + // Resolve candidates outside syncResources when possible — label matching + // and Groovy script evaluation are heavyweight and should not extend the + // critical section. + // NOTE: We store unmodifiable lists in the cache for thread-safety. Multiple + // threads may read from the cache concurrently, so cached lists must not be + // modified. Create a mutable copy below when modifications are needed. + List candidates = null; + if (candidatesByScript || (requiredResources.label != null && !requiredResources.label.isEmpty())) { + candidates = cachedCandidates.getIfPresent(queueItemId); + if (candidates == null) { + candidates = (systemGroovyScript == null) + ? getResourcesWithLabel(requiredResources.label) + : getResourcesMatchingScript(systemGroovyScript, params); + // Store as unmodifiable to prevent accidental modification of cached data + cachedCandidates.put(queueItemId, Collections.unmodifiableList(candidates)); + } + } + + 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; + } + + if (candidates != null) { + // Mutable copy required - cached list is unmodifiable for thread-safety + candidates = new ArrayList<>(candidates); + candidates.retainAll(this.resources); + } else { + candidates = requiredResources.required; + } + + 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) { + return lock(resourcesToLock, build, (String) null); + } + + // --------------------------------------------------------------------------- + /** + * Try to lock the resource and return true if locked. + * + * @param resourcesToLock The resources to lock. + * @param build The build that is locking the resources. + * @param reason The reason why the resources are being locked (displayed in UI). + * @return true if locked successfully. + */ + public boolean lock(List resourcesToLock, Run build, @Nullable String reason) { + + LOGGER.fine("lock it: " + resourcesToLock + " for build " + build + " with reason: " + reason); + + 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); + if (reason != null && !reason.isEmpty()) { + r.setLockReason(reason); + } + } + + 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); + resource.setLockReason(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(); + } + scheduleQueueMaintenance(); + } + + 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, nextContext.getReason()); + 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 toRemove = new ArrayList<>(); + QueuedContextStruct nextEntry = null; + long earliestDeadline = Long.MAX_VALUE; + + // 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); + toRemove.add(entry); + continue; + } + + // check if the entry has timed out waiting for resources + if (entry.isTimedOut()) { + LOGGER.info("Queue entry timed out waiting for resources: " + entry); + toRemove.add(entry); + PrintStream logger = entry.getLogger(); + String msg = "[" + entry.getResourceDescription() + + "] timed out waiting for resource allocation after " + + entry.getTimeoutForAllocateResource() + " " + + entry.getTimeoutUnit().toLowerCase(java.util.Locale.ENGLISH); + printLogs(msg, logger, Level.WARNING); + entry.getContext() + .onFailure(new org.jenkins.plugins.lockableresources.queue.LockWaitTimeoutException(msg)); + continue; + } + + // track the earliest deadline among remaining entries for rescheduling + long deadline = entry.getTimeoutDeadlineMillis(); + if (deadline > 0 && deadline < earliestDeadline) { + earliestDeadline = deadline; + } + + LOGGER.finest("oldest win - index: " + idx + " " + entry); + + nextEntry = getNextQueuedContextEntry(entry); + } + + if (!toRemove.isEmpty()) { + this.queuedContexts.removeAll(toRemove); + } + + // reschedule for the next earliest deadline + scheduleTimeoutAt(earliestDeadline); + + 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 and ephemeral resources are allowed. + * + * @param name the resource name + * @return true if resource was created, false if it already exists or ephemeral resources are + * disabled + */ + public boolean createResource(@CheckForNull String name) { + if (!allowEphemeralResources) { + LOGGER.fine("Ephemeral resources are disabled, not creating resource: " + name); + return false; + } + 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); + + // Invalidate cache and process waiting pipeline jobs while still holding the lock + cachedCandidates.invalidateAll(); + while (proceedNextContext()) { + // process as many contexts as possible + } + + if (doSave) { + this.save(); + } + } + // Notify Jenkins queue for freestyle jobs (must be outside synchronized block) + scheduleQueueMaintenance(); + 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) { + return reserve(resources, userName, null); + } + + // --------------------------------------------------------------------------- + /** + * Reserves an available resource for the userName indefinitely (until that person, or some + * explicit scripted action, decides to release the resource). + * + * @param resources list of resources to reserve + * @param userName the user reserving the resources + * @param reason the reason for reserving (optional) + * @return true if all resources were successfully reserved, false if any was not free + */ + public boolean reserve(List resources, String userName, String reason) { + LOGGER.info("reserve() called user='" + userName + "' resources=" + getResourcesNames(resources) + " reason='" + + reason + "'"); + synchronized (syncResources) { + for (LockableResource r : resources) { + if (!r.isFree()) { + LOGGER.fine("reserve() failed because resource not free: " + r.getName()); + return false; + } + } + for (LockableResource r : resources) { + r.reserve(userName, reason); + } + save(); + } + LOGGER.info("reserve() succeeded user='" + userName + "' resources=" + getResourcesNames(resources)); + 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) { + return steal(resources, userName, null); + } + + // --------------------------------------------------------------------------- + /** + * 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). + * + * @param resources list of resources to steal + * @param userName the user stealing the resources + * @param reason the reason for stealing (optional) + * @return true if stolen successfully + */ + public boolean steal(List resources, String userName, String reason) { + synchronized (syncResources) { + for (LockableResource r : resources) { + r.setReservedBy(userName); + r.setStolen(); + } + unlockResources(resources); + Date date = new Date(); + for (LockableResource r : resources) { + r.setReservedTimestamp(date); + r.setLockReason(reason); + } + 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(); + } + scheduleQueueMaintenance(); + } + + // --------------------------------------------------------------------------- + @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(); + } + + while (proceedNextContext()) { + // process as many contexts as possible + } + + save(); + } + scheduleQueueMaintenance(); + } + + // --------------------------------------------------------------------------- + /** + * 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); + } + scheduleQueueMaintenance(); + } + + // --------------------------------------------------------------------------- + /** + * 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) { + queueContext( + context, + requiredResources, + resourceDescription, + variableName, + inversePrecedence, + priority, + null, + 0, + "MINUTES"); + } + + // --------------------------------------------------------------------------- + /* + * Adds the given context and the required resources to the queue if + * this context is not yet queued, with reason and timeout for resource allocation. + */ + @Restricted(NoExternalUse.class) + public void queueContext( + StepContext context, + List requiredResources, + String resourceDescription, + String variableName, + boolean inversePrecedence, + int priority, + String reason, + long timeoutForAllocateResource, + String timeoutUnit) { + 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, + reason, + timeoutForAllocateResource, + timeoutUnit); + + 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++; + } + + this.queuedContexts.add(queueIndex, newQueueItem); + printLogs( + requiredResources + " added into queue at position " + queueIndex, + newQueueItem.getLogger(), + Level.FINE); + + save(); + + // If this entry has a timeout and its deadline is earlier than the + // currently scheduled one, (re)schedule so it fires on time. + long deadline = newQueueItem.getTimeoutDeadlineMillis(); + if (deadline > 0 && (nextTimeoutDeadline == 0 || deadline < nextTimeoutDeadline)) { + scheduleTimeoutAt(deadline); + } + } + } + + // --------------------------------------------------------------------------- + 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); + } + + // --------------------------------------------------------------------------- + /** + * Trigger an immediate Queue re-evaluation so items waiting for lockable + * resources are dispatched as soon as resources become available, instead of + * waiting for the next 5-second timer tick. + *

+ * Must be called outside {@code synchronized(syncResources)} to avoid + * holding the plugin lock while Jenkins acquires the Queue lock. + */ + public static void scheduleQueueMaintenance() { + Jenkins j = Jenkins.getInstanceOrNull(); + if (j != null) { + j.getQueue().scheduleMaintenance(); + } + } + + // --------------------------------------------------------------------------- + /** + * Refresh the queue to allow waiting jobs to re-evaluate available resources. + *

+ * This method should be called after modifying labels on existing resources, + * as label changes do not automatically trigger queue re-evaluation. + *

+ * It performs the following actions: + *

    + *
  1. Invalidates the cached resource candidates
  2. + *
  3. Processes waiting pipeline job contexts
  4. + *
  5. Triggers Jenkins queue maintenance for freestyle jobs
  6. + *
+ */ + public void refreshQueue() { + // Invalidate cached candidates so waiting jobs re-evaluate with current labels + cachedCandidates.invalidateAll(); + + // Process waiting pipeline jobs (also handles timeouts) + synchronized (syncResources) { + while (proceedNextContext()) { + // process as many contexts as possible + } + } + + // Notify Jenkins queue for freestyle jobs + scheduleQueueMaintenance(); + } + + // --------------------------------------------------------------------------- + /** + * Checks for timed-out entries in the pipeline lock queue and fails them. + * Called by {@link org.jenkins.plugins.lockableresources.queue.LockWaitTimeoutPeriodicWork} + * as a safety net. + */ + @Restricted(NoExternalUse.class) + public void checkTimeouts() { + synchronized (syncResources) { + // proceedNextContext → getNextQueuedContext handles timeouts + rescheduling + while (proceedNextContext()) { + // process as many contexts as possible + } + } + } + + // --------------------------------------------------------------------------- + /** + * Schedules (or reschedules) the single timeout task to fire at the given + * deadline. If {@code deadline} is {@link Long#MAX_VALUE} the current task + * is cancelled and nothing new is scheduled. + * Must be called while holding {@link #syncResources}. + */ + private void scheduleTimeoutAt(long deadline) { + // Cancel the current task — we will either replace it or clear it + if (nextTimeoutTask != null) { + nextTimeoutTask.cancel(false); + nextTimeoutTask = null; + nextTimeoutDeadline = 0; + } + + if (deadline == Long.MAX_VALUE || deadline <= 0) { + return; + } - try { - getConfigFile().write(this); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(),e); + nextTimeoutDeadline = deadline; + // Small buffer so the deadline has definitely passed when we check + long delayMs = Math.max(0, deadline - System.currentTimeMillis()) + 500L; + LOGGER.fine("Scheduling timeout check in " + delayMs + "ms"); + nextTimeoutTask = jenkins.util.Timer.get() + .schedule( + () -> { + LOGGER.fine("Scheduled timeout check fired"); + synchronized (syncResources) { + nextTimeoutDeadline = 0; + nextTimeoutTask = null; + while (proceedNextContext()) { + // process as many contexts as possible + } + } + }, + delayMs, + java.util.concurrent.TimeUnit.MILLISECONDS); + } + + // --------------------------------------------------------------------------- + private AtomicBoolean getSavePending() { + AtomicBoolean sp = savePending; + if (sp == null) { + synchronized (this) { + sp = savePending; + if (sp == null) { + savePending = sp = new AtomicBoolean(false); + } + } + } + return sp; + } + + private ScheduledExecutorService getSaveExecutor() { + ScheduledExecutorService se = saveExecutor; + if (se == null) { + synchronized (this) { + se = saveExecutor; + if (se == null) { + saveExecutor = se = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "lockable-resources-async-save"); + t.setDaemon(true); + return t; + }); } + } } + return se; + } + + @Override + public void save() { + if (enableSave == -1) { + // read system property and cache it. + enableSave = SystemProperties.getBoolean(Constants.SYSTEM_PROPERTY_DISABLE_SAVE) ? 0 : 1; + } + + if (enableSave == 0) return; // saving is disabled - private static final Logger LOGGER = Logger.getLogger(LockableResourcesManager.class.getName()); + if (BulkChange.contains(this)) return; + if (asyncSaveEnabled && saveCoalesceMs > 0) { + if (getSavePending().compareAndSet(false, true)) { + getSaveExecutor().schedule(this::doSave, saveCoalesceMs, TimeUnit.MILLISECONDS); + } + } else { + doSave(); + } + } + + private void doSave() { + getSavePending().set(false); + synchronized (syncResources) { + try { + getConfigFile().write(this); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(), e); + } + } + } + + // --------------------------------------------------------------------------- + /** + * Flush any pending async save during Jenkins shutdown so that lock state + * is never lost on an orderly restart. + */ + @Terminator + public static void flushPendingSave() { + LockableResourcesManager lrm = LockableResourcesManager.get(); + ScheduledExecutorService se = lrm.saveExecutor; + if (se != null) { + se.shutdownNow(); + } + if (lrm.savePending != null && lrm.savePending.compareAndSet(true, false)) { + lrm.doSave(); + LOGGER.fine("Flushed pending async save during shutdown"); + } + } + + // --------------------------------------------------------------------------- + /** 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..574ad5423 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java @@ -1,276 +1,377 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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 hudson.util.ListBoxModel; import java.util.ArrayList; import java.util.List; - +import java.util.regex.Pattern; +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.DataBoundSetter; 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; + + /** + * Timeout for waiting to acquire the resource, in the specified {@link #lockTimeoutUnit}. + * 0 means no timeout (wait indefinitely). + */ + private long lockTimeout = 0; + + /** + * Time unit for {@link #lockTimeout}. Defaults to MINUTES. + */ + private String lockTimeoutUnit = "MINUTES"; + + @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; + } + + public long getLockTimeout() { + return lockTimeout; + } + + @DataBoundSetter + public void setLockTimeout(long lockTimeout) { + this.lockTimeout = lockTimeout; + } + + public String getLockTimeoutUnit() { + return lockTimeoutUnit; + } + + @DataBoundSetter + public void setLockTimeoutUnit(String lockTimeoutUnit) { + this.lockTimeoutUnit = lockTimeoutUnit; + } + + @Extension + public static class DescriptorImpl extends JobPropertyDescriptor { + + /** Detects {@code ${...}} variable references that are resolved at build time. */ + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}"); + + @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+")) { + // Skip validation for names containing build-parameter references + if (VARIABLE_PATTERN.matcher(name).find()) { + continue; + } + boolean found = LockableResourcesManager.get().resourceExist(name); + if (!found) wrongNames.add(name); + } + if (wrongNames.isEmpty()) { + if (VARIABLE_PATTERN.matcher(names).find()) { + return FormValidation.warning(Messages.warning_resourceNameContainsVariable()); + } + 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 { + // Skip label validation when it contains build-parameter references + if (VARIABLE_PATTERN.matcher(label).find()) { + return FormValidation.warning(Messages.warning_labelContainsVariable()); + } + 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(); + } + + // Skip numeric validation when number contains build-parameter references + if (VARIABLE_PATTERN.matcher(number).find()) { + return FormValidation.warning(Messages.warning_resourceNumberContainsVariable()); + } + + 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); + } + } + + @RequirePOST + public ListBoxModel doFillLockTimeoutUnitItems(@AncestorInPath Item item) { + checkPermission(item); + ListBoxModel items = new ListBoxModel(); + items.add("Seconds", "SECONDS"); + items.add("Minutes", "MINUTES"); + items.add("Hours", "HOURS"); + return items; + } + } +} 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/UpdateLockStep.java b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStep.java new file mode 100644 index 000000000..eeb5bb40c --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStep.java @@ -0,0 +1,312 @@ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ +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.AutoCompletionCandidates; +import hudson.model.Item; +import hudson.model.TaskListener; +import hudson.util.FormValidation; +import java.io.Serializable; +import java.util.Collections; +import java.util.Set; +import java.util.logging.Logger; +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 org.kohsuke.stapler.interceptor.RequirePOST; + +/** + * Pipeline step to update the definition of a lockable resource. + * + *

This step allows pipelines to: + *

    + *
  • Create new resources
  • + *
  • Delete existing resources
  • + *
  • Add, remove, or set labels on resources
  • + *
  • Set notes on resources
  • + *
+ */ +public class UpdateLockStep extends Step implements Serializable { + + private static final Logger LOG = Logger.getLogger(UpdateLockStep.class.getName()); + private static final long serialVersionUID = -7955849755535282258L; + + @CheckForNull + private String resource = null; + + @CheckForNull + private String addLabels = null; + + @CheckForNull + private String setLabels = null; + + @CheckForNull + private String removeLabels = null; + + @CheckForNull + private String setNote = null; + + private boolean createResource = false; + private boolean deleteResource = false; + + @DataBoundConstructor + public UpdateLockStep() { + // default constructor + } + + @CheckForNull + public String getResource() { + return resource; + } + + @DataBoundSetter + public void setResource(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(); + } + } + + @CheckForNull + public String getAddLabels() { + return addLabels; + } + + @DataBoundSetter + public void setAddLabels(String addLabels) { + addLabels = Util.fixEmptyAndTrim(addLabels); + if (addLabels != null) { + this.addLabels = addLabels; + } + } + + @CheckForNull + public String getSetLabels() { + return setLabels; + } + + @DataBoundSetter + public void setSetLabels(String setLabels) { + setLabels = Util.fixEmptyAndTrim(setLabels); + if (setLabels != null) { + this.setLabels = setLabels; + } + } + + @CheckForNull + public String getRemoveLabels() { + return removeLabels; + } + + @DataBoundSetter + public void setRemoveLabels(String removeLabels) { + removeLabels = Util.fixEmptyAndTrim(removeLabels); + if (removeLabels != null) { + this.removeLabels = removeLabels; + } + } + + @CheckForNull + public String getSetNote() { + return setNote; + } + + @DataBoundSetter + public void setSetNote(String setNote) { + setNote = Util.fixEmptyAndTrim(setNote); + if (setNote != null) { + this.setNote = setNote; + } + } + + public boolean isCreateResource() { + return createResource; + } + + @DataBoundSetter + public void setCreateResource(boolean createResource) { + this.createResource = createResource; + } + + public boolean isDeleteResource() { + return deleteResource; + } + + @DataBoundSetter + public void setDeleteResource(boolean deleteResource) { + this.deleteResource = deleteResource; + } + + /** + * Validates the step configuration. + * + * @throws IllegalArgumentException if the configuration is invalid + */ + public void validate() { + if (Util.fixEmptyAndTrim(resource) == null) { + throw new IllegalArgumentException(Messages.UpdateLockStep_error_resourceRequired()); + } + if (deleteResource && createResource) { + throw new IllegalArgumentException(Messages.UpdateLockStep_error_deleteAndCreateConflict()); + } + if (deleteResource && (addLabels != null || setLabels != null || removeLabels != null || setNote != null)) { + throw new IllegalArgumentException(Messages.UpdateLockStep_error_deleteWithOtherOptions()); + } + if (setLabels != null && (addLabels != null || removeLabels != null)) { + throw new IllegalArgumentException(Messages.UpdateLockStep_error_setLabelsConflict()); + } + } + + @Override + public StepExecution start(StepContext context) { + return new UpdateLockStepExecution(this, context); + } + + @Override + public String toString() { + StringBuilder sb = + new StringBuilder("UpdateLockStep{resource='").append(resource).append("'"); + if (createResource) sb.append(", createResource=true"); + if (deleteResource) sb.append(", deleteResource=true"); + if (setLabels != null) sb.append(", setLabels='").append(setLabels).append("'"); + if (addLabels != null) sb.append(", addLabels='").append(addLabels).append("'"); + if (removeLabels != null) + sb.append(", removeLabels='").append(removeLabels).append("'"); + if (setNote != null) sb.append(", setNote='").append(setNote).append("'"); + sb.append("}"); + return sb.toString(); + } + + @Extension + public static final class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "updateLock"; + } + + @NonNull + @Override + public String getDisplayName() { + return Messages.UpdateLockStep_displayName(); + } + + @Override + public boolean takesImplicitBlockArgument() { + return false; + } + + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + + /** + * Provides auto-completion for resource names. + */ + @RequirePOST + public AutoCompletionCandidates doAutoCompleteResource( + @QueryParameter String value, @AncestorInPath Item item) { + return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value, item); + } + + /** + * Validates the resource name. + */ + @RequirePOST + public FormValidation doCheckResource(@QueryParameter String value, @AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + value = Util.fixEmptyAndTrim(value); + if (value == null) { + return FormValidation.error(Messages.UpdateLockStep_error_resourceRequired()); + } + return FormValidation.ok(); + } + + /** + * Validates addLabels option - cannot be used with setLabels. + */ + @RequirePOST + public FormValidation doCheckAddLabels( + @QueryParameter String value, @QueryParameter String setLabels, @AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + return doCheckLabelOperation(value, setLabels); + } + + /** + * Validates removeLabels option - cannot be used with setLabels. + */ + @RequirePOST + public FormValidation doCheckRemoveLabels( + @QueryParameter String value, @QueryParameter String setLabels, @AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + return doCheckLabelOperation(value, setLabels); + } + + private FormValidation doCheckLabelOperation(String value, String setLabels) { + if (Util.fixEmptyAndTrim(value) != null && Util.fixEmptyAndTrim(setLabels) != null) { + return FormValidation.error(Messages.UpdateLockStep_error_setLabelsConflict()); + } + return FormValidation.ok(); + } + + /** + * Validates deleteResource option - cannot be combined with other modify options. + */ + @RequirePOST + public FormValidation doCheckDeleteResource( + @QueryParameter boolean value, + @QueryParameter String setLabels, + @QueryParameter String addLabels, + @QueryParameter String removeLabels, + @QueryParameter String setNote, + @QueryParameter boolean createResource, + @AncestorInPath Item item) { + if (item != null) { + item.checkPermission(Item.CONFIGURE); + } else { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + } + if (!value) { + return FormValidation.ok(); + } + if (createResource) { + return FormValidation.error(Messages.UpdateLockStep_error_deleteAndCreateConflict()); + } + if (Util.fixEmptyAndTrim(setLabels) != null + || Util.fixEmptyAndTrim(addLabels) != null + || Util.fixEmptyAndTrim(removeLabels) != null + || Util.fixEmptyAndTrim(setNote) != null) { + return FormValidation.error(Messages.UpdateLockStep_error_deleteWithOtherOptions()); + } + return FormValidation.ok(); + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java new file mode 100644 index 000000000..d6d453201 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java @@ -0,0 +1,193 @@ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ +package org.jenkins.plugins.lockableresources; + +import hudson.model.TaskListener; +import java.io.PrintStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +/** + * Execution logic for the {@link UpdateLockStep}. + */ +public class UpdateLockStepExecution extends AbstractStepExecutionImpl implements Serializable { + + private static final long serialVersionUID = 1583205294263267002L; + private static final Logger LOGGER = Logger.getLogger(UpdateLockStepExecution.class.getName()); + + private final UpdateLockStep step; + + public UpdateLockStepExecution(UpdateLockStep step, StepContext context) { + super(context); + this.step = step; + } + + @Override + public boolean start() throws Exception { + step.validate(); + + PrintStream logger = getContext().get(TaskListener.class).getLogger(); + LockableResourcesManager lrm = LockableResourcesManager.get(); + + synchronized (LockableResourcesManager.syncResources) { + if (step.isDeleteResource()) { + handleDeleteResource(lrm, logger); + } else { + handleUpdateResource(lrm, logger); + } + } + + getContext().onSuccess(null); + return true; + } + + /** + * Handles the deletion of a resource. + */ + private void handleDeleteResource(LockableResourcesManager lrm, PrintStream logger) { + String resourceName = step.getResource(); + LockableResource resource = lrm.fromName(resourceName); + + if (resource == null) { + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] does not exist, nothing to delete.", + Level.WARNING, + LOGGER, + logger); + return; + } + + if (resource.isLocked() || resource.isQueued()) { + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] is currently locked or queued, cannot delete.", + Level.WARNING, + LOGGER, + logger); + throw new IllegalStateException(Messages.UpdateLockStep_error_resourceInUse(resourceName)); + } + + if (resource.isReserved()) { + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] is currently reserved, cannot delete.", + Level.WARNING, + LOGGER, + logger); + throw new IllegalStateException(Messages.UpdateLockStep_error_resourceReserved(resourceName)); + } + + lrm.removeResources(Collections.singletonList(resource)); + lrm.save(); + + LockableResourcesManager.printLogs("Resource [" + resourceName + "] deleted.", Level.FINE, LOGGER, logger); + } + + /** + * Handles updating an existing resource or creating a new one. + */ + private void handleUpdateResource(LockableResourcesManager lrm, PrintStream logger) { + String resourceName = step.getResource(); + LockableResource resource = lrm.fromName(resourceName); + + // Create resource if it doesn't exist and createResource is true + if (resource == null) { + if (step.isCreateResource()) { + lrm.createResource(resourceName); + resource = lrm.fromName(resourceName); + if (resource != null) { + // Make it persistent (not ephemeral) since it was explicitly created + resource.setEphemeral(false); + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] created.", Level.FINE, LOGGER, logger); + } + } else { + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] does not exist. Use createResource: true to create it.", + Level.WARNING, + LOGGER, + logger); + throw new IllegalStateException(Messages.UpdateLockStep_error_resourceNotFound(resourceName)); + } + } + + if (resource == null) { + throw new IllegalStateException(Messages.UpdateLockStep_error_resourceNotFound(resourceName)); + } + + // Handle labels + updateLabels(resource, logger); + + // Handle note + if (step.getSetNote() != null) { + resource.setNote(step.getSetNote()); + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] note updated.", Level.FINE, LOGGER, logger); + } + + lrm.save(); + LockableResourcesManager.printLogs("Resource [" + resourceName + "] updated.", Level.FINE, LOGGER, logger); + } + + /** + * Updates labels on the resource based on step configuration. + */ + private void updateLabels(LockableResource resource, PrintStream logger) { + String resourceName = resource.getName(); + + if (step.getSetLabels() != null) { + // setLabels replaces all existing labels + List newLabels = parseLabels(step.getSetLabels()); + resource.setLabels(String.join(" ", newLabels)); + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] labels set to: " + newLabels, Level.FINE, LOGGER, logger); + } else if (step.getAddLabels() != null || step.getRemoveLabels() != null) { + // addLabels/removeLabels modify existing labels + List currentLabels = new ArrayList<>(resource.getLabelsAsList()); + + if (step.getAddLabels() != null) { + List labelsToAdd = parseLabels(step.getAddLabels()); + for (String label : labelsToAdd) { + if (!currentLabels.contains(label)) { + currentLabels.add(label); + } + } + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] added labels: " + labelsToAdd, Level.FINE, LOGGER, logger); + } + + if (step.getRemoveLabels() != null) { + List labelsToRemove = parseLabels(step.getRemoveLabels()); + currentLabels.removeAll(labelsToRemove); + LockableResourcesManager.printLogs( + "Resource [" + resourceName + "] removed labels: " + labelsToRemove, + Level.FINE, + LOGGER, + logger); + } + + resource.setLabels(currentLabels.stream().collect(Collectors.joining(" "))); + } + } + + /** + * Parses a whitespace-separated label string into a list of labels. + */ + private List parseLabels(String labels) { + if (labels == null || labels.trim().isEmpty()) { + return new ArrayList<>(); + } + return Arrays.stream(labels.trim().split("\\s+")) + .filter(l -> !l.isEmpty()) + .collect(Collectors.toList()); + } +} 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..df7f18d61 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/actions/LockableResourcesRootAction.java @@ -1,170 +1,733 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.Util; +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 reason = Util.fixEmptyAndTrim(req.getParameter("reason")); + + LOGGER.info("doReserve called for resources=" + LockableResourcesManager.getResourcesNames(resources) + + " reason='" + reason + "' fromIP=" + req.getRemoteAddr()); + + String userName = getUserName(); + if (userName == null) { + LOGGER.warning("doReserve: userName is null (unauthenticated?) for resources=" + + LockableResourcesManager.getResourcesNames(resources)); + rsp.sendError(401, Messages.error_notAuthenticated()); + return; + } + + boolean ok = LockableResourcesManager.get().reserve(resources, userName, reason); + if (!ok) { + LOGGER.info("doReserve failed - resource already locked: " + + LockableResourcesManager.getResourcesNames(resources)); + rsp.sendError( + 423, Messages.error_resourceAlreadyLocked(LockableResourcesManager.getResourcesNames(resources))); + return; + } + LOGGER.info("doReserve succeeded for user='" + userName + "' resources=" + + LockableResourcesManager.getResourcesNames(resources)); + 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 reason = Util.fixEmptyAndTrim(req.getParameter("reason")); + + String userName = getUserName(); + if (userName == null) { + rsp.sendError(401, Messages.error_notAuthenticated()); + return; + } + + LockableResourcesManager.get().steal(resources, userName, reason); + 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..87831367e 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,182 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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 net.jcip.annotations.GuardedBy; +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; - } - - } - + @GuardedBy("logs") + private final List logs = new ArrayList<>(); + + @GuardedBy("resourcesInUse") + 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 (this.resourcesInUse) { + resourcesInUse.addAll(resourceNames); + } + } + + public void removeUsedResources(List resourceNames) { + synchronized (this.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)); + } + } + + @Restricted(NoExternalUse.class) + public List getReadOnlyResourcesInUse() { + synchronized (this.resourcesInUse) { + return new ArrayList<>(Collections.unmodifiableCollection(this.resourcesInUse)); + } + } + + /** Copy constructor, primarily for {@link #writeReplace} */ + private LockedResourcesBuildAction(LockedResourcesBuildAction other) { + synchronized (other.logs) { + synchronized (other.resourcesInUse) { + this.logs.addAll(other.getReadOnlyLogs()); + this.resourcesInUse.addAll(other.getReadOnlyResourcesInUse()); + } + } + } + /** + * Ensure iteration during XStream marshalling is also synchronized, + * otherwise we tend to get {@link java.util.ConcurrentModificationException}.
+ * + * The recommended approach is to copy-on-write the properties so a + * snapshot can always be scraped consistently. But this can be costly + * at run-time, so we use the next-best option: produce a consistent + * replica of the current object for actual saving only on demand.
+ * + * This method is found by XStream via reflection.
+ */ + protected synchronized Object writeReplace() { + return new LockedResourcesBuildAction(this); + } + + 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..d726376b8 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java @@ -1,120 +1,126 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ package org.jenkins.plugins.lockableresources.queue; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.EnvVars; 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.io.IOException; 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) { + AbstractBuild abstractBuild = (AbstractBuild) build; + LockableResourcesManager lrm = LockableResourcesManager.get(); + synchronized (lrm.syncResources) { + Job proj = Utils.getProject(build); + List required = new ArrayList<>(); + + // Resolve build parameters so that ${PARAM} references in + // resource names, labels, and numbers are expanded. + EnvVars buildEnv; + try { + buildEnv = abstractBuild.getEnvironment(listener); + } catch (IOException | InterruptedException e) { + buildEnv = new EnvVars(); + } + + LockableResourcesStruct resources = Utils.requiredResources(proj, buildEnv); + + 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/LockWaitTimeoutException.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockWaitTimeoutException.java new file mode 100644 index 000000000..7c9ea1e61 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockWaitTimeoutException.java @@ -0,0 +1,18 @@ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ +package org.jenkins.plugins.lockableresources.queue; + +/** + * Exception thrown when a lock step times out waiting for resource allocation. + */ +public class LockWaitTimeoutException extends Exception { + + private static final long serialVersionUID = 1L; + + public LockWaitTimeoutException(String message) { + super(message); + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockWaitTimeoutPeriodicWork.java b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockWaitTimeoutPeriodicWork.java new file mode 100644 index 000000000..a59e4660f --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockWaitTimeoutPeriodicWork.java @@ -0,0 +1,41 @@ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ +package org.jenkins.plugins.lockableresources.queue; + +import hudson.Extension; +import hudson.model.PeriodicWork; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jenkins.plugins.lockableresources.LockableResourcesManager; + +/** + * Periodic work that checks for timed-out lock queue entries. + * + *

This runs every 15 seconds to check if any queued pipeline contexts have exceeded + * their {@code timeoutForAllocateResource}. Without this, timed-out entries would only + * be detected when resources are freed (which might never happen if all resources are + * permanently busy). + */ +@Extension +public class LockWaitTimeoutPeriodicWork extends PeriodicWork { + + private static final Logger LOGGER = Logger.getLogger(LockWaitTimeoutPeriodicWork.class.getName()); + + @Override + public long getRecurrencePeriod() { + return 15_000L; // 15 seconds + } + + @Override + protected void doRun() { + LockableResourcesManager lrm = LockableResourcesManager.get(); + if (lrm.getCurrentQueuedContext().isEmpty()) { + return; + } + LOGGER.log(Level.FINEST, "Checking for timed-out lock queue entries"); + lrm.checkTimeouts(); + } +} 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..66f3de1d8 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java @@ -1,192 +1,279 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.EnvVars; 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; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; 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.jenkins.plugins.lockableresources.RequiredResourcesProperty; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @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(); + + /** Tracks the deadline (epoch millis) for each queue item waiting for resources. */ + private final transient ConcurrentHashMap deadlines = new ConcurrentHashMap<>(); + + 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; + + // Extract build parameters so that ${PARAM} references in resource + // names, labels, and numbers are expanded before scheduling. + EnvVars paramEnv = Utils.getParametersAsEnvVars(item); + LockableResourcesStruct resources = Utils.requiredResources(project, paramEnv); + 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); + deadlines.remove(item.getId()); + return null; + } else { + LOGGER.finest(project.getName() + " waiting for resources"); + CauseOfBlockage timeout = checkFreestyleTimeout(item, project); + if (timeout != null) return timeout; + return new BecauseResourcesLocked(resources); + } + + } else { + if (LockableResourcesManager.get().queue(resources.required, item.getId(), project.getFullDisplayName())) { + LOGGER.finest(project.getName() + " reserved resources " + resources.required); + deadlines.remove(item.getId()); + return null; + } else { + LOGGER.finest(project.getName() + " waiting for resources " + resources.required); + CauseOfBlockage timeout = checkFreestyleTimeout(item, project); + if (timeout != null) return timeout; + return new BecauseResourcesLocked(resources); + } + } + } + + /** + * Checks whether a freestyle queue item has exceeded the configured lock timeout. + * If timed out, the item is cancelled from the Jenkins queue. + * + * @return a {@link BecauseResourcesTimeout} if timed out, {@code null} otherwise + */ + private CauseOfBlockage checkFreestyleTimeout(Queue.Item item, Job project) { + RequiredResourcesProperty prop = project.getProperty(RequiredResourcesProperty.class); + if (prop == null || prop.getLockTimeout() <= 0) { + return null; + } + + long now = System.currentTimeMillis(); + long deadline = deadlines.computeIfAbsent(item.getId(), k -> { + long timeoutMillis; + try { + timeoutMillis = TimeUnit.valueOf(prop.getLockTimeoutUnit()).toMillis(prop.getLockTimeout()); + } catch (IllegalArgumentException e) { + timeoutMillis = TimeUnit.MINUTES.toMillis(prop.getLockTimeout()); + } + return now + timeoutMillis; + }); + + if (now >= deadline) { + LOGGER.log(Level.INFO, "{0} timed out waiting for lockable resources (timeout: {1} {2})", new Object[] { + project.getFullName(), + prop.getLockTimeout(), + prop.getLockTimeoutUnit().toLowerCase(java.util.Locale.ENGLISH) + }); + deadlines.remove(item.getId()); + // Cancel the queue item + jenkins.model.Jenkins.get().getQueue().cancel(item); + return new BecauseResourcesTimeout(project.getFullName(), prop.getLockTimeout(), prop.getLockTimeoutUnit()); + } + return null; + } + + 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(); + } + } + + // Only for UI + @Restricted(NoExternalUse.class) + public static class BecauseResourcesTimeout extends CauseOfBlockage { + + private final String projectName; + private final long timeout; + private final String timeoutUnit; + + public BecauseResourcesTimeout(String projectName, long timeout, String timeoutUnit) { + this.projectName = projectName; + this.timeout = timeout; + this.timeoutUnit = timeoutUnit; + } + + @Override + public String getShortDescription() { + return projectName + " cancelled: timed out after " + timeout + " " + + timeoutUnit.toLowerCase(java.util.Locale.ENGLISH) + + " waiting for lockable resources"; + } + + /** + * Signals that this blockage is fatal and the queue item should be removed. + * Since Jenkins 2.532 (core PR #11173), the queue checks this flag and + * cancels the item itself instead of re-adding it as a {@code BlockedItem}. + */ + @Override + public boolean isFatal() { + return true; + } + } } 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..61dbd8354 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java @@ -1,128 +1,169 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (c) 2013, Aki Asikainen. 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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 = env.expand(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..f6b089095 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/QueuedContextStruct.java @@ -1,20 +1,24 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.concurrent.TimeUnit; +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 +27,257 @@ */ 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; + + /* + * The reason why the resource is being locked. + */ + private String reason; + + private int priority = 0; + + /* + * Timeout for waiting to acquire the resource, in the specified timeoutUnit. + * 0 means no timeout (wait indefinitely). + */ + private long timeoutForAllocateResource = 0; + + /* + * Time unit for the timeout. Defaults to MINUTES. + */ + private String timeoutUnit = "MINUTES"; + + /* + * Pre-computed absolute deadline (epoch millis) when this entry times out. + * 0 means no timeout. Calculated once at construction time to avoid + * repeated TimeUnit.valueOf() + toMillis() on every queue check. + */ + private long timeoutDeadlineMillis = 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, lockableResourcesStruct, resourceDescription, variableName, priority, null, 0, "MINUTES"); + } + + /* + * Constructor for the QueuedContextStruct class with reason and timeout. + */ + @Restricted(NoExternalUse.class) + public QueuedContextStruct( + StepContext context, + List lockableResourcesStruct, + String resourceDescription, + String variableName, + int priority, + String reason, + long timeoutForAllocateResource, + String timeoutUnit) { + this.context = context; + this.lockableResourcesStruct = lockableResourcesStruct; + this.resourceDescription = resourceDescription; + this.variableName = variableName; + this.priority = priority; + this.reason = reason; + this.timeoutForAllocateResource = timeoutForAllocateResource; + this.timeoutUnit = timeoutUnit != null ? timeoutUnit : "MINUTES"; + this.id = UUID.randomUUID().toString(); + + // Pre-compute deadline once to avoid repeated calculation on every queue check + if (timeoutForAllocateResource > 0) { + try { + TimeUnit unit = TimeUnit.valueOf(this.timeoutUnit); + this.timeoutDeadlineMillis = System.currentTimeMillis() + unit.toMillis(timeoutForAllocateResource); + } catch (IllegalArgumentException e) { + LOGGER.warning("Invalid timeoutUnit '" + this.timeoutUnit + "', timeout disabled"); + this.timeoutDeadlineMillis = 0; + } + } + } + + @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; + } + + /** + * Checks whether this queued context has exceeded its allocation timeout. + * Uses a pre-computed deadline for performance since this is called on every queue check. + * + * @return true if a timeout was set and has expired, false otherwise + */ + @Restricted(NoExternalUse.class) + public boolean isTimedOut() { + return timeoutDeadlineMillis > 0 && System.currentTimeMillis() > timeoutDeadlineMillis; + } + + /** + * Returns the pre-computed deadline (epoch millis) when this entry times out. + * 0 means no timeout is configured. + */ + @Restricted(NoExternalUse.class) + public long getTimeoutDeadlineMillis() { + return this.timeoutDeadlineMillis; + } + + /** + * Returns the configured timeout for resource allocation. + */ + @Restricted(NoExternalUse.class) + public long getTimeoutForAllocateResource() { + return this.timeoutForAllocateResource; + } + + /** + * Returns the time unit for the allocation timeout. + */ + @Restricted(NoExternalUse.class) + public String getTimeoutUnit() { + return this.timeoutUnit; + } + + @Restricted(NoExternalUse.class) + /* + * Gets the reason for locking. + */ + public String getReason() { + return this.reason; + } + + @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..234937686 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java @@ -1,48 +1,138 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * 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. * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* + * The MIT License + * + * See the "LICENSE.txt" file for full copyright and license information. + */ 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.ParameterValue; +import hudson.model.ParametersAction; import hudson.model.Queue; - import hudson.model.Run; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.jenkins.plugins.lockableresources.ExcludeFromJacocoGeneratedReport; import org.jenkins.plugins.lockableresources.RequiredResourcesProperty; +import org.jenkinsci.plugins.variant.OptionalExtension; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +public final class Utils { + private Utils() {} + + /** Pattern to detect {@code ${...}} variable references in configuration values. */ + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}"); + + @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(); + } + + /** + * Build the required-resources structure for a project, without additional environment variables. + * + * @see #requiredResources(Job, EnvVars) + */ + @Deprecated + @ExcludeFromJacocoGeneratedReport + @CheckForNull + @Restricted(NoExternalUse.class) + public static LockableResourcesStruct requiredResources(@NonNull Job project) { + return requiredResources(project, null); + } + + /** + * Build the required-resources structure for a project, merging any additional + * environment variables (e.g. build parameters) into the expansion context. + * + * @param project the job whose {@link RequiredResourcesProperty} is read + * @param additionalEnv extra variables to use when expanding {@code ${...}} references; + * may be {@code null} + * @return the struct, or {@code null} if the project has no lockable-resource property + */ + @CheckForNull + @Restricted(NoExternalUse.class) + public static LockableResourcesStruct requiredResources( + @NonNull Job project, @CheckForNull EnvVars additionalEnv) { + EnvVars env = new EnvVars(); + + for (var ma : ExtensionList.lookup(MatrixAssist.class)) { + env.putAll(ma.getCombination(project)); + project = ma.getMainProject(project); + } + + if (additionalEnv != null) { + env.putAll(additionalEnv); + } + + RequiredResourcesProperty property = project.getProperty(RequiredResourcesProperty.class); + if (property != null) return new LockableResourcesStruct(property, env); -public class Utils { + return null; + } - public static Job getProject(Queue.Item item) { - if (item.task instanceof Job) - return (Job) item.task; - return null; - } + /** + * Extract build parameters from a {@link Queue.Item} and return them as {@link EnvVars} + * so that {@code ${PARAM}} references in resource names, labels and numbers are expanded. + */ + @NonNull + @Restricted(NoExternalUse.class) + public static EnvVars getParametersAsEnvVars(@NonNull Queue.Item item) { + EnvVars env = new EnvVars(); + List paramActions = item.getActions(ParametersAction.class); + for (ParametersAction action : paramActions) { + if (action == null) continue; + for (ParameterValue p : action.getParameters()) { + if (p == null) continue; + Object value = p.getValue(); + if (value != null) { + env.put(p.getName(), value.toString()); + } + } + } + return env; + } - public static Job getProject(Run build) { - Object p = build.getParent(); - return (Job) p; - } + /** + * Returns {@code true} when the given string contains at least one {@code ${...}} variable + * reference that will be resolved at build time. + */ + @Restricted(NoExternalUse.class) + public static boolean containsVariable(@CheckForNull String value) { + return value != null && VARIABLE_PATTERN.matcher(value).find(); + } - 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..72ecef5cc --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/util/Constants.java @@ -0,0 +1,29 @@ +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"; + /// Enable asynchronous save to reduce syncResources lock hold time. + public static final String SYSTEM_PROPERTY_ASYNC_SAVE = "org.jenkins.plugins.lockableresources.ASYNC_SAVE"; + /// Coalesce window (ms) for async saves — rapid state changes within this window are batched. + public static final String SYSTEM_PROPERTY_SAVE_COALESCE_MS = + "org.jenkins.plugins.lockableresources.SAVE_COALESCE_MS"; + /// TTL (ms) for Groovy script evaluation result cache per resource. + public static final String SYSTEM_PROPERTY_SCRIPT_CACHE_TTL_MS = + "org.jenkins.plugins.lockableresources.SCRIPT_CACHE_TTL_MS"; + /// TTL (ms) for label expression evaluation result cache per resource. + public static final String SYSTEM_PROPERTY_LABEL_CACHE_TTL_MS = + "org.jenkins.plugins.lockableresources.LABEL_CACHE_TTL_MS"; +} 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..3841a88ac 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/util/SerializableSecureGroovyScript.java @@ -1,39 +1,21 @@ /* * The MIT License * - * Copyright (c) 2017 Oleg Nenashev. - * - * 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. + * See the "LICENSE.txt" file for full copyright and license information. */ 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 +24,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..835dfba36 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,47 @@ - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
+ 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..a912b9524 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/config.properties @@ -0,0 +1,35 @@ +# 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.reason.title=Reason +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.timeoutForAllocateResource.title=Timeout for resource allocation +entry.timeoutUnit.title=Timeout unit +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-reason.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-reason.html new file mode 100644 index 000000000..b1fee9c81 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-reason.html @@ -0,0 +1,13 @@ +
+

+ An optional reason explaining why the resource is being locked. + This is displayed in the lockable resources UI while the resource is locked, + helping other users understand why a resource is currently unavailable. +

+

+ Example: +

lock(resource: 'my-resource', reason: 'Running integration tests') {
+    // ...
+}
+

+
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-timeoutForAllocateResource.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-timeoutForAllocateResource.html new file mode 100644 index 000000000..e4b2b5bd1 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStep/help-timeoutForAllocateResource.html @@ -0,0 +1,9 @@ +
+

+ Maximum time (in the specified time unit) to wait for a resource to become available. +

+

+ If the resource is not acquired within this time, the pipeline step will fail with a timeout error. + Set to 0 (the default) to wait indefinitely. +

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

+ The time unit for the resource allocation timeout. +

+

+ Allowed values: SECONDS, MINUTES, HOURS. Default is MINUTES. +

+
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..27811e565 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,16 @@ - - - - - - - - - + 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..7488d4d8f --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/config.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. + +entry.resource.title=Resource +entry.label.title=Label +entry.quantity.title=Quantity +entry.reason.title=Reason \ 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-reason.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-reason.html new file mode 100644 index 000000000..4ade12d16 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockStepResource/help-reason.html @@ -0,0 +1,6 @@ +
+

+ An optional reason explaining why the resource is being locked. + This is displayed in the lockable resources UI while the resource is locked. +

+
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..98cef6ca3 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,27 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> - - - - - - -
-
-
-
-
-
+ 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..0f2bf056a --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/config.properties @@ -0,0 +1,29 @@ +# 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 +configuration.allowEphemeralResources.title=Allow ephemeral resources +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/LockableResourcesManager/help-allowEphemeralResources.html b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/help-allowEphemeralResources.html new file mode 100644 index 000000000..20fa88798 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/LockableResourcesManager/help-allowEphemeralResources.html @@ -0,0 +1,14 @@ +
+ Controls whether ephemeral resources can be created automatically. +
    +
  • Enabled (default): When a lock step references a resource name that does not exist, + the plugin automatically creates a temporary (ephemeral) resource for the duration of the build.
  • +
  • Disabled: Locking a non-existent resource will cause the job to block until the + resource is manually created. This prevents accidental creation of resources due to typos + or misconfiguration.
  • +
+

+ Note: This setting only affects resources referenced by name. Resources selected by + label are not affected. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties index f634f92cb..032f5db1b 100644 --- a/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties +++ b/src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties @@ -8,9 +8,51 @@ # * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * # 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.notAuthenticated=You must be authenticated to perform this action. +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 +# warnings (build-parameter references) +warning.resourceNameContainsVariable=Resource name contains build parameter references. \ + Validation will occur at build time. +warning.labelContainsVariable=Label contains build parameter references. \ + Validation will occur at build time. +warning.resourceNumberContainsVariable=Resource number contains build parameter references. \ + Validation will occur at build time. +UpdateLockStep.displayName=Update lockable resource +# UpdateLockStep errors +UpdateLockStep.error.resourceRequired=The resource name must be specified. +UpdateLockStep.error.resourceNotFound=Resource {0} does not exist. Use createResource: true to create it. +UpdateLockStep.error.resourceInUse=Resource {0} is currently locked or queued and cannot be deleted. +UpdateLockStep.error.resourceReserved=Resource {0} is currently reserved and cannot be deleted. +UpdateLockStep.error.deleteAndCreateConflict=Cannot specify both deleteResource and createResource. +UpdateLockStep.error.deleteWithOtherOptions=When deleteResource is true, other modification options cannot be specified. +UpdateLockStep.error.setLabelsConflict=Cannot use setLabels together with addLabels or removeLabels. 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..b78a514b1 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,30 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> - - - - - - - - - - - - - - - - - + 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..a7a527456 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/config.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. + +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 +entry.lockTimeout.title=Lock wait timeout +entry.lockTimeoutUnit.title=Timeout unit \ 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-lockTimeout.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-lockTimeout.html new file mode 100644 index 000000000..f6dbad972 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-lockTimeout.html @@ -0,0 +1,9 @@ +
+

+ Maximum time (in the specified time unit) to wait for a resource to become available. +

+

+ If the resource is not acquired within this time, the build will be cancelled from the queue. + Set to 0 (the default) to wait indefinitely. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-lockTimeoutUnit.html b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-lockTimeoutUnit.html new file mode 100644 index 000000000..b8fbddaba --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/RequiredResourcesProperty/help-lockTimeoutUnit.html @@ -0,0 +1,8 @@ +
+

+ The time unit for the lock wait timeout. +

+

+ Allowed values: SECONDS, MINUTES, HOURS. Default is MINUTES. +

+
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/UpdateLockStep/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.jelly new file mode 100644 index 000000000..1e2289fdf --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.jelly @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.properties b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.properties new file mode 100644 index 000000000..373ef8f85 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.properties @@ -0,0 +1,7 @@ +entry.resource.title=Resource +entry.createResource.title=Create resource if it does not exist +entry.deleteResource.title=Delete resource +entry.setLabels.title=Set labels +entry.addLabels.title=Add labels +entry.removeLabels.title=Remove labels +entry.setNote.title=Set note diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html new file mode 100644 index 000000000..aef6735a5 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html @@ -0,0 +1,13 @@ +
+

+ Adds labels to the resource without removing existing labels. + Multiple labels should be separated by whitespace. + Labels that already exist on the resource will not be duplicated. +

+

+ Cannot be used together with setLabels. +

+

+ Example: addLabels: 'newlabel1 newlabel2' +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html new file mode 100644 index 000000000..53d3032ed --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html @@ -0,0 +1,9 @@ +
+

+ When set to true, creates the resource if it does not already exist. + The newly created resource will be persistent (not ephemeral). +

+

+ Cannot be used together with deleteResource. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html new file mode 100644 index 000000000..9a41ca269 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html @@ -0,0 +1,9 @@ +
+

+ When set to true, deletes the specified resource. + The resource must not be locked, queued, or reserved. +

+

+ Cannot be used together with createResource or any label/note modification options. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html new file mode 100644 index 000000000..8b39973c1 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html @@ -0,0 +1,13 @@ +
+

+ Removes the specified labels from the resource. + Multiple labels should be separated by whitespace. + Labels that do not exist on the resource will be ignored. +

+

+ Cannot be used together with setLabels. +

+

+ Example: removeLabels: 'oldlabel1 oldlabel2' +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html new file mode 100644 index 000000000..fd4c7e967 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html @@ -0,0 +1,8 @@ +
+

+ The name of the resource to update. This parameter is required. +

+

+ The resource must already exist, unless createResource is set to true. +

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

+ Sets the labels of the resource, replacing any existing labels. + Multiple labels should be separated by whitespace. +

+

+ Cannot be used together with addLabels or removeLabels. +

+

+ Example: setLabels: 'label1 label2 label3' +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html new file mode 100644 index 000000000..978acefdf --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html @@ -0,0 +1,9 @@ +
+

+ Sets the note field of the resource, replacing any existing note. + This can be used to store additional information or comments about the resource. +

+

+ Example: setNote: 'Updated by build #123' +

+
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..5f3e6aaca 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,88 @@ + - - + + + + + - - -

${%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")} + +

+
+ + +