Repository: jenkinsci/docker-ssh-agent Branch: master Commit: b8c9b018fcc1 Files: 45 Total size: 134.2 KB Directory structure: gitextract_vtv8hsk6/ ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── release-drafter.yml │ └── updatecli.yaml ├── .gitignore ├── .gitmodules ├── CreateProfile.psm1 ├── Jenkinsfile ├── LICENSE ├── Makefile ├── README.md ├── alpine/ │ └── Dockerfile ├── build.ps1 ├── debian/ │ └── Dockerfile ├── docker-bake.hcl ├── jdk-download-url.sh ├── jdk-download.sh ├── setup-sshd ├── setup-sshd.ps1 ├── tests/ │ ├── golden/ │ │ └── expected_tags.txt │ ├── keys.bash │ ├── sshAgent.Tests.ps1 │ ├── tags.bats │ ├── test_helpers.bash │ ├── test_helpers.psm1 │ ├── tests.bats │ └── update-golden-file.sh ├── updatecli/ │ ├── updatecli.d/ │ │ ├── alpine.yaml │ │ ├── bats.yaml │ │ ├── debian.yaml │ │ ├── git-lfs.yml │ │ ├── git-windows.yml │ │ ├── jdk17.yaml │ │ ├── jdk21.yaml │ │ ├── jdk25.yaml │ │ ├── openssh.yml │ │ └── pester.yaml │ ├── values.github-action.yaml │ └── values.temurin.yaml └── windows/ ├── nanoserver/ │ └── Dockerfile └── windowsservercore/ └── Dockerfile ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ tests README.md .git .gitignore .gitattributes .DS_Store ================================================ FILE: .gitattributes ================================================ # Force checkout as Unix endline style text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ * @jenkinsci/team-docker-packaging ================================================ FILE: .github/FUNDING.yml ================================================ community_bridge: jenkins custom: ["https://jenkins.io/donate/#why-donate"] ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: docker directory: "/alpine" schedule: interval: weekly open-pull-requests-limit: 10 - package-ecosystem: docker directory: "/debian" schedule: interval: weekly open-pull-requests-limit: 10 - package-ecosystem: docker directory: "/windows/nanoserver" schedule: interval: weekly open-pull-requests-limit: 10 - package-ecosystem: docker directory: "/windows/windowsservercore" schedule: interval: weekly open-pull-requests-limit: 10 - package-ecosystem: "github-actions" target-branch: master directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "weekly" labels: - github-action - dependencies - skip-changelog ================================================ FILE: .github/release-drafter.yml ================================================ # https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc _extends: github:jenkinsci/.github:/.github/release-drafter.yml # Semantic versioning: https://semver.org/ version-template: $MAJOR.$MINOR.$PATCH tag-template: $NEXT_MINOR_VERSION name-template: $NEXT_MINOR_VERSION ================================================ FILE: .github/workflows/release-drafter.yml ================================================ # Automates creation of Release Drafts using Release Drafter # Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc name: Release Drafter (Changelog) on: push: branches: - master workflow_dispatch: jobs: update_release_draft: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into the default branch - uses: release-drafter/release-drafter@v7.3.0 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/updatecli.yaml ================================================ name: updatecli on: # Allow to be run manually workflow_dispatch: schedule: - cron: '0 1 * * *' # Once a day at 01:00am UTC push: pull_request: jobs: updatecli: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Install Updatecli in the runner uses: updatecli/updatecli-action@v3.2.0 - name: Run Updatecli in Dry Run mode run: updatecli diff --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml --values ./updatecli/values.temurin.yaml env: UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Updatecli in Apply mode if: github.ref == 'refs/heads/master' run: updatecli apply --config ./updatecli/updatecli.d --values ./updatecli/values.github-action.yaml --values ./updatecli/values.temurin.yaml env: UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ .DS_Store bats-core/ bats/ target/ /.vscode/ build-windows.yaml ================================================ FILE: .gitmodules ================================================ [submodule "tests/test_helper/bats-support"] path = tests/test_helper/bats-support url = https://github.com/bats-core/bats-support.git [submodule "tests/test_helper/bats-assert"] path = tests/test_helper/bats-assert url = https://github.com/bats-core/bats-assert.git ================================================ FILE: CreateProfile.psm1 ================================================ # Based on code developed by Josh Rickard (@MS_dministrator) and Thom Schumacher (@driberif) # Location: https://gist.github.com/crshnbrn66/7e81bf20408c05ddb2b4fdf4498477d8 #function to register a native method function Register-NativeMethod { [CmdletBinding()] [Alias()] [OutputType([int])] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string]$dll, # Param2 help description [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] [string] $methodSignature ) $script:nativeMethods += [PSCustomObject]@{ Dll = $dll; Signature = $methodSignature; } } #function to add native method function Add-NativeMethods { [CmdletBinding()] [Alias()] [OutputType([int])] Param($typeName = 'NativeMethods') $nativeMethodsCode = $script:nativeMethods | ForEach-Object { " [DllImport(`"$($_.Dll)`")] public static extern $($_.Signature); " } Add-Type @" using System; using System.Text; using System.Runtime.InteropServices; public static class $typeName { $nativeMethodsCode } "@ } #Main function to create the new user profile function New-UserWithProfile { [CmdletBinding()] [Alias()] [OutputType([int])] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string]$UserName, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] [string]$Description = '' ) Write-Verbose "Creating local user $Username"; try { net user $UserName /ADD /ACTIVE:YES /EXPIRES:NEVER /FULLNAME:"$Description" /PASSWORDCHG:NO /PASSWORDREQ:NO net localgroup Administrators /add $UserName } catch { Write-Error $_.Exception.Message; break; } $localUser = New-Object System.Security.Principal.NTAccount($UserName) $methodName = 'UserEnvCP' $script:nativeMethods = @(); if (-not ([System.Management.Automation.PSTypeName]$MethodName).Type) { Register-NativeMethod "userenv.dll" "int CreateProfile([MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,` [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,` [Out][MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath, uint cchProfilePath)"; Add-NativeMethods -typeName $MethodName; } $userSID = $localUser.Translate([System.Security.Principal.SecurityIdentifier]) $sb = New-Object System.Text.StringBuilder(260) $pathLen = $sb.Capacity Write-Verbose "Creating user profile for $UserName"; try { [UserEnvCP]::CreateProfile($userSID.Value, $UserName, $sb, $pathLen) | Out-Null; } catch { Write-Error $_.Exception.Message; break; } $profilePath = $sb.ToString() Write-Verbose "Profile created at $profilePath" if(-not (Test-Path (Join-Path $profilePath "NTUSER.DAT"))) { Copy-Item "C:\Users\Default\NTUSER.DAT" $profilePath } } ================================================ FILE: Jenkinsfile ================================================ final String cronExpr = env.BRANCH_IS_PRIMARY ? '@daily' : '' properties([ buildDiscarder(logRotator(numToKeepStr: '10')), disableConcurrentBuilds(abortPrevious: true), pipelineTriggers([cron(cronExpr)]), ]) def agentSelector(String imageType, retryCounter) { def platform switch (imageType) { // nanoserver-ltsc2019 and windowservercore-ltsc2019 case ~/.*2019/: platform = 'windows-2019' break // nanoserver-ltsc2022 and windowservercore-ltsc2022 case ~/.*2022/: platform = 'windows-2022' break // All other Windows images case ~/(nanoserver|windowsservercore).*/: platform = 'windows-2025' break // Linux default: // Need Docker and a LOT of memory for faster builds (due to multi archs) platform = 'docker-highmem' break } // Defined in https://github.com/jenkins-infra/pipeline-library/blob/master/vars/infra.groovy return infra.getBuildAgentLabel([ useContainerAgent: false, platform: platform, spotRetryCounter: retryCounter ]) } // Specify parallel stages def parallelStages = [failFast: false] [ 'linux', 'nanoserver-ltsc2019', 'nanoserver-ltsc2022', 'windowsservercore-ltsc2019', 'windowsservercore-ltsc2022' ].each { imageType -> parallelStages[imageType] = { withEnv([ "IMAGE_TYPE=${imageType}", "REGISTRY_ORG=${infra.isTrusted() ? 'jenkins' : 'jenkins4eval'}", ]) { int retryCounter = 0 retry(count: 2, conditions: [agent(), nonresumable()]) { // Use local variable to manage concurrency and increment BEFORE spinning up any agent final String resolvedAgentLabel = agentSelector(imageType, retryCounter) retryCounter++ node(resolvedAgentLabel) { timeout(time: 60, unit: 'MINUTES') { checkout scm if (imageType == "linux") { stage('Prepare Docker') { sh 'make docker-init' } } // This function is defined in the jenkins-infra/pipeline-library if (infra.isTrusted()) { // trusted.ci.jenkins.io builds (e.g. publication to DockerHub) stage('Deploy to DockerHub') { withEnv([ "ON_TAG=true", "VERSION=${env.TAG_NAME}", ]) { // This function is defined in the jenkins-infra/pipeline-library infra.withDockerCredentials { if (isUnix()) { sh 'make publish' } else { powershell '& ./build.ps1 build' powershell '& ./build.ps1 publish' } } } } } else { // ci.jenkins.io builds (e.g. no publication) stage('Build') { if (isUnix()) { sh 'make build' } else { powershell '& ./build.ps1 build' archiveArtifacts artifacts: 'build-windows.yaml', allowEmptyArchive: true } } stage('Test') { if (isUnix()) { sh 'make test' } else { powershell '& ./build.ps1 test' } junit(allowEmptyResults: true, keepLongStdio: true, testResults: 'target/**/junit-results.xml') } // If the tests are passing for Linux AMD64, then we can build all the CPU architectures if (isUnix()) { stage('Multi-Arch Build') { sh 'make every-build' } } } } } } } } } // Execute parallel stages parallel parallelStages // // vim: ft=groovy ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2015-2019 Jenkins project 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 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. ================================================ FILE: Makefile ================================================ ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) ## For Docker <=20.04 export DOCKER_BUILDKIT=1 ## For Docker <=20.04 export DOCKER_CLI_EXPERIMENTAL=enabled ## Required to have docker build output always printed on stdout export BUILDKIT_PROGRESS=plain current_arch := $(shell uname -m) export ARCH ?= $(shell case $(current_arch) in (x86_64) echo "amd64" ;; (i386) echo "386";; (aarch64|arm64) echo "arm64" ;; (armv6*) echo "arm/v6";; (armv7*) echo "arm/v7";; (s390*|riscv*|ppc64le) echo $(current_arch);; (*) echo "UNKNOWN-CPU";; esac) IMAGE_NAME:=jenkins4eval/ssh-agent # Set to the path of a specific test suite to restrict execution only to this # default is "all test suites in the "tests/" directory TEST_SUITES ?= $(CURDIR)/tests ##### Macros ## Check the presence of a CLI in the current PATH check_cli = type "$(1)" >/dev/null 2>&1 || { echo "Error: command '$(1)' required but not found. Exiting." ; exit 1 ; } ## Check if a given image exists in the current manifest docker-bake.hcl check_image = make --silent list | grep -w '$(1)' >/dev/null 2>&1 || { echo "Error: the image '$(1)' does not exist in manifest for the platform 'linux/$(ARCH)'. Please check the output of 'make list'. Exiting." ; exit 1 ; } ## Base "docker buildx base" command to be reused everywhere bake_base_cli := docker buildx bake --file docker-bake.hcl bake_cli := $(bake_base_cli) --load .PHONY: build .PHONY: test test-alpine test-debian check-reqs: ## Build requirements @$(call check_cli,bash) @$(call check_cli,git) @$(call check_cli,docker) @docker info | grep 'buildx:' >/dev/null 2>&1 || { echo "Error: Docker BuildX plugin required but not found. Exiting." ; exit 1 ; } ## Test requirements @$(call check_cli,curl) @$(call check_cli,jq) ## This function is specific to Jenkins infrastructure and isn't required in other contexts docker-init: check-reqs ifeq ($(CI),true) ifeq ($(wildcard /etc/buildkitd.toml),) echo 'WARNING: /etc/buildkitd.toml not found, using default configuration.' docker buildx create --use --bootstrap --driver docker-container else docker buildx create --use --bootstrap --driver docker-container --config /etc/buildkitd.toml endif else docker buildx create --use --bootstrap --driver docker-container endif docker run --rm --privileged multiarch/qemu-user-static --reset -p yes build: check-reqs @set -x; $(bake_cli) $(shell make --silent list) --set '*.platform=linux/$(ARCH)' build-%: @$(call check_image,$*) @set -x; $(bake_cli) '$*' --set '*.platform=linux/$(ARCH)' every-build: check-reqs @set -x; $(bake_base_cli) linux show: @$(bake_base_cli) --progress=quiet linux --print | jq tags: @make show | jq -r '.target[].tags[]' | LC_ALL=C sort list: check-reqs @set -x; make --silent show | jq -r '.target | path(.. | select(.platforms[] | contains("linux/$(ARCH)"))?) | add' bats: git clone --branch v1.13.0 https://github.com/bats-core/bats-core bats prepare-test: bats check-reqs git submodule update --init --recursive mkdir -p target publish: @set -x; $(bake_base_cli) linux --push ## Define bats options based on environment # common flags for all tests bats_flags := $(TEST_SUITES) # if DISABLE_PARALLEL_TESTS true, then disable parallel execution ifneq (true,$(DISABLE_PARALLEL_TESTS)) # If the GNU 'parallel' command line is absent, then disable parallel execution parallel_cli := $(shell command -v parallel 2>/dev/null) ifneq (,$(parallel_cli)) # If parallel execution is enabled, then set 2 tests per core available for the Docker Engine test-%: PARALLEL_JOBS ?= $(shell echo $$(( $(shell docker run --rm alpine grep -c processor /proc/cpuinfo) * 2))) test-%: bats_flags += --jobs $(PARALLEL_JOBS) endif endif test-%: prepare-test # Check that the image exists in the manifest @$(call check_image,$*) # Ensure that the image is built @make --silent build-$* ifeq ($(CI), true) # Execute the test harness and write result to a TAP file IMAGE=$* bats/bin/bats $(bats_flags) --formatter junit | tee target/junit-results-$*.xml else # Execute the test harness IMAGE=$* bats/bin/bats $(bats_flags) endif test: prepare-test @make --silent list | while read image; do make --silent "test-$${image}"; done ================================================ FILE: README.md ================================================ # Docker image for Jenkins agents connected over SSH [![Join the chat at https://gitter.im/jenkinsci/docker](https://badges.gitter.im/jenkinsci/docker.svg)](https://gitter.im/jenkinsci/docker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GitHub stars](https://img.shields.io/github/stars/jenkinsci/docker-ssh-agent?label=GitHub%20stars)](https://github.com/jenkinsci/docker-ssh-agent) [![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/ssh-agent.svg)](https://hub.docker.com/r/jenkins/ssh-agent/) [![GitHub release](https://img.shields.io/github/release/jenkinsci/docker-ssh-agent.svg?label=changelog)](https://github.com/jenkinsci/docker-ssh-agent/releases) A [Jenkins](https://jenkins.io) agent image which allows using SSH to establish the connection. It can be used together with the [SSH Build Agents plugin](https://plugins.jenkins.io/ssh-slaves) or other similar plugins. See [Jenkins Distributed builds](https://wiki.jenkins-ci.org/display/JENKINS/Distributed+builds) for more info. ## Running ### Running with the SSH Build Agents plugin To run a Docker container ```bash docker run -d --rm --name=agent --publish 2200:22 -e "JENKINS_AGENT_SSH_PUBKEY=" jenkins/ssh-agent ``` - `-d`: To start a container in detached mode, use the `-d` option. Containers started in detached mode exit when the root process used to run the container exits, unless you also specify the --rm option. - `--rm`: If you use -d with --rm, the container is removed when it exits or when the daemon exits, whichever happens first. - `--name`: Assigns a name to the container. If you do not specify a name, Docker generates a random name. - `--publish 2200:22`: Publishes the host port 2200 to the agent container port 22 (SSH) to allow connection from the host with `ssh jenkins@localhost -p 2200` Please note none of these options are mandatory, they are just examples. You will then be able to connect this agent using the [SSH Build Agents plugin](https://plugins.jenkins.io/ssh-slaves) as "jenkins" with the matching private key. When using the Linux image, you have to set the value of the `Remote root directory` to `/home/jenkins/agent` in the agent configuration UI. ![Remote root directory with a Linux agent](docs/ssh-plugin-remote-root-directory-linux.png "Remote root directory with a Linux agent") When using the Windows image, you have to set the value of the `Remote root directory` to `C:/Users/jenkins/Work` in the agent configuration UI. ![Remote root directory with a Windows agent](docs/ssh-plugin-remote-root-directory-windows.png "Remote root directory with a Windows agent") If you intend to use another directory than `/home/jenkins/agent` under Linux or `C:/Users/jenkins/Work` under Windows, don't forget to add it as a data volume. ```bash docker run -v docker-volume-for-jenkins-ssh-agent:/home/jenkins/agent:rw jenkins/ssh-agent "" ``` ### How to use this image with Docker Plugin To use this image with [Docker Plugin](https://plugins.jenkins.io/docker-plugin), you need to pass the public SSH key using environment variable `JENKINS_AGENT_SSH_PUBKEY` and not as a startup argument. In _Environment_ field of the Docker Template (advanced section), just add: JENKINS_AGENT_SSH_PUBKEY= Don't put quotes around the public key. Please note that you have to set the value of the `Remote File System Root` to `/home/jenkins/agent` in the Docker Agent Template configuration UI. ![Remote File System Root](docs/docker-plugin-remote-filesystem-root.png "Remote File System Root directory") If you intend to use another directory than `/home/jenkins/agent`, don't forget to add it as a data volume. ![Docker Volumes mounts](docs/docker-plugin-volumes.png "Docker Volumes mounts") You should be all set. ## Extending the image Should you need to extend the image, you could use something along those lines: ```Dockerfile FROM jenkins/ssh-agent:debian-jdk17 as ssh-agent # [...] COPY --chown=jenkins mykey "${JENKINS_AGENT_HOME}"/.ssh/mykey # [...] ``` ## Configurations The image has several supported configurations, which can be accessed via the following tags: `${IMAGE_VERSION}` can be found on the [releases](https://github.com/jenkinsci/docker-ssh-agent/releases) page. List of tags can be consulted at https://github.com/jenkinsci/docker-ssh-agent/tree/master/tests/golden ## Host keys Host keys are generated with `ssh-keygen -A` in `setup-httpd`. Host keys reside inside the container. When the container is recreated, different host keys are also generated. Jenkins master may need to re-trust new host keys. We can preserve host keys in mounted volume. Steps: 1. Copy host keys from the mounted volume to /etc/ssh/ 2. Run ssh-keygen -A to generate host keys 3. Copy host keys from /etc/ssh/ to the mounted volume 4. Run setup-sshd Entry point example: ``` #!/bin/bash # /mnt/agent is the mounted volume mkdir -p /mnt/agent/host_keys/ cp -u /mnt/agent/host_keys/ssh_host*_key* /etc/ssh/ ssh-keygen -A cp -u /etc/ssh/ssh_host*_key* /mnt/agent/host_keys/ setup-ssd "$@" ``` ## Building instructions ### Pre-requisites Should you want to build this image on your machine (before submitting a pull request for example), please have a look at the pre-requisites: * A GNU/Linux machine with [Docker](https://docs.docker.com/engine/install/), a macOS machine with [Docker Desktop](https://docs.docker.com/desktop/install/mac-install/), or a Windows machine with [Docker for Windows](https://docs.docker.com/docker-for-windows/) installed * Docker BuildX plugin [installed](https://github.com/docker/buildx#installing) on older versions of Docker (from `19.03`). Docker Buildx is included in recent versions of Docker Desktop for Windows, macOS, and Linux. Docker Linux packages also include Docker Buildx when installed using the DEB or RPM packages. * [GNU Make](https://www.gnu.org/software/make/) [installed](https://command-not-found.com/make) * jq [installed](https://command-not-found.com/jq) * yq [installed](https://github.com/mikefarah/yq) (for Windows) * [GNU Bash](https://www.gnu.org/software/bash/) [installed](https://command-not-found.com/bash) * git [installed](https://command-not-found.com/git) * curl [installed](https://command-not-found.com/curl) ### Building #### Target images If you want to see the target images that will be built, you can issue the following command: ```bash make list alpine_jdk11 alpine_jdk17 debian_jdk11 debian_jdk17 ``` #### Building a specific image If you want to build a specific image, you can issue the following command: ```bash make build-_ ``` That would give for JDK 17 on Alpine Linux: ```bash make build-alpine_jdk17 ``` #### Building images supported by your current architecture Then, you can build the images supported by your current architecture by running: ```bash make build ``` #### Testing all images If you want to test these images, you can run: ```bash make test ``` #### Testing a specific image If you want to test a specific image, you can run: ```bash make test-_ ``` That would give for JDK 17 on Alpine Linux: ```bash make test-alpine_jdk17 ``` #### Building all images You can build all images (even those unsupported by your current architecture) by running: ```bash make every-build ``` #### Other `make` targets `show` gives us a detailed view of the images that will be built, with the tags, platforms, and Dockerfiles. ```bash make show { "group": { "default": { "targets": [ "alpine_jdk17", "alpine_jdk11", "debian_jdk11", "debian_jdk17", ] } }, "target": { "alpine_jdk11": { "context": ".", "dockerfile": "alpine/Dockerfile", "tags": [ "docker.io/jenkins/ssh-agent:alpine-jdk11", "docker.io/jenkins/ssh-agent:latest-alpine-jdk11" ], "platforms": [ "linux/amd64" ], "output": [ "type=docker" ] }, [...] ``` `bats` is a dependency target. It will update the [`bats` submodule](https://github.com/bats-core/bats-core) and run the tests. ```bash make bats make: 'bats' is up to date. ``` `publish` allows the publication of all images targeted by 'linux' to a registry. `docker-init` is dedicated to Jenkins infrastructure for initializing docker and isn't required in other contexts. ### Building and testing on Windows #### Building all images Run `.\build.ps1` to launch the build of the images corresponding to the "windows" target of docker-bake.hcl. Internally, the first time you'll run this script and if there is no build-windows.yaml file in your repository, it will use a combination of `docker buildx bake` and `yq` to generate a build-windows.yaml docker compose file containing all Windows image definitions from docker-bake.hcl. Then it will run `docker compose` on this file to build these images. You can modify this docker compose file as you want, then rerun `.\build.ps1`. It won't regenerate the docker compose file from docker-bake.hcl unless you add the `-OverwriteDockerComposeFile` build.ps1 parameter: `.\build.ps1 -OverwriteDockerComposeFile`. Note: you can generate this docker compose file from docker-bake.hcl yourself with the following command (require `docker buildx` and `yq`): ```console # - Use docker buildx bake to output image definitions from the "windows" bake target # - Convert with yq to the format expected by docker compose # - Store the result in the docker compose file $ docker buildx bake --progress=plain --file=docker-bake.hcl windows --print ` | yq --prettyPrint '.target[] | del(.output) | {(. | key): {\"image\": .tags[0], \"build\": .}}' | yq '{\"services\": .}' ` | Out-File -FilePath build-windows.yaml ``` Note that you don't need build.ps1 to build (or to publish) your images from this docker compose file, you can use `docker compose --file=build-windows.yaml build`. #### Testing all images Run `.\build.ps1 test` if you also want to run the tests harness suit. Run `.\build.ps1 test -TestsDebug 'debug'` to also get commands & stderr of tests, displayed on top of them. You can set it to `'verbose'` to also get stdout of every test command. Note that instead of passing `-TestsDebug` parameter to build.ps1, you can set the $env:TESTS_DEBUG environment variable to the desired value. Also note that contrary to the Linux part, you have to build the images before testing them. #### Dry run Add the `-DryRun` parameter to print out any build, publish or tests commands instead of executing them: `.\build.ps1 test -DryRun` #### Building and testing a specific image You can build (and test) only one image type by setting `-ImageType` to a combination of Windows flavors ("nanoserver" & "windowsservercore") and Windows versions ("ltsc2019", "ltsc2022"). Ex: `.\build.ps1 -ImageType 'nanoserver-ltsc2019'` ## Changelog See [GitHub Releases](https://github.com/jenkinsci/docker-ssh-agent/releases/latest). Note that the changelogs and release tags were introduced in Dec 2019, and there are no entries for previous releases. Please consult with the commit history if needed. ================================================ FILE: alpine/Dockerfile ================================================ # MIT License # # Copyright (c) 2019-2022 Fabio Kruger and other 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 # 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. ARG JAVA_VERSION=17.0.19_10 ARG ALPINE_TAG=3.23.4 FROM alpine:"${ALPINE_TAG}" AS jre-build SHELL ["/bin/ash", "-eo", "pipefail", "-c"] # This Build ARG is populated by Docker # Ref. https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope ARG TARGETPLATFORM COPY jdk-download-url.sh /usr/bin/local/jdk-download-url.sh COPY jdk-download.sh /usr/bin/local/jdk-download.sh ARG JAVA_VERSION=17.0.19_10 # hadolint ignore=DL3018 RUN apk add --no-cache \ ca-certificates \ jq \ curl \ && /usr/bin/local/jdk-download.sh alpine ENV PATH="/opt/jdk-${JAVA_VERSION}/bin:${PATH}" RUN case "$(jlink --version 2>&1)" in \ "17."*) set -- "--compress=2" --add-modules ALL-MODULE-PATH ;; \ "21."*) set -- "--compress=zip-6" --add-modules ALL-MODULE-PATH ;; \ # JDK 25 switches to a minimal module set for smaller images "25"*) set -- "--compress=zip-6" --add-modules "java.base,java.logging,java.xml,java.management,java.net.http,jdk.crypto.ec" ;; \ *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \ esac; \ jlink \ --strip-java-debug-attributes \ "$@" \ --no-man-pages \ --no-header-files \ --output /javaruntime # Downloading git-lfs from this intermediate stage as there is no wget/curl in the final one ARG GIT_LFS_VERSION=3.7.1 RUN arch=$(uname -m | sed -e 's/x86_64/amd64/g' -e 's/aarch64/arm64/g') \ && curl -L -s -o git-lfs.tgz "https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/git-lfs-linux-${arch}-v${GIT_LFS_VERSION}.tar.gz" FROM alpine:"${ALPINE_TAG}" AS build ARG user=jenkins ARG group=jenkins ARG uid=1000 ARG gid=1000 ARG JENKINS_AGENT_HOME=/home/${user} ENV JENKINS_AGENT_HOME=${JENKINS_AGENT_HOME} ARG AGENT_WORKDIR="${JENKINS_AGENT_HOME}"/agent # Persist agent workdir path through an environment variable for people extending the image ENV AGENT_WORKDIR=${AGENT_WORKDIR} RUN addgroup -g "${gid}" "${group}" \ # Set the home directory (h), set user and group id (u, G), set the shell, don't ask for password (D) && adduser -h "${JENKINS_AGENT_HOME}" -u "${uid}" -G "${group}" -s /bin/bash -D "${user}" \ # Unblock user && passwd -u "${user}" \ # Prepare subdirectories && mkdir -p "${JENKINS_AGENT_HOME}/.ssh/" "${JENKINS_AGENT_HOME}/.jenkins/" "${AGENT_WORKDIR}" \ && chown -R "${uid}":"${gid}" "${JENKINS_AGENT_HOME}" "${AGENT_WORKDIR}" RUN apk add --no-cache \ bash \ git \ less \ musl-locales \ netcat-openbsd \ openssh \ patch \ # Cleanup SSH host keys if any && rm -f /etc/ssh/ssh_host*_key* # Retrieve git-lfs from the build stage and install it COPY --from=jre-build git-lfs.tgz git-lfs.tgz RUN tar xzf git-lfs.tgz \ && bash git-lfs-*/install.sh \ && rm -rf git-lfs* # setup SSH server RUN sed -i /etc/ssh/sshd_config \ -e 's/#PermitRootLogin.*/PermitRootLogin no/' \ -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \ -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \ -e 's/#LogLevel.*/LogLevel INFO/' \ -e 's/#PermitUserEnvironment.*/PermitUserEnvironment yes/' \ && mkdir /var/run/sshd # Install JDK ENV JAVA_HOME=/opt/java/openjdk COPY --from=jre-build /javaruntime "$JAVA_HOME" ENV PATH="${JAVA_HOME}/bin:${PATH}" # VOLUME directive must happen after setting up permissions and content VOLUME "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}"/.jenkins "/tmp" "/run" "/var/run" WORKDIR "${JENKINS_AGENT_HOME}" # Alpine's ssh doesn't use $PATH defined in /etc/environment, so we define `$PATH` in `~/.ssh/environment` # The file path has been created earlier in the file by `mkdir -p` and we also have configured sshd so that it will # allow environment variables to be sourced (see `sed` command related to `PermitUserEnvironment`) RUN echo "PATH=${PATH}" >> ${JENKINS_AGENT_HOME}/.ssh/environment COPY setup-sshd /usr/local/bin/setup-sshd EXPOSE 22 ENTRYPOINT ["setup-sshd"] LABEL \ org.opencontainers.image.vendor="Jenkins project" \ org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" \ org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" \ org.opencontainers.image.url="https://www.jenkins.io/" \ org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" \ org.opencontainers.image.licenses="MIT" ================================================ FILE: build.ps1 ================================================ [CmdletBinding()] Param( [Parameter(Position=1)] # Default build.ps1 target [String] $Target = 'build', # Image version [String] $VersionTag = '0.0.1', # Windows flavor and windows version to build [String] $ImageType = 'nanoserver-ltsc2019', # Generate a docker compose file even if it already exists [switch] $OverwriteDockerComposeFile = $false, # Print the build and publish command instead of executing them if set [switch] $DryRun = $false, # Pester version to install and use for tests [String] $PesterVersion = '5.7.1', # Output debug info for tests: 'empty' (no additional test output), 'debug' (test cmd & stderr outputed), 'verbose' (test cmd, stderr, stdout outputed) [String] $TestsDebug = '' ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Disable Progress bar for faster downloads $dockerComposeFile = 'build-windows.yaml' $baseDockerCmd = 'docker-compose --file={0}' -f $dockerComposeFile $baseDockerBuildCmd = '{0} build --pull' -f $baseDockerCmd $Repository = 'ssh-agent' $Organisation = 'jenkins' if(![String]::IsNullOrWhiteSpace($env:TESTS_DEBUG)) { $TestsDebug = $env:TESTS_DEBUG } $env:TESTS_DEBUG = $TestsDebug if(![String]::IsNullOrWhiteSpace($env:DOCKERHUB_REPO)) { $Repository = $env:DOCKERHUB_REPO } if(![String]::IsNullOrWhiteSpace($env:DOCKERHUB_ORGANISATION)) { $Organisation = $env:DOCKERHUB_ORGANISATION } if(![String]::IsNullOrWhiteSpace($env:VERSION)) { $VersionTag = $env:VERSION } if(![String]::IsNullOrWhiteSpace($env:IMAGE_TYPE)) { $ImageType = $env:IMAGE_TYPE } # Ensure constant env vars used in the docker compose file are defined $env:DOCKERHUB_ORGANISATION = "$Organisation" $env:DOCKERHUB_REPO = "$Repository" $env:VERSION = "$VersionTag" # Check for required commands Function Test-CommandExists { # From https://devblogs.microsoft.com/scripting/use-a-powershell-function-to-see-if-a-command-exists/ Param ( [String] $command ) $oldPreference = $ErrorActionPreference $ErrorActionPreference = 'stop' try { # Special case to test "docker buildx" if ($command.Contains(" ")) { Invoke-Expression $command | Out-Null Write-Debug "$command exists" } else { if(Get-Command $command){ Write-Debug "$command exists" } } } Catch { "$command does not exist" } Finally { $ErrorActionPreference=$oldPreference } } function Test-Image { param ( $ImageNameAndJavaVersion ) # Ex: docker.io/jenkins/ssh-agent:windowsservercore-ltsc2019-jdk21|21.0.3_9 $items = $ImageNameAndJavaVersion.Split('|') $imageName = $items[0] -replace 'docker.io/', '' $javaVersion = $items[1] $imageNameItems = $imageName.Split(':') $imageTag = $imageNameItems[1] Write-Host "= TEST: Testing ${ImageName} image" $env:IMAGE_NAME = $ImageName $env:JAVA_VERSION = "$javaVersion" $targetPath = '.\target\{0}' -f $imageTag if(Test-Path $targetPath) { Remove-Item -Recurse -Force $targetPath } New-Item -Path $targetPath -Type Directory | Out-Null $configuration.TestResult.OutputPath = '{0}\junit-results.xml' -f $targetPath $TestResults = Invoke-Pester -Configuration $configuration $failed = $false if ($TestResults.FailedCount -gt 0) { Write-Host "There were $($TestResults.FailedCount) failed tests out of $($TestResults.TotalCount) in ${ImageName}" $failed = $true } else { Write-Host "There were $($TestResults.PassedCount) passed tests in ${ImageName}" } Remove-Item env:\IMAGE_NAME Remove-Item env:\JAVA_VERSION return $failed } function Initialize-DockerComposeFile { $baseDockerBakeCmd = 'docker buildx bake --progress=plain --file=docker-bake.hcl' $items = $ImageType.Split('-') $windowsFlavor = $items[0] $windowsVersion = $items[1] # Override the list of Windows versions taken defined in docker-bake.hcl by the version from image type $env:WINDOWS_VERSION_OVERRIDE = $windowsVersion # Retrieve the targets from docker buildx bake --print output # Remove the 'output' section (unsupported by docker compose) # For each target name as service key, return a map consisting of: # - 'image' set to the first tag value # - 'build' set to the content of the bake target $yqMainQuery = '''.target[]' + ` ' | del(.output)' + ` ' | {(. | key): {\"image\": .tags[0], \"build\": .}}''' # Encapsulate under a top level 'services' map $yqServicesQuery = '''{\"services\": .}''' # - Use docker buildx bake to output image definitions from the "" bake target # - Convert with yq to the format expected by docker compose # - Store the result in the docker compose file $generateDockerComposeFileCmd = ' {0} {1} --print' -f $baseDockerBakeCmd, $windowsFlavor + ` ' | yq --prettyPrint {0} | yq {1}' -f $yqMainQuery, $yqServicesQuery + ` ' | Out-File -FilePath {0}' -f $dockerComposeFile Write-Host "= PREPARE: Docker compose file generation command`n$generateDockerComposeFileCmd" Invoke-Expression $generateDockerComposeFileCmd # Remove override Remove-Item env:\WINDOWS_VERSION_OVERRIDE } Test-CommandExists 'docker' Test-CommandExists 'docker-compose' Test-CommandExists 'docker buildx' Test-CommandExists 'yq' # Generate the docker compose file if it doesn't exists or if the parameter OverwriteDockerComposeFile is set if ((Test-Path $dockerComposeFile) -and -not $OverwriteDockerComposeFile) { Write-Host "= PREPARE: The docker compose file '$dockerComposeFile' containing the image definitions already exists." } else { Write-Host "= PREPARE: Initialize the docker compose file '$dockerComposeFile' containing the image definitions." Initialize-DockerComposeFile } Write-Host '= PREPARE: List of images and tags to be processed:' Invoke-Expression "$baseDockerCmd config" if ($target -eq 'build') { Write-Host '= BUILD: Building all images...' switch ($DryRun) { $true { Write-Host "(dry-run) $baseDockerBuildCmd" } $false { Invoke-Expression $baseDockerBuildCmd } } Write-Host '= BUILD: Finished building all images.' if($lastExitCode -ne 0) { exit $lastExitCode } } if($target -eq 'test') { if ($DryRun) { Write-Host '= TEST: (dry-run) test harness' } else { Write-Host '= TEST: Starting test harness' $mod = Get-InstalledModule -Name Pester -MinimumVersion $PesterVersion -MaximumVersion $PesterVersion -ErrorAction SilentlyContinue if($null -eq $mod) { Write-Host "= TEST: Pester $PesterVersion not found: installing..." Install-Module -Force -Name Pester -MaximumVersion $PesterVersion -Scope CurrentUser } Import-Module Pester Write-Host '= TEST: Setting up Pester environment...' $configuration = [PesterConfiguration]::Default $configuration.Run.PassThru = $true $configuration.Run.Path = '.\tests' $configuration.Run.Exit = $true $configuration.TestResult.Enabled = $true $configuration.TestResult.OutputFormat = 'JUnitXml' $configuration.Output.Verbosity = 'Diagnostic' $configuration.CodeCoverage.Enabled = $false Write-Host '= TEST: Testing all images...' # Only fail the run afterwards in case of any test failures $testFailed = $false $imageDefinitions = Invoke-Expression "$baseDockerCmd config" | yq --unwrapScalar --output-format json '.services' | ConvertFrom-Json foreach ($imageDefinition in $imageDefinitions.PSObject.Properties) { $testFailed = $testFailed -or (Test-Image ('{0}|{1}' -f $imageDefinition.Value.image, $imageDefinition.Value.build.args.JAVA_VERSION)) } # Fail if any test failures if($testFailed -ne $false) { Write-Error '= TEST: stage failed!' exit 1 } else { Write-Host '= TEST: stage passed!' } } } if($target -eq 'publish') { Write-Host '= PUBLISH: push all images and tags' switch($DryRun) { $true { Write-Host "(dry-run) $baseDockerCmd push" } $false { Invoke-Expression "$baseDockerCmd push" } } # Fail if any issues when publising the docker images if($lastExitCode -ne 0) { Write-Error '= PUBLISH: failed!' exit 1 } } if($lastExitCode -ne 0) { Write-Error 'Build failed!' } else { Write-Host 'Build finished successfully' } exit $lastExitCode ================================================ FILE: debian/Dockerfile ================================================ # The MIT License # # Copyright (c) 2015-2024, CloudBees, Inc. and other Jenkins 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 # 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. ARG DEBIAN_RELEASE=trixie-20260505 FROM debian:"${DEBIAN_RELEASE}"-slim AS jre-build SHELL ["/bin/bash", "-e", "-u", "-o", "pipefail", "-c"] # This Build ARG is populated by Docker # Ref. https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope ARG TARGETPLATFORM COPY jdk-download-url.sh /usr/bin/local/jdk-download-url.sh COPY jdk-download.sh /usr/bin/local/jdk-download.sh ARG JAVA_VERSION=17.0.19_10 # hadolint ignore=DL3008 RUN set -x; apt-get update \ && apt-get install --no-install-recommends -y \ ca-certificates \ jq \ curl \ && /usr/bin/local/jdk-download.sh ENV PATH="/opt/jdk-${JAVA_VERSION}/bin:${PATH}" # Generate smaller java runtime without unneeded files # for now we include the full module path to maintain compatibility # while still saving space (approx 200mb from the full distribution) RUN case "$(jlink --version 2>&1)" in \ "17."*) set -- "--compress=2" --add-modules ALL-MODULE-PATH ;; \ "21."*) set -- "--compress=zip-6" --add-modules ALL-MODULE-PATH ;; \ # JDK 25 switches to a minimal module set for smaller images "25"*) set -- "--compress=zip-6" --add-modules "java.base,java.logging,java.xml,java.management,java.net.http,jdk.crypto.ec" ;; \ *) echo "ERROR: unmanaged jlink version pattern" && exit 1 ;; \ esac; \ jlink \ --strip-java-debug-attributes \ "$@" \ --no-man-pages \ --no-header-files \ --output /javaruntime # Downloading git-lfs from this intermediate stage as there is no wget/curl in the final one ARG GIT_LFS_VERSION=3.7.1 RUN arch=$(uname -m | sed -e 's/x86_64/amd64/g' -e 's/aarch64/arm64/g') \ && curl -L -s -o git-lfs.tgz "https://github.com/git-lfs/git-lfs/releases/download/v${GIT_LFS_VERSION}/git-lfs-linux-${arch}-v${GIT_LFS_VERSION}.tar.gz" FROM debian:"${DEBIAN_RELEASE}" ARG user=jenkins ARG group=jenkins ARG uid=1000 ARG gid=1000 ARG JENKINS_AGENT_HOME=/home/${user} ENV JENKINS_AGENT_HOME=${JENKINS_AGENT_HOME} ARG AGENT_WORKDIR="${JENKINS_AGENT_HOME}/agent" # Persist agent workdir path through an environment variable for people extending the image ENV AGENT_WORKDIR=${AGENT_WORKDIR} RUN groupadd -g ${gid} ${group} \ && useradd -d "${JENKINS_AGENT_HOME}" -u "${uid}" -g "${gid}" -m -s /bin/bash "${user}" \ # Prepare subdirectories && mkdir -p "${JENKINS_AGENT_HOME}/.ssh/" "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}/.jenkins" \ # Make sure that user 'jenkins' own these directories and their content && chown -R "${uid}":"${gid}" "${JENKINS_AGENT_HOME}" "${AGENT_WORKDIR}" RUN apt-get update \ && apt-get install --no-install-recommends -y \ ca-certificates \ git \ less \ netcat-traditional \ openssh-server \ patch \ # Cleanup APT cache && rm -rf /var/lib/apt/lists/* \ # Cleanup SSH host keys if any && rm -f /etc/ssh/ssh_host*_key* # Retrieve git-lfs from the build stage and install it COPY --from=jre-build git-lfs.tgz git-lfs.tgz RUN tar xzf git-lfs.tgz \ && bash git-lfs-*/install.sh \ && rm -rf git-lfs* # setup SSH server RUN sed -i /etc/ssh/sshd_config \ -e 's/#PermitRootLogin.*/PermitRootLogin no/' \ -e 's/#RSAAuthentication.*/RSAAuthentication yes/' \ -e 's/#PasswordAuthentication.*/PasswordAuthentication no/' \ -e 's/#SyslogFacility.*/SyslogFacility AUTH/' \ -e 's/#LogLevel.*/LogLevel INFO/' && \ mkdir -p /var/run/sshd && \ sed -i /etc/pam.d/sshd \ -e 's/\(session\s*\)required\(\s*pam_loginuid.so\)/\1optional\2/' \ -e '/pam_motd/s/^/#/' # VOLUME directive must happen after setting up permissions and content VOLUME "${AGENT_WORKDIR}" "${JENKINS_AGENT_HOME}"/.jenkins "/tmp" "/run" "/var/run" WORKDIR "${JENKINS_AGENT_HOME}" ENV LANG='C.UTF-8' LC_ALL='C.UTF-8' ENV JAVA_HOME=/opt/java/openjdk ENV PATH="${JAVA_HOME}/bin:${PATH}" COPY --from=jre-build /javaruntime $JAVA_HOME RUN echo "PATH=${PATH}" >> /etc/environment COPY setup-sshd /usr/local/bin/setup-sshd EXPOSE 22 ENTRYPOINT ["setup-sshd"] LABEL \ org.opencontainers.image.vendor="Jenkins project" \ org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" \ org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" \ org.opencontainers.image.url="https://www.jenkins.io/" \ org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" \ org.opencontainers.image.licenses="MIT" ================================================ FILE: docker-bake.hcl ================================================ ## Variables variable "jdks_to_build" { default = [17, 21, 25] } variable "default_jdk" { default = 21 } variable "REGISTRY" { default = "docker.io" } variable "JENKINS_REPO" { default = "jenkins/ssh-agent" } variable "ON_TAG" { default = "false" } variable "VERSION" { default = "" } variable "ALPINE_FULL_TAG" { default = "3.23.4" } variable "ALPINE_SHORT_TAG" { default = regex_replace(ALPINE_FULL_TAG, "\\.\\d+$", "") } variable "JAVA17_VERSION" { default = "17.0.19_10" } variable "JAVA21_VERSION" { default = "21.0.11_10" } variable "JAVA25_VERSION" { default = "25.0.3_9" } variable "DEBIAN_RELEASE" { default = "trixie-20260505" } # Set this value to a specific Windows version to override Windows versions to build returned by windowsversions function variable "WINDOWS_VERSION_OVERRIDE" { default = "" } ## Targets target "alpine" { matrix = { jdk = jdks_to_build } name = "alpine_${jdk}" dockerfile = "alpine/Dockerfile" context = "." args = { ALPINE_TAG = ALPINE_FULL_TAG JAVA_VERSION = "${javaversion(jdk)}" } tags = [ # If there is a tag, add versioned tags suffixed by the jdk equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-alpine-jdk${jdk}" : "", equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-alpine${ALPINE_SHORT_TAG}-jdk${jdk}" : "", # If the jdk is the default one, add Alpine short tags is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:alpine" : "", is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:alpine${ALPINE_SHORT_TAG}" : "", is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:latest-alpine${ALPINE_SHORT_TAG}" : "", "${REGISTRY}/${JENKINS_REPO}:alpine-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:latest-alpine-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:alpine${ALPINE_SHORT_TAG}-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:latest-alpine${ALPINE_SHORT_TAG}-jdk${jdk}", ] platforms = alpine_platforms(jdk) } target "debian" { matrix = { jdk = jdks_to_build } name = "debian_${jdk}" dockerfile = "debian/Dockerfile" context = "." args = { DEBIAN_RELEASE = DEBIAN_RELEASE JAVA_VERSION = "${javaversion(jdk)}" } tags = [ # If there is a tag, add versioned tag suffixed by the jdk equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-jdk${jdk}" : "", # If there is a tag and if the jdk is the default one, add versioned short tag equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}" : "") : "", # If the jdk is the default one, add latest short tag is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:latest" : "", "${REGISTRY}/${JENKINS_REPO}:trixie-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:debian-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:latest-trixie-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:latest-debian-jdk${jdk}", "${REGISTRY}/${JENKINS_REPO}:latest-jdk${jdk}", ] platforms = debian_platforms(jdk) } target "nanoserver" { matrix = { jdk = jdks_to_build windows_version = windowsversions("nanoserver") } name = "nanoserver-${windows_version}_jdk${jdk}" dockerfile = "windows/nanoserver/Dockerfile" context = "." args = { JAVA_HOME = "C:/openjdk-${jdk}" JAVA_VERSION = "${replace(javaversion(jdk), "_", "+")}" TOOLS_WINDOWS_VERSION = "${toolsversion(windows_version)}" WINDOWS_VERSION_TAG = windows_version } tags = [ # If there is a tag, add versioned tag suffixed by the jdk equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-nanoserver-${windows_version}-jdk${jdk}" : "", # If there is a tag and if the jdk is the default one, add versioned and short tags equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-nanoserver-${windows_version}" : "") : "", equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:nanoserver-${windows_version}" : "") : "", "${REGISTRY}/${JENKINS_REPO}:nanoserver-${windows_version}-jdk${jdk}", ] platforms = ["windows/amd64"] } target "windowsservercore" { matrix = { jdk = jdks_to_build windows_version = windowsversions("windowsservercore") } name = "windowsservercore-${windows_version}_jdk${jdk}" dockerfile = "windows/windowsservercore/Dockerfile" context = "." args = { JAVA_HOME = "C:/openjdk-${jdk}" JAVA_VERSION = "${replace(javaversion(jdk), "_", "+")}" TOOLS_WINDOWS_VERSION = "${toolsversion(windows_version)}" WINDOWS_VERSION_TAG = windows_version } tags = [ # If there is a tag, add versioned tag suffixed by the jdk equal(ON_TAG, "true") ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-windowsservercore-${windows_version}-jdk${jdk}" : "", # If there is a tag and if the jdk is the default one, add versioned and short tags equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:${VERSION}-windowsservercore-${windows_version}" : "") : "", equal(ON_TAG, "true") ? (is_default_jdk(jdk) ? "${REGISTRY}/${JENKINS_REPO}:windowsservercore-${windows_version}" : "") : "", "${REGISTRY}/${JENKINS_REPO}:windowsservercore-${windows_version}-jdk${jdk}", ] platforms = ["windows/amd64"] } ## Groups group "linux" { targets = [ "alpine", "debian", ] } group "windows" { targets = [ "nanoserver", "windowsservercore" ] } group "linux-arm64" { targets = [ "debian", "alpine_jdk21", ] } group "linux-s390x" { targets = [ "debian_jdk21" ] } group "linux-ppc64le" { targets = [ "debian" ] } ## Common functions # Return "true" if the jdk passed as parameter is the same as the default jdk, "false" otherwise function "is_default_jdk" { params = [jdk] result = equal(default_jdk, jdk) ? "true" : "false" } # Return the complete Java version corresponding to the jdk passed as parameter function "javaversion" { params = [jdk] result = (equal(17, jdk) ? "${JAVA17_VERSION}" : equal(21, jdk) ? "${JAVA21_VERSION}" : "${JAVA25_VERSION}") } ## Specific functions # Return an array of Alpine platforms to use depending on the jdk passed as parameter function "alpine_platforms" { params = [jdk] result = (equal(17, jdk) ? ["linux/amd64"] : ["linux/amd64", "linux/arm64"]) } # Return an array of Debian platforms to use depending on the jdk passed as parameter function "debian_platforms" { params = [jdk] result = (equal(17, jdk) ? ["linux/amd64", "linux/arm64", "linux/ppc64le"] : ["linux/amd64", "linux/arm64", "linux/ppc64le", "linux/s390x", "linux/riscv64"]) } # Return array of Windows version(s) to build # Can be overriden by setting WINDOWS_VERSION_OVERRIDE to a specific Windows version # Ex: WINDOWS_VERSION_OVERRIDE=ltsc2025 docker buildx bake windows function "windowsversions" { params = [flavor] result = (notequal(WINDOWS_VERSION_OVERRIDE, "") ? [WINDOWS_VERSION_OVERRIDE] : ["ltsc2019", "ltsc2022"]) } # Return the Windows version to use as base image for the Windows version passed as parameter # There is no mcr.microsoft.com/powershell ltsc2019 base image, using a "1809" instead function "toolsversion" { params = [version] result = (equal("ltsc2019", version) ? "1809" : version) } ================================================ FILE: jdk-download-url.sh ================================================ #!/bin/sh # Check if at least one argument was passed to the script # If one argument was passed and JAVA_VERSION is set, assign the argument to OS # If two arguments were passed, assign them to JAVA_VERSION and OS respectively # If three arguments were passed, assign them to JAVA_VERSION, OS and ARCHS respectively # If not, check if JAVA_VERSION and OS are already set. If they're not set, exit the script with an error message if [ $# -eq 1 ] && [ -n "$JAVA_VERSION" ]; then OS=$1 elif [ $# -eq 2 ]; then JAVA_VERSION=$1 OS=$2 elif [ $# -eq 3 ]; then JAVA_VERSION=$1 OS=$2 ARCHS=$3 elif [ -z "$JAVA_VERSION" ] && [ -z "$OS" ]; then echo "Error: No Java version and OS specified. Please set the JAVA_VERSION and OS environment variables or pass them as arguments." >&2 exit 1 elif [ -z "$JAVA_VERSION" ]; then echo "Error: No Java version specified. Please set the JAVA_VERSION environment variable or pass it as an argument." >&2 exit 1 elif [ -z "$OS" ]; then OS=$1 if [ -z "$OS" ]; then echo "Error: No OS specified. Please set the OS environment variable or pass it as an argument." >&2 exit 1 fi fi # Check if ARCHS is set. If it's not set, assign the current architecture to it if [ -z "$ARCHS" ]; then ARCHS=$(uname -m | sed -e 's/x86_64/x64/' -e 's/armv7l/arm/') else # Convert ARCHS to an array OLD_IFS=$IFS IFS=',' set -- "$ARCHS" ARCHS="" for arch in "$@"; do ARCHS="$ARCHS $arch" done IFS=$OLD_IFS fi # Check if jq and curl are installed # If they are not installed, exit the script with an error message if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then echo "jq and curl are required but not installed. Exiting with status 1." >&2 exit 1 fi # Replace underscores with plus signs in JAVA_VERSION ARCHIVE_DIRECTORY=$(echo "$JAVA_VERSION" | tr '_' '+') # URL encode ARCHIVE_DIRECTORY ENCODED_ARCHIVE_DIRECTORY=$(echo "$ARCHIVE_DIRECTORY" | xargs -I {} printf %s {} | jq "@uri" -jRr) # Determine the OS type for the URL OS_TYPE="linux" if [ "$OS" = "alpine" ]; then OS_TYPE="alpine-linux" fi if [ "$OS" = "windows" ]; then OS_TYPE="windows" fi # Initialize a variable to store the URL for the first architecture FIRST_ARCH_URL="" # Loop over the array of architectures for ARCH in $ARCHS; do # Fetch the download URL from the Adoptium API URL="https://api.adoptium.net/v3/binary/version/jdk-${ENCODED_ARCHIVE_DIRECTORY}/${OS_TYPE}/${ARCH}/jdk/hotspot/normal/eclipse?project=jdk" if ! RESPONSE=$(curl -fsI "$URL"); then echo "Error: Failed to fetch the URL for architecture ${ARCH} from ${URL}. Exiting with status 1." >&2 echo "Response: $RESPONSE" >&2 exit 1 fi # Extract the redirect URL from the HTTP response REDIRECTED_URL=$(echo "$RESPONSE" | grep -i location | awk '{print $2}' | tr -d '\r') # If no redirect URL was found, exit the script with an error message if [ -z "$REDIRECTED_URL" ]; then echo "Error: No redirect URL found for architecture ${ARCH} from ${URL}. Exiting with status 1." >&2 echo "Response: $RESPONSE" >&2 exit 1 fi # Use curl to check if the URL is reachable # If the URL is not reachable, print an error message and exit the script with status 1 if ! curl -v -fs "$REDIRECTED_URL" >/dev/null 2>&1; then echo "${REDIRECTED_URL}" is not reachable for architecture "${ARCH}". >&2 exit 1 fi # If FIRST_ARCH_URL is empty, store the current URL if [ -z "$FIRST_ARCH_URL" ]; then FIRST_ARCH_URL=$REDIRECTED_URL fi done # If all downloads are successful, print the URL for the first architecture echo "$FIRST_ARCH_URL" ================================================ FILE: jdk-download.sh ================================================ #!/bin/sh set -x # Check if curl and tar are installed if ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1 ; then echo "curl and tar are required but not installed. Exiting with status 1." >&2 exit 1 fi # Set the OS to "standard" by default OS="standard" # If a second argument is provided, use it as the OS if [ $# -eq 1 ]; then OS=$1 fi # Call jdk-download-url.sh with JAVA_VERSION and OS as arguments # The two scripts should be in the same directory. # That's why we're trying to find the directory of the current script and use it to call the other script. SCRIPT_DIR=$(cd "$(dirname "$0")" || exit; pwd) if ! DOWNLOAD_URL=$("${SCRIPT_DIR}"/jdk-download-url.sh "${JAVA_VERSION}" "${OS}"); then echo "Error: Failed to fetch the URL. Exiting with status 1." >&2 exit 1 fi # Use curl to download the JDK archive from the URL if ! curl --silent --location --output /tmp/jdk.tar.gz "${DOWNLOAD_URL}"; then echo "Error: Failed to download the JDK archive. Exiting with status 1." >&2 exit 1 fi # Extract the archive to the /opt/ directory if ! tar -xzf /tmp/jdk.tar.gz -C /opt/; then echo "Error: Failed to extract the JDK archive. Exiting with status 1." >&2 exit 1 fi # Get the name of the extracted directory EXTRACTED_DIR=$(tar -tzf /tmp/jdk.tar.gz | head -n 1 | cut -f1 -d"/") # Rename the extracted directory to /opt/jdk-${JAVA_VERSION} if ! mv "/opt/${EXTRACTED_DIR}" "/opt/jdk-${JAVA_VERSION}"; then echo "Error: Failed to rename the extracted directory. Exiting with status 1." >&2 exit 1 fi # Remove the downloaded archive if ! rm -f /tmp/jdk.tar.gz; then echo "Error: Failed to remove the downloaded archive. Exiting with status 1." >&2 exit 1 fi ================================================ FILE: setup-sshd ================================================ #!/usr/bin/env bash set -ex # The MIT License # # Copyright (c) 2015, CloudBees, Inc. # # 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. # Usage: # docker run jenkins/ssh-agent # or # docker run -e "JENKINS_AGENT_SSH_PUBKEY=" jenkins/ssh-agent write_key() { local ID_GROUP # As user, group, uid, gid and JENKINS_AGENT_HOME can be overridden at build, # we need to find the values for JENKINS_AGENT_HOME # ID_GROUP contains the user:group of JENKINS_AGENT_HOME directory ID_GROUP=$(stat -c '%U:%G' "${JENKINS_AGENT_HOME}") mkdir -p "${JENKINS_AGENT_HOME}/.ssh" echo "$1" > "${JENKINS_AGENT_HOME}/.ssh/authorized_keys" chown -Rf "${ID_GROUP}" "${JENKINS_AGENT_HOME}/.ssh" chmod 0700 -R "${JENKINS_AGENT_HOME}/.ssh" } if [[ ${JENKINS_AGENT_SSH_PUBKEY} == ssh-* ]]; then write_key "${JENKINS_AGENT_SSH_PUBKEY}" fi if [[ ${JENKINS_SLAVE_SSH_PUBKEY} == ssh-* ]]; then write_key "${JENKINS_SLAVE_SSH_PUBKEY}" fi # ensure variables passed to docker container are also exposed to ssh sessions env | grep _ >> /etc/environment if [[ $# -gt 0 ]]; then echo "${0##*/} params: $@" if [[ $1 == ssh-* ]]; then echo "Authorizing ssh pubkey found in params." write_key "$1" shift 1 elif [[ "$@" == "/usr/sbin/sshd -D -p 22" ]]; then # neutralize default jenkins docker-plugin command # we will run sshd at the end anyway echo "Ignoring provided sshd command." # if unquoted (4 tokens) shift extra 3 [[ "$2" == "-D" ]] && shift 3 shift 1 else echo "Executing params: '$@'" exec "$@" fi fi # generate host keys if not present ssh-keygen -A # do not detach (-D), log to stderr (-e), passthrough other arguments exec /usr/sbin/sshd -D -e "${@}" ================================================ FILE: setup-sshd.ps1 ================================================ # The MIT License # # Copyright (c) 2019-2020, Alex Earl # # 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. # Usage: # docker run jenkins/ssh-agent # or # docker run -e "JENKINS_AGENT_SSH_PUBKEY=" jenkins/ssh-agent # or # docker run -e "JENKINS_AGENT_SSH_PUBKEY=" -e "JENKINS_AGENT_SSH_KNOWNHOST_0=" -e "JENKINS_AGENT_SSH_KNOWNHOST_n=" jenkins/ssh-agent [CmdletBinding()] Param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string] $Cmd ) function Get-SSHDir { return Join-Path "C:/Users/$env:JENKINS_AGENT_USER" '.ssh' } function Check-SSHDir { $sshDir = Get-SSHDir if(-not (Test-Path $sshDir)) { New-Item -Type Directory -Path $sshDir | Out-Null icacls.exe $sshDir /setowner $env:JENKINS_AGENT_USER | Out-Null icacls.exe $sshDir /grant $('{0}:(CI)(OI)(F)' -f $env:JENKINS_AGENT_USER) /grant "administrators:(CI)(OI)(F)" | Out-Null icacls.exe $sshDir /inheritance:r | Out-Null } } function Write-Key($Key) { # this writes the key and sets the permissions correctly for pubkey auth $authorizedKeys = Join-Path (Get-SSHDir) 'authorized_keys' Set-Content -Path $authorizedKeys -Value "$Key" -Encoding UTF8 icacls.exe $authorizedKeys /setowner $env:JENKINS_AGENT_USER | Out-Null } function Write-HostKey($Key) { # this writes the key and sets the permissions $knownHosts = Join-Path (Get-SSHDir) 'known_hosts' Set-Content -Path $knownHosts -Value "$Key" -Encoding UTF8 icacls.exe $knownHosts /setowner $env:JENKINS_AGENT_USER | Out-Null } # Give the user Full Access to the home directory icacls.exe "C:/Users/$env:JENKINS_AGENT_USER" /grant "${env:JENKINS_AGENT_USER}:(CI)(OI)(F)" | Out-Null # check the .ssh dir permissions Check-SSHDir if($env:JENKINS_AGENT_SSH_PUBKEY -match "^ssh-.*") { Write-Key $env:JENKINS_AGENT_SSH_PUBKEY } $index = 0 $knownHostKeyVar = Get-ChildItem -Path "env:JENKINS_AGENT_SSH_KNOWNHOST_$index" -ErrorAction 'SilentlyContinue' while($null -ne $knownHostKeyVar) { Write-HostKey $knownHostKeyVar.Value $index++ $knownHostKeyVar = Get-ChildItem env: -Name "JENKINS_AGENT_SSH_KNOWNHOST_$index" } # ensure variables passed to docker container are also exposed to ssh sessions Get-ChildItem env: | ForEach-Object { setx /m $_.Name $_.Value | Out-Null } if(![System.String]::IsNullOrWhiteSpace($Cmd)) { Write-Host "$($MyInvocation.MyCommand.Name) param: '$Cmd'" if($Cmd -match "^ssh-.*") { Write-Host "Authorizing ssh pubkey found in params." Write-Key $Cmd } elseif($Cmd -match "^/usr/sbin/sshd") { # neutralize default jenkins docker-plugin command # we will run sshd at the end anyway Write-Host "Ignoring provided (linux) sshd command." } else { Write-Host "Executing param: $Cmd" & $Cmd exit } } Start-Service sshd # dump network information ipconfig netstat -a # aside from forwarding ssh logs, this keeps the container open Get-Content -Path "C:\ProgramData\ssh\logs\sshd.log" -Wait ================================================ FILE: tests/golden/expected_tags.txt ================================================ docker.io/jenkins/ssh-agent:alpine docker.io/jenkins/ssh-agent:alpine-jdk17 docker.io/jenkins/ssh-agent:alpine-jdk21 docker.io/jenkins/ssh-agent:alpine-jdk25 docker.io/jenkins/ssh-agent:alpine3.23 docker.io/jenkins/ssh-agent:alpine3.23-jdk17 docker.io/jenkins/ssh-agent:alpine3.23-jdk21 docker.io/jenkins/ssh-agent:alpine3.23-jdk25 docker.io/jenkins/ssh-agent:debian-jdk17 docker.io/jenkins/ssh-agent:debian-jdk21 docker.io/jenkins/ssh-agent:debian-jdk25 docker.io/jenkins/ssh-agent:jdk17 docker.io/jenkins/ssh-agent:jdk21 docker.io/jenkins/ssh-agent:jdk25 docker.io/jenkins/ssh-agent:latest docker.io/jenkins/ssh-agent:latest-alpine-jdk17 docker.io/jenkins/ssh-agent:latest-alpine-jdk21 docker.io/jenkins/ssh-agent:latest-alpine-jdk25 docker.io/jenkins/ssh-agent:latest-alpine3.23 docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk17 docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk21 docker.io/jenkins/ssh-agent:latest-alpine3.23-jdk25 docker.io/jenkins/ssh-agent:latest-debian-jdk17 docker.io/jenkins/ssh-agent:latest-debian-jdk21 docker.io/jenkins/ssh-agent:latest-debian-jdk25 docker.io/jenkins/ssh-agent:latest-jdk17 docker.io/jenkins/ssh-agent:latest-jdk21 docker.io/jenkins/ssh-agent:latest-jdk25 docker.io/jenkins/ssh-agent:latest-trixie-jdk17 docker.io/jenkins/ssh-agent:latest-trixie-jdk21 docker.io/jenkins/ssh-agent:latest-trixie-jdk25 docker.io/jenkins/ssh-agent:trixie-jdk17 docker.io/jenkins/ssh-agent:trixie-jdk21 docker.io/jenkins/ssh-agent:trixie-jdk25 ================================================ FILE: tests/keys.bash ================================================ PUBLIC_SSH_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAvnRN27LdPPQq2OH3GiFFGWX/SH5TCPVePLR21ngMFV8nAthXgYrFkRi/t+Wafe3ByTu2XYUDlXHKGIPIoAKo4gz5dIjUFfoac1ZuCDIbEiqPEjkk4tkfc2qr/BnIZsOYQi4Mbu+Z40VZEsAQU7eBinnZaHE1qGMHjS1xfrRtp2rdeO1EBz92FJ8dfnkUnohTXo3qPVSFGIPbh7UKEoKcyCosRO1P41iWD1rVsH1SLLXYAh2t49L7IPiplg09Dep6H47LyQVbxU9eXY8yMtUrRuwEk9IUX/IqpxNhk5hngHPP3JjsP0hyyrYSPkZlbs3izd9kk3y09Wn/ElHidiEk0Q==" PRIVATE_SSH_KEY=$(cat <&1 ; Write-Host `$version`"" $r = [regex] "^openjdk version `"(?\d+)" $m = $r.Match($stdout) $m | Should -Not -Be $null $m.Groups['major'].ToString() | Should -Be "$global:JAVAMAJORVERSION" } It 'has git-lfs (and thus git) installed and in the path' { $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"`& git lfs env`"" $exitCode | Should -Be 0 $r = [regex] "^git-lfs/(?\d+\.\d+\.\d+)" $m = $r.Match($stdout) $m | Should -Not -Be $null $m.Groups['version'].ToString() | Should -Be "$global:GITLFSVERSION" } AfterAll { Cleanup($global:CONTAINERNAME) } } Describe "[$global:IMAGE_TAG] create agent container with pubkey as argument" { BeforeAll { $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:PUBLIC_SSH_KEY`"" $exitCode | Should -Be 0 Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue } It 'runs commands via ssh' { $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`"" $exitCode | Should -Be 0 $stdout | Should -Match 'f00' } AfterAll { Cleanup($global:CONTAINERNAME) } } Describe "[$global:IMAGE_TAG] create agent container with pubkey as envvar" { BeforeAll { $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all `"$global:IMAGE_NAME`" `"$global:PUBLIC_SSH_KEY`"" $exitCode | Should -Be 0 Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue } It 'runs commands via ssh' { $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`"" $exitCode | Should -Be 0 $stdout | Should -Match 'f00' } AfterAll { Cleanup($global:CONTAINERNAME) } } $global:DOCKER_PLUGIN_DEFAULT_ARG="/usr/sbin/sshd -D -p 22" Describe "[$global:IMAGE_TAG] create agent container like docker-plugin with '$global:DOCKER_PLUGIN_DEFAULT_ARG' as argument" { BeforeAll { [string]::IsNullOrWhiteSpace($global:DOCKER_PLUGIN_DEFAULT_ARG) | Should -BeFalse $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=`"$global:CONTAINERNAME`" --publish-all --env=`"JENKINS_AGENT_SSH_PUBKEY=$global:PUBLIC_SSH_KEY`" `"$global:IMAGE_NAME`" `"$global:DOCKER_PLUGIN_DEFAULT_ARG`"" $exitCode | Should -Be 0 Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue } It 'runs commands via ssh' { $exitCode, $stdout, $stderr = Run-ThruSSH $global:CONTAINERNAME "$global:PRIVATE_SSH_KEY" "$global:CONTAINERSHELL -NoLogo -C `"Write-Host 'f00'`"" $exitCode | Should -Be 0 $stdout | Should -Match 'f00' } AfterAll { Cleanup($global:CONTAINERNAME) } } Describe "[$global:IMAGE_TAG] image can be built" { It 'builds image' { $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg `"TOOLS_WINDOWS_VERSION=${global:TOOLSWINDOWSVERSION}`" --build-arg `"JAVA_VERSION=${global:JAVA_VERSION}`" --build-arg `"JAVA_HOME=C:\openjdk-${global:JAVAMAJORVERSION}`" --tag=${global:IMAGE_TAG} --file ./windows/${global:WINDOWSFLAVOR}/Dockerfile ." $exitCode | Should -Be 0 } } Describe "[$global:IMAGE_TAG] image can be built with custom build args" { BeforeAll { Push-Location -StackName 'agent' -Path "$PSScriptRoot/.." } It 'uses build args correctly' { $TEST_USER = 'testuser' $TEST_JAW = 'C:/hamster' $CUSTOM_IMAGE_NAME = "custom-${IMAGE_NAME}" $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg `"TOOLS_WINDOWS_VERSION=${global:TOOLSWINDOWSVERSION}`" --build-arg `"JAVA_VERSION=${global:JAVA_VERSION}`" --build-arg `"JAVA_HOME=C:\openjdk-${global:JAVAMAJORVERSION}`" --build-arg `"user=$TEST_USER`" --build-arg `"JENKINS_AGENT_WORK=$TEST_JAW`" --tag=$CUSTOM_IMAGE_NAME --file ./windows/${global:WINDOWSFLAVOR}/Dockerfile ." $exitCode | Should -Be 0 $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name=$global:CONTAINERNAME --publish-all $CUSTOM_IMAGE_NAME $global:CONTAINERSHELL" $exitCode | Should -Be 0 Is-ContainerRunning "$global:CONTAINERNAME" | Should -BeTrue $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME net user $TEST_USER" $exitCode | Should -Be 0 $stdout | Should -Match "User name\s*$TEST_USER" $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"(Get-ChildItem env:\ | Where-Object { `$_.Name -eq 'JENKINS_AGENT_WORK' }).Value`"" $exitCode | Should -Be 0 $stdout.Trim() | Should -Match "$TEST_JAW" } AfterAll { Cleanup($global:CONTAINERNAME) Pop-Location -StackName 'agent' } } ================================================ FILE: tests/tags.bats ================================================ #!/usr/bin/env bats load test_helpers SUT_DESCRIPTION="tags" @test "[${SUT_DESCRIPTION}] Default tags unchanged" { assert_matches_golden expected_tags make --silent tags } ================================================ FILE: tests/test_helpers.bash ================================================ #!/usr/bin/env bash set -eu # check dependencies ( type docker &>/dev/null || ( echo "docker is not available"; exit 1 ) type curl &>/dev/null || ( echo "curl is not available"; exit 1 ) type ssh &>/dev/null || ( echo "ssh is not available"; exit 1 ) )>&2 function printMessage { echo "# ${@}" >&3 } # Assert that $1 is the output of a command $2 function assert_run_cmd_output_equal { local expected_output local actual_output expected_output="${1}" shift run "${@}" assert_output "${expected_output}" } # Assert that golden file $1 matches the output of a command $2 assert_matches_golden() { local golden="$1" shift local golden_path="tests/golden/${golden}.txt" if [[ ! -f "${golden_path}" ]]; then echo "Golden file '${golden_path}' does not exist" return 1 fi # Run the command passed as arguments and capture its output local output output="$(mktemp)" "$@" > "${output}" # Compare with golden file diff -u "${golden_path}" <(cat "${output}") } function get_sut_image { test -n "${IMAGE:?"[sut_image] Please set the variable 'IMAGE' to the name of the image to test in 'docker-bake.hcl'."}" ## Retrieve the SUT image name from buildx # Option --print for 'docker buildx bake' prints the JSON configuration on the stdout # Option --silent for 'make' suppresses the echoing of command so the output is valid JSON # The image name is the 1st of the "tags" array, on the first "image" found make --silent show | jq -r ".target.${IMAGE}.tags[0]" } # Retry a command $1 times until it succeeds. Wait $2 seconds between retries. # Command is passed as the "rest" of arguments: $3 $4 $5 ... function retry { local attempts local delay local i attempts="${1}" shift delay="${1}" shift for ((i=0; i < attempts; i++)); do run "${@}" if assert_success; then return 0 fi sleep "${delay}" done printMessage "Command '${BATS_RUN_COMMAND}' failed ${attempts} times. Status: ${status}. Output: ${output}" false } # return the published port for given container port $1 function get_port { local agent_container_name="${1}" local port="${2}" docker port "${agent_container_name}" "${port}" | cut -d: -f2 } # run a given command through ssh on the test container. # Use the $status, $output and $lines variables to make assertions function run_through_ssh { local agent_container_name="${1}" shift 1 SSH_PORT=$(get_port "${agent_container_name}" 22) if [[ "${SSH_PORT}" = "" ]]; then printMessage "failed to get SSH port" false else TMP_PRIV_KEY_FILE=$(mktemp "${BATS_TMPDIR}"/bats_private_ssh_key_XXXXXXX) echo "${PRIVATE_SSH_KEY}" > "${TMP_PRIV_KEY_FILE}" \ && chmod 0600 "${TMP_PRIV_KEY_FILE}" echo "[DEBUG] *** Running ssh command" run ssh -i "${TMP_PRIV_KEY_FILE}" \ -o LogLevel=quiet \ -o UserKnownHostsFile=/dev/null \ -o StrictHostKeyChecking=no \ -l jenkins \ 127.0.0.1 \ -p "${SSH_PORT}" \ "${@}" echo "[DEBUG] *** Command was: ${BATS_RUN_COMMAND}" rm -f "${TMP_PRIV_KEY_FILE}" fi } function clean_test_container { local agent_container=$1 docker kill "${agent_container}" &>/dev/null ||: docker rm --force --volumes "${agent_container}" &>/dev/null ||: } function is_agent_container_running { local agent_container=$1 # 30s is considered enough for the SSH server to start, even under constraint retry 15 2 assert_run_cmd_output_equal healthy docker inspect -f '{{.State.Health.Status}}' "${agent_container}" } ================================================ FILE: tests/test_helpers.psm1 ================================================ function Test-CommandExists($command) { $oldPreference = $ErrorActionPreference $ErrorActionPreference = 'stop' $res = $false try { if(Get-Command $command) { $res = $true } } catch { $res = $false } finally { $ErrorActionPreference=$oldPreference } return $res } # check dependencies if(-Not (Test-CommandExists docker)) { Write-Error 'docker is not available' } function Get-EnvOrDefault($name, $def) { $entry = Get-ChildItem env: | Where-Object { $_.Name -eq $name } | Select-Object -First 1 if(($null -ne $entry) -and ![System.String]::IsNullOrWhiteSpace($entry.Value)) { return $entry.Value } return $def } function Retry-Command { [CmdletBinding()] param ( [parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [scriptblock] $ScriptBlock, [int] $RetryCount = 3, [int] $Delay = 30, [string] $SuccessMessage = 'Command executed successfuly!', [string] $FailureMessage = 'Failed to execute the command' ) process { $Attempt = 1 $Flag = $true do { try { $PreviousPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' Invoke-Command -NoNewScope -ScriptBlock $ScriptBlock -OutVariable Result 4>&1 $ErrorActionPreference = $PreviousPreference # flow control will execute the next line only if the command in the scriptblock executed without any errors # if an error is thrown, flow control will go to the 'catch' block Write-Verbose "$SuccessMessage `n" $Flag = $false } catch { if ($Attempt -gt $RetryCount) { Write-Verbose "$FailureMessage! Total retry attempts: $RetryCount" Write-Verbose "[Error Message] $($_.exception.message) `n" $Flag = $false } else { Write-Verbose "[$Attempt/$RetryCount] $FailureMessage. Retrying in $Delay seconds..." Start-Sleep -Seconds $Delay $Attempt = $Attempt + 1 } } } While ($Flag) } } function Cleanup($name='') { if([System.String]::IsNullOrWhiteSpace($name)) { $name = Get-EnvOrDefault 'IMAGE_NAME' '' } if(![System.String]::IsNullOrWhiteSpace($name)) { docker kill "$name" 2>&1 | Out-Null docker rm -fv "$name" 2>&1 | Out-Null } } function CleanupNetwork($name) { docker network rm $name 2>&1 | Out-Null } function Is-ContainerRunning($container) { Start-Sleep -Seconds 5 return Retry-Command -RetryCount 10 -Delay 2 -ScriptBlock { $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "inspect --format `"{{.State.Running}}`" $container" if(($exitCode -ne 0) -or (-not $stdout.Contains('true')) ) { throw('Exit code incorrect, or invalid value for running state') } return $true } } function Run-Program($cmd, $params) { $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.CreateNoWindow = $true $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.WorkingDirectory = (Get-Location) $psi.FileName = $cmd $psi.Arguments = $params $proc = New-Object System.Diagnostics.Process $proc.StartInfo = $psi [void]$proc.Start() $stdout = $proc.StandardOutput.ReadToEnd() $stderr = $proc.StandardError.ReadToEnd() $proc.WaitForExit() if(($env:TESTS_DEBUG -eq 'debug') -or ($env:TESTS_DEBUG -eq 'verbose')) { Write-Host -ForegroundColor DarkBlue "[cmd] $cmd $params" if ($env:TESTS_DEBUG -eq 'verbose') { Write-Host -ForegroundColor DarkGray "[stdout] $stdout" } if($proc.ExitCode -ne 0){ Write-Host -ForegroundColor DarkRed "[stderr] $stderr" } } return $proc.ExitCode, $stdout, $stderr } # return the published port for given container port $1 function Get-Port($container, $port=22) { $exitCode, $stdout, $stderr = Run-Program 'docker.exe' "port $container $port" return ($stdout -split ":" | Select-Object -Skip 1).Trim() } # run a given command through ssh on the test container. function Run-ThruSSH($container, $privateKeyVal, $cmd) { $SSH_PORT = Get-Port $container 22 if([System.String]::IsNullOrWhiteSpace($SSH_PORT)) { Write-Error 'Failed to get SSH port' return -1, $null, $null } else { $TMP_PRIV_KEY_FILE = New-TemporaryFile Set-Content -Path $TMP_PRIV_KEY_FILE -Value "$privateKeyVal" $exitCode, $stdout, $stderr = Run-Program 'ssh.exe' "-v -i `"${TMP_PRIV_KEY_FILE}`" -o LogLevel=quiet -o UserKnownHostsFile=NUL -o StrictHostKeyChecking=no -l jenkins localhost -p $SSH_PORT $cmd" Remove-Item -Force $TMP_PRIV_KEY_FILE return $exitCode, $stdout, $stderr } } ================================================ FILE: tests/tests.bats ================================================ #!/usr/bin/env bats load test_helpers load 'test_helper/bats-support/load' # this is required by bats-assert! load 'test_helper/bats-assert/load' load keys IMAGE=${IMAGE:-debian_jdk11} SUT_IMAGE=$(get_sut_image) ARCH=${ARCH:-x86_64} AGENT_CONTAINER=bats-jenkins-ssh-agent # TODO: uncomment when git-lfs version is the same across all images # GIT_LFS_VERSION='3.7.1' # About the health CMD: the netcat command (`nc`) needs the options `-w1` to return 1s after reaches EOF. It's a portable option of `nc` (on BSD, Debian, Windows, busybox). # Of course, to reach EOF, you need to provide something to the stding: it's the reason of the `echo` piped command docker_run_opts=('--detach' '--publish-all' '--health-cmd=echo | nc -w1 localhost 22' '--health-start-period=2s' '--health-interval=2s' '--health-retries=10' '--health-timeout=2s' "${SUT_IMAGE}") @test "[${SUT_IMAGE}] test label in docker metadata" { local expected_source="https://github.com/jenkinsci/docker-ssh-agent" local actual_source actual_source=$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.source"}}' "${SUT_IMAGE}") assert_equal "${expected_source}" "${actual_source}" } @test "[${SUT_IMAGE}] checking image metadata" { local VOLUMES_MAP VOLUMES_MAP="$(docker inspect -f '{{.Config.Volumes}}' "${SUT_IMAGE}")" echo "${VOLUMES_MAP}" | grep '/tmp' echo "${VOLUMES_MAP}" | grep '/home/jenkins' echo "${VOLUMES_MAP}" | grep '/run' echo "${VOLUMES_MAP}" | grep '/var/run' } @test "[${SUT_IMAGE}] image has bash and java installed and in the PATH" { local test_container_name=${AGENT_CONTAINER}-bash-java clean_test_container "${test_container_name}" docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}" run docker exec "${test_container_name}" which bash assert_success run docker exec "${test_container_name}" bash --version assert_success run docker exec "${test_container_name}" which java assert_success run docker exec "${test_container_name}" sh -c "java -version" assert_success clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] image has no pre-existing SSH host keys" { local test_container_name=${AGENT_CONTAINER}-ssh-hostkeys clean_test_container "${test_container_name}" docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}" run docker exec "${test_container_name}" ls -l /etc/ssh/ssh_host*_key* assert_failure clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] create agent container with pubkey as argument" { local test_container_name=${AGENT_CONTAINER}-pubkey-arg clean_test_container "${test_container_name}" docker run --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}" is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" echo f00 assert_success assert_equal "${output}" "f00" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] create agent container with pubkey as environment variable (legacy environment variable)" { local test_container_name=${AGENT_CONTAINER}-pubkey-legacy-env clean_test_container "${test_container_name}" docker run --env="JENKINS_SLAVE_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" echo f00 assert_success assert_equal "${output}" "f00" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] create agent container with pubkey as environment variable (JENKINS_AGENT_SSH_PUBKEY)" { local test_container_name=${AGENT_CONTAINER}-pubkey-env clean_test_container "${test_container_name}" docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" echo f00 assert_success assert_equal "${output}" "f00" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] Run Java in a SSH connection" { local test_container_name=${AGENT_CONTAINER}-java-in-ssh clean_test_container "${test_container_name}" docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" java -version assert_success assert_output --regexp '^openjdk version \"[[:digit:]]+' clean_test_container "${test_container_name}" } DOCKER_PLUGIN_DEFAULT_ARG="/usr/sbin/sshd -D -p 22" @test "[${SUT_IMAGE}] create agent container like docker-plugin with '${DOCKER_PLUGIN_DEFAULT_ARG}' (unquoted) as argument" { [ -n "$DOCKER_PLUGIN_DEFAULT_ARG" ] local test_container_name=${AGENT_CONTAINER}-docker-plugin clean_test_container "${test_container_name}" docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" ${DOCKER_PLUGIN_DEFAULT_ARG} is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" echo f00 assert_success assert_equal "${output}" "f00" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] create agent container with '${DOCKER_PLUGIN_DEFAULT_ARG}' (quoted) as argument" { [ -n "$DOCKER_PLUGIN_DEFAULT_ARG" ] local test_container_name=${AGENT_CONTAINER}-docker-plugin-quoted clean_test_container "${test_container_name}" docker run --env="JENKINS_AGENT_SSH_PUBKEY=${PUBLIC_SSH_KEY}" --name="${test_container_name}" "${docker_run_opts[@]}" "${DOCKER_PLUGIN_DEFAULT_ARG}" is_agent_container_running "${test_container_name}" run_through_ssh "${test_container_name}" echo f00 assert_success assert_equal "${output}" "f00" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] use build args correctly" { cd "${BATS_TEST_DIRNAME}"/.. || false local TEST_USER=test-user local TEST_GROUP=test-group local TEST_UID=2000 local TEST_GID=3000 local TEST_JAH=/home/something local sut_image="${SUT_IMAGE}-tests-${BATS_TEST_NUMBER}" # false positive detecting platform # shellcheck disable=SC2140 docker buildx bake \ --set "${IMAGE}".args.user="${TEST_USER}" \ --set "${IMAGE}".args.group="${TEST_GROUP}" \ --set "${IMAGE}".args.uid="${TEST_UID}" \ --set "${IMAGE}".args.gid="${TEST_GID}" \ --set "${IMAGE}".args.JENKINS_AGENT_HOME="${TEST_JAH}" \ --set "${IMAGE}".platform="linux/${ARCH}" \ --set "${IMAGE}".tags="${sut_image}" \ --load `# Image should be loaded on the Docker engine`\ "${IMAGE}" local test_container_name=${AGENT_CONTAINER}-build-args clean_test_container "${test_container_name}" docker run --detach --name="${test_container_name}" --publish-all "${sut_image}" "${PUBLIC_SSH_KEY}" run docker exec "${test_container_name}" sh -c "id -u -n ${TEST_USER}" assert_line --index 0 "${TEST_USER}" run docker exec "${test_container_name}" sh -c "id -g -n ${TEST_USER}" assert_line --index 0 "${TEST_GROUP}" run docker exec "${test_container_name}" sh -c "id -u ${TEST_USER}" assert_line --index 0 "${TEST_UID}" run docker exec "${test_container_name}" sh -c "id -g ${TEST_USER}" assert_line --index 0 "${TEST_GID}" run docker exec "${test_container_name}" sh -c 'stat -c "%U:%G" "${JENKINS_AGENT_HOME}"' assert_line --index 0 "${TEST_USER}:${TEST_GROUP}" clean_test_container "${test_container_name}" } @test "[${SUT_IMAGE}] has utf-8 locale" { run docker run --entrypoint sh --rm "${SUT_IMAGE}" -c 'locale charmap' assert_equal "${output}" "UTF-8" } @test "[${SUT_IMAGE}] the default 'jenkins' user is allowed to write in the default agent directory" { run docker run --user=jenkins --entrypoint='' --rm "${SUT_IMAGE}" sh -c 'touch "${AGENT_WORKDIR}"/test.txt' assert_success } @test "[${SUT_IMAGE}] image has required tools installed and present in the PATH, can clone a repo and list large files" { local test_container_name=${AGENT_CONTAINER}-bash-java clean_test_container "${test_container_name}" docker run --name="${test_container_name}" --name="${test_container_name}" "${docker_run_opts[@]}" "${PUBLIC_SSH_KEY}" run docker exec "${test_container_name}" sh -c "command -v ssh" assert_success run docker exec "${test_container_name}" ssh -V assert_success run docker exec "${test_container_name}" sh -c "command -v git" assert_success run docker exec "${test_container_name}" git --version assert_success run docker exec "${test_container_name}" sh -c "command -v less" assert_success run docker exec "${test_container_name}" less -V assert_success run docker exec "${test_container_name}" sh -c "command -v patch" assert_success run docker exec "${test_container_name}" patch --version assert_success run docker exec "${test_container_name}" git clone https://github.com/jenkinsci/docker-ssh-agent.git assert_success run docker exec "${test_container_name}" git lfs env assert_success # TODO: replace assert_success with assert_output when git-lfs version is the same across all images # assert_output --partial "${GIT_LFS_VERSION}" clean_test_container "${test_container_name}" } ================================================ FILE: tests/update-golden-file.sh ================================================ #!/usr/bin/env bash set -euo pipefail # This script runs a specified command, captures its output, # and compares it against a "golden file" representing the expected output. # If the output differs, the script shows a diff and allows the user to update the golden file. # If the output matches, it reports that the golden file is up-to-date. # # Usage: # ./update-golden-file.sh # # Arguments: # Name of the test, used to determine the golden file path. # The corresponding golden file will be stored as: # golden/.txt # # Command to run, whose stdout will be compared to the golden file. # This can include arguments, e.g.: # ./update-golden-file.sh expected_tags make tags # # Notes: # - Requires Bash 4+ for `BASH_SOURCE` handling. # - The script is safe to run from any directory; golden files are always relative to the script's own location. if [[ $# -lt 2 ]]; then echo "Usage: $0 " echo "Example:" echo " $0 expected_tags make tags" exit 1 fi name="$1" shift # Ensure golden folder is always relative to this script script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" golden_file="${name}.txt" golden_path="${script_dir}/golden/${golden_file}" tmp="$(mktemp)" echo echo "Golden file path:" echo " ${golden_path}" echo echo "Running command:" echo " $*" echo "$@" > "${tmp}" action="create" if [[ -f "${golden_path}" ]]; then if diff -u "${golden_path}" "${tmp}" > /dev/null; then echo "Golden file '${golden_file}' is already up-to-date." rm "${tmp}" exit 0 fi echo "Diff against existing golden file '${golden_file}':" diff -u "${golden_path}" "${tmp}" || true action="update" else echo "Golden file '${golden_file}' does not exist yet." fi echo echo "Golden file to ${action}: '${golden_file}'" read -rp "Proceed? [y/N] " answer if [[ "${answer}" =~ ^[Yy]$ ]]; then mkdir -p "$(dirname "${golden_path}")" mv "${tmp}" "${golden_path}" echo "Golden file '${golden_file}' ${action}d." else rm "${tmp}" echo "Aborted. Golden file '${golden_file}' unchanged." fi ================================================ FILE: updatecli/updatecli.d/alpine.yaml ================================================ --- name: Bump Alpine version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: latestVersion: kind: githubrelease name: "Get the latest Alpine Linux version" spec: owner: "alpinelinux" repository: "aports" # Its release process follows Alpine's token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" versionfilter: kind: semver pattern: "~3" transformers: - trimprefix: "v" conditions: testDockerfileArg: name: "Does the Dockerfile have an ARG instruction for the Alpine Linux version?" kind: dockerfile disablesourceinput: true spec: file: alpine/Dockerfile instruction: keyword: "ARG" matcher: "ALPINE_TAG" testDockerImageExists: name: "Does the Docker Image exist on the Docker Hub?" kind: dockerimage sourceid: latestVersion spec: image: "alpine" # tag come from the source architecture: amd64 targets: updateDockerfile: name: "Update the value of the base image (ARG ALPINE_TAG) in the Dockerfile" kind: dockerfile spec: file: alpine/Dockerfile instruction: keyword: "ARG" matcher: "ALPINE_TAG" scmid: default updateDockerBake: name: "Update the value of the base image (ARG ALPINE_TAG) in the docker-bake.hcl" kind: hcl spec: file: docker-bake.hcl path: variable.ALPINE_FULL_TAG.default scmid: default actions: default: kind: github/pullrequest scmid: default title: Bump Alpine Linux Version to {{ source "latestVersion" }} spec: labels: - dependencies ================================================ FILE: updatecli/updatecli.d/bats.yaml ================================================ --- name: Bump `bats` version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: lastVersion: kind: githubrelease spec: owner: bats-core repository: bats-core token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" versionfilter: kind: semver targets: updateMakefile: name: "Updates `bats` version in the Makefile" kind: file spec: file: Makefile matchpattern: >- git clone --branch (.*) https://github.com/bats-core/bats-core bats replacepattern: >- git clone --branch {{ source "lastVersion" }} https://github.com/bats-core/bats-core bats scmid: default actions: default: kind: github/pullrequest scmid: default title: 'chore(tests): Bump `bats` version to {{ source "lastVersion" }}' spec: labels: - chore # Because bats is only used for testing - bats ================================================ FILE: updatecli/updatecli.d/debian.yaml ================================================ --- name: Bump Debian Trixie version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: trixieLatestVersion: kind: dockerimage name: "Get latest Debian Trixie Linux version" spec: image: "debian" tagfilter: "trixie-*" versionfilter: kind: regex pattern: >- trixie-\d+$ conditions: checkArchitecturesAvailability: kind: dockerimage name: Check if container image is available for all architectures sourceid: trixieLatestVersion spec: image: "debian" architectures: - linux/amd64 - linux/arm64 - linux/s390x - linux/ppc64le targets: updateDockerfile: name: "Update value of base image (ARG DEBIAN_RELEASE) in Dockerfile" kind: dockerfile sourceid: trixieLatestVersion spec: file: debian/Dockerfile instruction: keyword: "ARG" matcher: "DEBIAN_RELEASE" scmid: default updateDockerBake: name: "Update default value of variable DEBIAN_RELEASE in docker-bake.hcl" kind: hcl sourceid: trixieLatestVersion spec: file: docker-bake.hcl path: variable.DEBIAN_RELEASE.default scmid: default actions: default: kind: github/pullrequest scmid: default title: Bump Debian Trixie Linux version to {{ source "trixieLatestVersion" }} spec: labels: - dependencies ================================================ FILE: updatecli/updatecli.d/git-lfs.yml ================================================ name: Bump `git-lfs` version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: lastVersion: kind: githubrelease name: Get latest `git-lfs` version spec: owner: git-lfs repository: git-lfs token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" versionfilter: kind: semver transformers: - trimprefix: "v" targets: setGitLfsVersion: name: Update `git-lfs` version in Dockerfiles kind: dockerfile spec: files: - alpine/Dockerfile - debian/Dockerfile - windows/nanoserver/Dockerfile - windows/windowsservercore/Dockerfile instruction: keyword: ARG matcher: GIT_LFS_VERSION scmid: default setGitLfsVersionLinuxTests: name: Update `git-lfs` version in Linux tests kind: file spec: file: tests/tests.bats matchpattern: > GIT_LFS_VERSION=(.*) replacepattern: > GIT_LFS_VERSION='{{ source "lastVersion" }}' scmid: default setGitLfsVersionWindowsTests: name: Update `git-lfs` version in Windows tests kind: file spec: file: tests/sshAgent.Tests.ps1 matchpattern: > global:GITLFSVERSION =(.*) replacepattern: > global:GITLFSVERSION = '{{ source "lastVersion" }}' scmid: default actions: default: kind: github/pullrequest title: Bump `git-lfs` version to {{ source "lastVersion" }} scmid: default spec: labels: - enhancement - git-lfs ================================================ FILE: updatecli/updatecli.d/git-windows.yml ================================================ --- name: Bump Git version on Windows scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: lastVersion: kind: githubrelease name: Get the latest Git version spec: owner: "git-for-windows" repository: "git" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" versionfilter: kind: regex ## Latest stable v{x.y.z}.windows. pattern: 'v(\d*)\.(\d*)\.(\d*)\.windows\.(\d*)$' transformers: - trimprefix: "v" targets: # Nanoserver setGitVersionWindowsNanoserver: name: Update the Git Windows version for Windows Nanoserver transformers: - findsubmatch: pattern: '(.*).windows\.(\d*)$' captureindex: 1 kind: dockerfile spec: file: windows/nanoserver/Dockerfile instruction: keyword: ARG matcher: GIT_VERSION scmid: default setGitPackagePatchWindowsNanoserver: name: Update the Git Package Windows patch for Windows Nanoserver transformers: - findsubmatch: pattern: '(.*).windows\.(\d*)$' captureindex: 2 kind: dockerfile spec: file: windows/nanoserver/Dockerfile instruction: keyword: ARG matcher: GIT_PATCH_VERSION scmid: default # Windows Server Core setGitVersionWindowsServer: name: Update the Git Windows version for Windows Server Core transformers: - findsubmatch: pattern: '(.*).windows\.(\d*)$' captureindex: 1 kind: dockerfile spec: file: windows/windowsservercore/Dockerfile instruction: keyword: ARG matcher: GIT_VERSION scmid: default setGitPackagePatchWindowsServer: name: Update the Git Package Windows patch for Windows Server Core transformers: - findsubmatch: pattern: '(.*).windows\.(\d*)$' captureindex: 2 kind: dockerfile spec: file: windows/windowsservercore/Dockerfile instruction: keyword: ARG matcher: GIT_PATCH_VERSION scmid: default actions: default: kind: github/pullrequest title: Bump Git version on Windows to {{ source "lastVersion" }} scmid: default spec: labels: - enhancement - git - windows ================================================ FILE: updatecli/updatecli.d/jdk17.yaml ================================================ --- name: Bump Temurin's JDK17 version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: jdk17LastVersion: kind: temurin name: Get the latest Adoptium JDK17 version spec: featureversion: 17 transformers: - trimprefix: "jdk-" conditions: checkTemurinAllReleases: name: Check if the "" is available for all platforms kind: temurin spec: featureversion: 17 platforms: - alpine-linux/x64 - linux/x64 - linux/aarch64 - linux/ppc64le - linux/s390x - linux/arm - windows/x64 targets: setJDK17VersionDockerBake: name: "Bump JDK17 version for Linux images in the docker-bake.hcl file" kind: hcl transformers: - replacer: from: "+" to: "_" spec: file: docker-bake.hcl path: variable.JAVA17_VERSION.default scmid: default setJDK17VersionAlpine: name: "Bump JDK17 default ARG version on Alpine Dockerfile" kind: dockerfile transformers: - replacer: from: "+" to: "_" spec: file: alpine/Dockerfile instruction: keyword: ARG matcher: JAVA_VERSION scmid: default setJDK17VersionDebian: name: "Bump JDK17 default ARG version on Debian Dockerfile" kind: dockerfile transformers: - replacer: from: "+" to: "_" spec: file: debian/Dockerfile instruction: keyword: ARG matcher: JAVA_VERSION scmid: default actions: default: kind: github/pullrequest scmid: default title: Bump JDK17 version to {{ source "jdk17LastVersion" }} spec: labels: - dependencies - jdk17 ================================================ FILE: updatecli/updatecli.d/jdk21.yaml ================================================ --- name: Bump Temurin's JDK21 version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" temurin21-binaries: kind: "github" spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "adoptium" repository: "temurin21-binaries" token: '{{ requiredEnv .github.token }}' branch: "main" sources: jdk21LastVersion: kind: temurin name: Get the latest Adoptium JDK21 version spec: featureversion: 21 transformers: - trimprefix: "jdk-" conditions: checkTemurinAllReleases: name: Check if the "" is available for all platforms kind: temurin spec: featureversion: 21 platforms: - alpine-linux/x64 - alpine-linux/aarch64 - linux/x64 - linux/aarch64 - linux/ppc64le - linux/s390x - windows/x64 targets: setJDK21VersionDockerBake: name: "Bump JDK21 version for Linux images in the docker-bake.hcl file" kind: hcl sourceid: jdk21LastVersion transformers: - replacer: from: "+" to: "_" spec: file: docker-bake.hcl path: variable.JAVA21_VERSION.default scmid: default actions: default: kind: github/pullrequest scmid: default title: Bump JDK21 version to {{ source "jdk21LastVersion" }} spec: labels: - dependencies - jdk21 ================================================ FILE: updatecli/updatecli.d/jdk25.yaml ================================================ --- name: Bump JDK25 version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" temurin25-binaries: kind: "github" spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "adoptium" repository: "temurin25-binaries" token: '{{ requiredEnv .github.token }}' branch: "main" sources: latestJDK25Version: kind: temurin name: Get the latest Adoptium JDK25 version via the API spec: featureversion: 25 transformers: - trimprefix: "jdk-" # Architectures must match those of the targets in docker-bake.hcl conditions: checkTemurinAllReleases: name: Check if the "" is available for all platforms kind: temurin sourceid: latestJDK25Version spec: featureversion: 25 platforms: - alpine-linux/x64 - alpine-linux/aarch64 - linux/x64 - linux/aarch64 - linux/ppc64le - linux/s390x - windows/x64 targets: setJDK25VersionDockerBake: name: "Bump JDK25 version in the docker-bake.hcl file" kind: hcl transformers: - replacer: from: "+" to: "_" spec: file: docker-bake.hcl path: variable.JAVA25_VERSION.default scmid: default actions: default: kind: github/pullrequest scmid: default title: Bump JDK25 version to {{ source "latestJDK25Version" }} spec: labels: - dependencies - jdk25 ================================================ FILE: updatecli/updatecli.d/openssh.yml ================================================ name: Bump OpenSSH version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: lastVersion: kind: githubrelease name: Get the latest OpenSSH version spec: owner: PowerShell repository: Win32-OpenSSH token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" targets: setOpenSSHVersionNanoserver: name: Update the OpenSSH version for Windows Nanoserver kind: dockerfile spec: file: windows/nanoserver/Dockerfile instruction: keyword: ARG matcher: OPENSSH_VERSION scmid: default setOpenSSHVersionWindowsServerCore: name: Update the OpenSSH version for Windows Server Core kind: dockerfile spec: file: windows/windowsservercore/Dockerfile instruction: keyword: ARG matcher: OPENSSH_VERSION scmid: default actions: default: kind: github/pullrequest title: Bump OpenSSH version to {{ source "lastVersion" }} scmid: default spec: labels: - enhancement - openssh - windows ================================================ FILE: updatecli/updatecli.d/pester.yaml ================================================ name: Bump `pester` version scms: default: kind: github spec: user: "{{ .github.user }}" email: "{{ .github.email }}" owner: "{{ .github.owner }}" repository: "{{ .github.repository }}" token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" branch: "{{ .github.branch }}" sources: lastVersion: kind: githubrelease name: Get latest `pester` version spec: owner: pester repository: pester token: "{{ requiredEnv .github.token }}" username: "{{ .github.username }}" versionfilter: kind: semver transformers: - trimprefix: "v" targets: setGitLfsVersion: name: Update `pester` version in build.ps1 kind: file spec: file: build.ps1 matchpattern: > PesterVersion = (.*) replacepattern: > PesterVersion = '{{ source "lastVersion" }}', scmid: default actions: default: kind: github/pullrequest title: Bump `pester` version to {{ source "lastVersion" }} scmid: default spec: labels: - dependencies - pester ================================================ FILE: updatecli/values.github-action.yaml ================================================ github: user: "GitHub Actions" email: "41898282+github-actions[bot]@users.noreply.github.com" username: "github-actions" token: "UPDATECLI_GITHUB_TOKEN" branch: "master" owner: "jenkinsci" repository: "docker-ssh-agent" ================================================ FILE: updatecli/values.temurin.yaml ================================================ temurin: version_pattern: "^jdk-[17|21].(\\d*).(\\d*).(\\d*)(.(\\d*))\\+(\\d*)?$" ================================================ FILE: windows/nanoserver/Dockerfile ================================================ # escape=` # The MIT License # # Copyright (c) 2019-2020, Alex Earl and other Jenkins 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 # 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. ARG WINDOWS_VERSION_TAG ARG TOOLS_WINDOWS_VERSION FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" AS jdk-core # $ProgressPreference: https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251261324 SHELL ["powershell.exe", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] ARG JAVA_VERSION RUN New-Item -ItemType Directory -Path C:\temp | Out-Null ; ` $javaMajorVersion = $env:JAVA_VERSION.substring(0,2) ; ` $msiUrl = 'https://api.adoptium.net/v3/installer/version/jdk-{0}/windows/x64/jdk/hotspot/normal/eclipse?project=jdk' -f $env:JAVA_VERSION ; ` Invoke-WebRequest $msiUrl -OutFile 'C:\temp\jdk.msi' ; ` $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList '/i', 'C:\temp\jdk.msi', '/L*V', 'C:\temp\OpenJDK.log', '/quiet', 'ADDLOCAL=FeatureEnvironment,FeatureJarFileRunWith,FeatureJavaHome', "INSTALLDIR=C:\openjdk-${javaMajorVersion}" -Wait -Passthru ; ` $proc.WaitForExit() ; ` Remove-Item -Path C:\temp -Recurse | Out-Null FROM mcr.microsoft.com/powershell:nanoserver-"${TOOLS_WINDOWS_VERSION}" AS pwsh-source FROM mcr.microsoft.com/windows/nanoserver:"${WINDOWS_VERSION_TAG}" ARG JAVA_HOME ENV PSHOME="C:\Program Files\PowerShell" ENV PATH="C:\Windows\system32;C:\Windows;${PSHOME};" # The nanoserver image is nice and small, but we need a couple of things to get SSH working COPY --from=jdk-core /windows/system32/netapi32.dll /windows/system32/netapi32.dll COPY --from=jdk-core /windows/system32/whoami.exe /windows/system32/whoami.exe COPY --from=jdk-core $JAVA_HOME $JAVA_HOME COPY --from=pwsh-source $PSHOME $PSHOME SHELL ["pwsh.exe", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] USER ContainerAdministrator # Backward compatibility with version <= 5.x: create a symlink to the "new" JAVA_HOME RUN $javaMajorVersion = $env:JAVA_HOME.Substring($env:JAVA_HOME.Length - 2); ` New-Item -Path "C:\jdk-${javaMajorVersion}" -ItemType SymbolicLink -Value "${env:JAVA_HOME}" # Install git ARG GIT_VERSION=2.54.0 ARG GIT_PATCH_VERSION=1 RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` # The patch "windows.1" always have a different URL than the subsequent patch (ZIP filename is different) if($env:GIT_PATCH_VERSION -eq 1) { $url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION); } ` else {$url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}.{1}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION)} ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest $url -OutFile 'mingit.zip' -UseBasicParsing ; ` Expand-Archive mingit.zip -DestinationPath c:\mingit ; ` Remove-Item mingit.zip -Force # Add java & git to PATH ENV ProgramFiles="C:\Program Files" ENV WindowsPATH="C:\Windows\system32;C:\Windows" ENV PATH="${WindowsPATH};${ProgramFiles}\PowerShell;${JAVA_HOME}\bin;C:\mingit\cmd" # Install git-lfs ARG GIT_LFS_VERSION=3.7.1 RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` $url = $('https://github.com/git-lfs/git-lfs/releases/download/v{0}/git-lfs-windows-amd64-v{0}.zip' -f $env:GIT_LFS_VERSION) ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest $url -OutFile 'GitLfs.zip' -UseBasicParsing ; ` Expand-Archive GitLfs.zip -DestinationPath c:\mingit\mingw64\bin ; ` $gitLfsFolder = 'c:\mingit\mingw64\bin\git-lfs-{0}' -f $env:GIT_LFS_VERSION ; ` Move-Item -Path "${gitLfsFolder}\git-lfs.exe" -Destination c:\mingit\mingw64\bin\ ; ` Remove-Item -Path $gitLfsFolder -Recurse -Force ; ` Remove-Item GitLfs.zip -Force ; ` & C:\mingit\cmd\git.exe lfs install ARG user=jenkins ARG JENKINS_AGENT_WORK="C:/Users/${user}/Work" ENV JENKINS_AGENT_USER=${user} ENV JENKINS_AGENT_WORK=${JENKINS_AGENT_WORK} # Setup SSH server ARG OPENSSH_VERSION=10.0.0.0p2-Preview RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` $url = 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/{0}/OpenSSH-Win64.zip' -f $env:OPENSSH_VERSION ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest -Uri $url -OutFile C:/openssh.zip -UseBasicParsing ; ` Expand-Archive c:/openssh.zip 'C:/Program Files' ; ` Remove-Item C:/openssh.zip ; ` $env:PATH = '{0};{1}' -f $env:PATH,'C:\Program Files\OpenSSH-Win64' ; ` if(!(Test-Path 'C:\ProgramData\ssh')) { New-Item -Type Directory -Path 'C:\ProgramData\ssh' | Out-Null } ; ` icacls 'C:\ProgramData\ssh' /inheritance:d ; ` icacls 'C:\ProgramData\ssh' /remove 'CREATOR OWNER' ; ` & 'C:/Program Files/OpenSSH-Win64/Install-SSHd.ps1' ; ` Copy-Item 'C:\Program Files\OpenSSH-Win64\sshd_config_default' 'C:\ProgramData\ssh\sshd_config' ; ` $content = Get-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` $content | ForEach-Object { $_ -replace '#PermitRootLogin.*','PermitRootLogin no' ` -replace '#PasswordAuthentication.*','PasswordAuthentication no' ` -replace '#PermitEmptyPasswords.*','PermitEmptyPasswords no' ` -replace '#PubkeyAuthentication.*','PubkeyAuthentication yes' ` -replace '#SyslogFacility.*','SyslogFacility LOCAL0' ` -replace '#LogLevel.*','LogLevel INFO' ` -replace 'Match Group administrators','' ` -replace '(\s*)AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','' ` } | ` Set-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'ChallengeResponseAuthentication no' ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'HostKeyAgent \\.\pipe\openssh-ssh-agent' ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('Match User {0}' -f $env:JENKINS_AGENT_USER) ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value (' AuthorizedKeysFile C:/Users/{0}/.ssh/authorized_keys' -f $env:JENKINS_AGENT_USER) ; ` New-Item -Path HKLM:\SOFTWARE -Name OpenSSH -Force | Out-Null ; ` New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value 'C:\Program Files\Powershell\pwsh.exe' -PropertyType string -Force | Out-Null ; ` Remove-Item -Path "C:\ProgramData\ssh\ssh_host*_key*" COPY CreateProfile.psm1 C:/ # Create user and user directory RUN Import-Module -Force C:/CreateProfile.psm1 ; ` New-UserWithProfile -UserName $env:JENKINS_AGENT_USER -Description 'Jenkins Agent User' ; ` Remove-Item -Force C:/CreateProfile.psm1 VOLUME "${JENKINS_AGENT_WORK}" "C:/Users/${user}/AppData/Local/Temp" WORKDIR "${JENKINS_AGENT_WORK}" COPY setup-sshd.ps1 C:/ProgramData/Jenkins/setup-sshd.ps1 EXPOSE 22 LABEL ` org.opencontainers.image.vendor="Jenkins project" ` org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" ` org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" ` org.opencontainers.image.url="https://www.jenkins.io/" ` org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" ` org.opencontainers.image.licenses="MIT" ENTRYPOINT ["pwsh.exe", "-NoExit", "-Command", "& C:/ProgramData/Jenkins/setup-sshd.ps1"] ================================================ FILE: windows/windowsservercore/Dockerfile ================================================ # escape=` # The MIT License # # Copyright (c) 2019-2020, Alex Earl # # 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. ARG WINDOWS_VERSION_TAG ARG TOOLS_WINDOWS_VERSION FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" AS jdk-core # $ProgressPreference: https://github.com/PowerShell/PowerShell/issues/2138#issuecomment-251261324 SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] ARG JAVA_VERSION RUN New-Item -ItemType Directory -Path C:\temp | Out-Null ; ` $javaMajorVersion = $env:JAVA_VERSION.substring(0,2) ; ` $msiUrl = 'https://api.adoptium.net/v3/installer/version/jdk-{0}/windows/x64/jdk/hotspot/normal/eclipse?project=jdk' -f $env:JAVA_VERSION ; ` Invoke-WebRequest $msiUrl -OutFile 'C:\temp\jdk.msi' ; ` $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList '/i', 'C:\temp\jdk.msi', '/L*V', 'C:\temp\OpenJDK.log', '/quiet', 'ADDLOCAL=FeatureEnvironment,FeatureJarFileRunWith,FeatureJavaHome', "INSTALLDIR=C:\openjdk-${javaMajorVersion}" -Wait -Passthru ; ` $proc.WaitForExit() ; ` Remove-Item -Path C:\temp -Recurse | Out-Null FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" ARG JAVA_HOME ENV JAVA_HOME=${JAVA_HOME} COPY --from=jdk-core $JAVA_HOME $JAVA_HOME SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] ARG user=jenkins ARG JENKINS_AGENT_WORK="C:/Users/${user}/Work" ENV JENKINS_AGENT_USER=${user} ENV JENKINS_AGENT_WORK=${JENKINS_AGENT_WORK} USER ContainerAdministrator # Install git ARG GIT_VERSION=2.54.0 ARG GIT_PATCH_VERSION=1 RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` # The patch "windows.1" always have a different URL than the subsequent patch (ZIP filename is different) if($env:GIT_PATCH_VERSION -eq 1) { $url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION); } ` else {$url = $('https://github.com/git-for-windows/git/releases/download/v{0}.windows.{1}/MinGit-{0}.{1}-64-bit.zip' -f $env:GIT_VERSION, $env:GIT_PATCH_VERSION)} ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest $url -OutFile 'mingit.zip' -UseBasicParsing ; ` Expand-Archive mingit.zip -DestinationPath c:\mingit ; ` Remove-Item mingit.zip -Force # Install git-lfs ARG GIT_LFS_VERSION=3.7.1 RUN $CurrentPath = (Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path).Path ; ` $NewPath = $CurrentPath + ';{0}\bin;C:\mingit\cmd' -f $env:JAVA_HOME ; ` Set-ItemProperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path -Value $NewPath ; ` [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` $url = $('https://github.com/git-lfs/git-lfs/releases/download/v{0}/git-lfs-windows-amd64-v{0}.zip' -f $env:GIT_LFS_VERSION) ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest $url -OutFile 'GitLfs.zip' -UseBasicParsing ; ` Expand-Archive GitLfs.zip -DestinationPath c:\mingit\mingw64\bin ; ` $gitLfsFolder = 'c:\mingit\mingw64\bin\git-lfs-{0}' -f $env:GIT_LFS_VERSION ; ` Move-Item -Path "${gitLfsFolder}\git-lfs.exe" -Destination c:\mingit\mingw64\bin\ ; ` Remove-Item -Path $gitLfsFolder -Recurse -Force ; ` Remove-Item GitLfs.zip -Force ; ` & C:\mingit\cmd\git.exe lfs install # Setup SSH server ARG OPENSSH_VERSION=10.0.0.0p2-Preview RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` $url = 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/{0}/OpenSSH-Win64.zip' -f $env:OPENSSH_VERSION ; ` Write-Host "Retrieving $url..." ; ` Invoke-WebRequest -Uri $url -OutFile C:/openssh.zip -UseBasicParsing ; ` Expand-Archive c:/openssh.zip 'C:/Program Files' ; ` Remove-Item C:/openssh.zip ; ` $env:PATH = '{0};{1}' -f $env:PATH,'C:\Program Files\OpenSSH-Win64' ; ` & 'C:/Program Files/OpenSSH-Win64/install-sshd.ps1' ; ` if(!(Test-Path 'C:\ProgramData\ssh')) { New-Item -Type Directory -Path 'C:\ProgramData\ssh' | Out-Null } ; ` Copy-Item 'C:\Program Files\OpenSSH-Win64\sshd_config_default' 'C:\ProgramData\ssh\sshd_config' ; ` $content = Get-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` $content | ForEach-Object { $_ -replace '#PermitRootLogin.*','PermitRootLogin no' ` -replace '#PasswordAuthentication.*','PasswordAuthentication no' ` -replace '#PermitEmptyPasswords.*','PermitEmptyPasswords no' ` -replace '#PubkeyAuthentication.*','PubkeyAuthentication yes' ` -replace '#SyslogFacility.*','SyslogFacility LOCAL0' ` -replace '#LogLevel.*','LogLevel INFO' ` -replace 'Match Group administrators','' ` -replace '(\s*)AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys','' ` } | ` Set-Content -Path "C:\ProgramData\ssh\sshd_config" ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'ChallengeResponseAuthentication no' ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value 'HostKeyAgent \\.\pipe\openssh-ssh-agent' ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value ('Match User {0}' -f $env:JENKINS_AGENT_USER) ; ` Add-Content -Path "C:\ProgramData\ssh\sshd_config" -Value (' AuthorizedKeysFile C:/Users/{0}/.ssh/authorized_keys' -f $env:JENKINS_AGENT_USER) ; ` New-Item -Path HKLM:\SOFTWARE -Name OpenSSH -Force | Out-Null ; ` New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType string -Force | Out-Null ; ` Remove-Item -Path "C:\ProgramData\ssh\ssh_host*_key*" COPY CreateProfile.psm1 C:/ # Create user and user directory RUN Import-Module -Force C:/CreateProfile.psm1 ; ` New-UserWithProfile -UserName $env:JENKINS_AGENT_USER -Description 'Jenkins Agent User' ; ` Remove-Item -Force C:/CreateProfile.psm1 VOLUME "${JENKINS_AGENT_WORK}" "C:/Users/${user}/AppData/Local/Temp" WORKDIR "${JENKINS_AGENT_WORK}" COPY setup-sshd.ps1 C:/ProgramData/Jenkins/setup-sshd.ps1 EXPOSE 22 LABEL ` org.opencontainers.image.vendor="Jenkins project" ` org.opencontainers.image.title="Official Jenkins SSH Agent Docker image" ` org.opencontainers.image.description="A Jenkins agent image which allows using SSH to establish the connection" ` org.opencontainers.image.url="https://www.jenkins.io/" ` org.opencontainers.image.source="https://github.com/jenkinsci/docker-ssh-agent" ` org.opencontainers.image.licenses="MIT" ENTRYPOINT ["powershell.exe", "-NoExit", "-Command", "& C:/ProgramData/Jenkins/setup-sshd.ps1"]