diff --git a/.github/workflows/update-dockerhub-description.yaml b/.github/workflows/update-dockerhub-description.yaml new file mode 100644 index 000000000..037b79c2c --- /dev/null +++ b/.github/workflows/update-dockerhub-description.yaml @@ -0,0 +1,19 @@ +name: Update Docker Hub Description +on: + release: + types: [ published ] + workflow_dispatch: + +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Update Docker Hub description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + enable-url-completion: true + short-description: ${{ github.event.repository.description }} + repository: jenkins/inbound-agent diff --git a/README_inbound-agent.md b/README_inbound-agent.md new file mode 100644 index 000000000..d6621f0d4 --- /dev/null +++ b/README_inbound-agent.md @@ -0,0 +1,111 @@ +# Docker image for inbound Jenkins agents + +[![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-inbound-agent?label=GitHub%20stars)](https://github.com/jenkinsci/docker-inbound-agent) +[![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/inbound-agent.svg)](https://hub.docker.com/r/jenkins/inbound-agent/) +[![GitHub release](https://img.shields.io/github/release/jenkinsci/docker-inbound-agent.svg?label=changelog)](https://github.com/jenkinsci/docker-inbound-agent/releases/latest) + +:exclamation: **Warning!** This image used to be published as [jenkinsci/jnlp-slave](https://hub.docker.com/r/jenkinsci/jnlp-slave/) and [jenkins/jnlp-slave](https://hub.docker.com/r/jenkins/jnlp-slave/). +These images are deprecated, use [jenkins/inbound-agent](https://hub.docker.com/r/jenkins/inbound-agent/). + +This is an image for [Jenkins](https://jenkins.io) agents using TCP or WebSockets to establish inbound connection to the Jenkins master. +This agent is powered by the [Jenkins Remoting library](https://github.com/jenkinsci/remoting), which version is being taken from the base [Docker Agent](https://github.com/jenkinsci/docker-agent/) image. + +See [Using Agents](https://www.jenkins.io/doc/book/using/using-agents/) for more info. + +## Configuring agents with this container image + +### Setup the agent on Jenkins + +1. Go to your Jenkins dashboard +2. Go to `Manage Jenkins` option in main menu +3. Go to `Nodes` item in `System Configuration` + ![image](images/screen-4.png) +4. Go to `New Node` option in side menu +5. Fill the Node(agent) name and select the type; (e.g. Name: agent1, Type: Permanent Agent) +6. Now fill the fields like remote root directory, labels, # of executors, etc. + * **`Launch method` is `Launch agent by connecting it to the controller`** + ![image](images/screen-1.png) +7. Press the `Save` button and the agent1 will be registered, but offline for the time being. Click on it. + ![image](images/screen-2.png) +8. You should now see the secret. Use the secret value to pass it to the argument of container, or set to `JENKINS_SECRET` as environment variable. + ![image](images/screen-3.png) + +### Running this container + +To run a Docker container + > **Note** + > Remember to replace the `` and `` for secret and agent name, which can be you can get(and set) from [above section](#Setup-the-agent-on-Jenkins). + > Your agent node should be possible to connect to Jenkins controller with agent port (not Jenkins server's port like 80, 443, 8080), which can be set in `Manage Jenkins` > `Security` > `Agent`. Default port is 50000. + + Linux agent: + + docker run --init jenkins/inbound-agent -url http://jenkins-server:port + Note: `--init` is necessary for correct subprocesses handling (zombie reaping) + + Windows agent: + + docker run jenkins/inbound-agent:windowsservercore-ltsc2019 -Url http://jenkins-server:port -Secret -Name + +To run a Docker container with [Work Directory](https://github.com/jenkinsci/remoting/blob/master/docs/workDir.md) + + Linux agent: + + docker run --init jenkins/inbound-agent -url http://jenkins-server:port -workDir=/home/jenkins/agent + + Windows agent: + + docker run jenkins/inbound-agent:windowsservercore-ltsc2019 -Url http://jenkins-server:port -WorkDir=C:/Jenkins/agent -Secret -Name + +Optional environment variables: + +* `JENKINS_JAVA_BIN`: Path to Java executable to use instead of the default in PATH or obtained from JAVA_HOME +* `JENKINS_JAVA_OPTS` : Java Options to use for the remoting process, otherwise obtained from JAVA_OPTS, **Warning** :exclamation: For more information on Windows usage, please see the **Windows Jenkins Java Opts** [section below](#windows-jenkins-java-opts). +* `JENKINS_URL`: url for the Jenkins server, can be used as a replacement to `-url` option, or to set alternate jenkins URL +* `JENKINS_TUNNEL`: (`HOST:PORT`) connect to this agent host and port instead of Jenkins server, assuming this one do route TCP traffic to Jenkins controller. Useful when when Jenkins runs behind a load balancer, reverse proxy, etc. +* `JENKINS_SECRET`: (use only if not set as an argument) the secret as shown on the controller after creating the agent +* `JENKINS_AGENT_NAME`: (use only if not set as an argument) the name of the agent, it should match the name you specified when creating the agent on the controller +* `JENKINS_AGENT_WORKDIR`: agent work directory, if not set by optional parameter `-workDir` +* `JENKINS_WEB_SOCKET`: `true` if the connection should be made via WebSocket rather than TCP +* `JENKINS_DIRECT_CONNECTION`: (`HOST:PORT`) Connect directly to this TCP agent port, skipping the HTTP(S) connection parameter download. +* `JENKINS_INSTANCE_IDENTITY`: The base64 encoded InstanceIdentity byte array of the Jenkins controller. When this is set, the agent skips connecting to an HTTP(S) port for connection info. +* `JENKINS_PROTOCOLS`: Specify the remoting protocols to attempt when `JENKINS_INSTANCE_IDENTITY` is provided. + +#### Example + +1. Enter the command above. + ![image](images/screen-5.png) +2. Check the Jenkins dashboard if the agent is connected well. + ![image](images/screen-6.png) + + +## Windows Jenkins Java Opts + +The processing of the JENKINS_JAVA_OPTS environment variable or -JenkinsJavaOpts command line parameter follow the [command parsing semantics of Powershell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.3). This means that if a parameter contains any characters that are part of an expression in Powershell, it will need to be surrounded by quotes. +For example: + +-XX:+PrintCommandLineFlags --show-version + +This would need to be escaped with quotes like this: + +"-XX:+PrintCommandLineFlags" --show-version + +Or another example: +-Dsome.property=some value --show-version + +This would need to be escaped like this: + +"-Dsome.property='some value'" --show-version + + +## Configuration specifics + +### Enabled JNLP protocols + +As of version 3.40-1 this image only supports the [JNLP4-connect](https://github.com/jenkinsci/remoting/blob/master/docs/protocols.md#jnlp4-connect) protocol. +Earlier, long-unsupported protocols have been removed. +As a result, Jenkins versions prior to 2.32 are no longer supported. + +### Amazon ECS + +Make sure your ECS container agent is [updated](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-update.html) before running. Older versions do not properly handle the entryPoint parameter. See the [entryPoint](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definitions) definition for more information. diff --git a/jenkins-agent b/jenkins-agent new file mode 100755 index 000000000..22025cd1b --- /dev/null +++ b/jenkins-agent @@ -0,0 +1,131 @@ +#!/usr/bin/env sh + +# The MIT License +# +# Copyright (c) 2015-2020, 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 jenkins-agent.sh [options] -url http://jenkins -secret [SECRET] -name [AGENT_NAME] +# Optional environment variables : +# * JENKINS_JAVA_BIN : Java executable to use instead of the default in PATH or obtained from JAVA_HOME +# * JENKINS_JAVA_OPTS : Java Options to use for the remoting process, otherwise obtained from JAVA_OPTS +# * JENKINS_TUNNEL : HOST:PORT for a tunnel to route TCP traffic to jenkins host, when jenkins can't be directly accessed over network +# * JENKINS_URL : alternate jenkins URL +# * JENKINS_SECRET : agent secret, if not set as an argument +# * JENKINS_AGENT_NAME : agent name, if not set as an argument +# * JENKINS_AGENT_WORKDIR : agent work directory, if not set by optional parameter -workDir +# * JENKINS_WEB_SOCKET: true if the connection should be made via WebSocket rather than TCP +# * JENKINS_DIRECT_CONNECTION: Connect directly to this TCP agent port, skipping the HTTP(S) connection parameter download. +# Value: ":" +# * JENKINS_INSTANCE_IDENTITY: The base64 encoded InstanceIdentity byte array of the Jenkins controller. When this is set, +# the agent skips connecting to an HTTP(S) port for connection info. +# * JENKINS_PROTOCOLS: Specify the remoting protocols to attempt when instanceIdentity is provided. + +if [ $# -eq 1 ] && [ "${1#-}" = "$1" ] ; then + + # if `docker run` only has one arguments and it is not an option as `-help`, we assume user is running alternate command like `bash` to inspect the image + exec "$@" + +else + + # if -tunnel is not provided, try env vars + case "$@" in + *"-tunnel "*) ;; + *) + if [ ! -z "$JENKINS_TUNNEL" ]; then + TUNNEL="-tunnel $JENKINS_TUNNEL" + fi ;; + esac + + # if -workDir is not provided, try env vars + if [ ! -z "$JENKINS_AGENT_WORKDIR" ]; then + case "$@" in + *"-workDir"*) echo "Warning: Work directory is defined twice in command-line arguments and the environment variable" ;; + *) + WORKDIR="-workDir $JENKINS_AGENT_WORKDIR" ;; + esac + fi + + if [ -n "$JENKINS_URL" ]; then + URL="-url $JENKINS_URL" + fi + + if [ -n "$JENKINS_NAME" ]; then + JENKINS_AGENT_NAME="$JENKINS_NAME" + fi + + if [ "$JENKINS_WEB_SOCKET" = true ]; then + WEB_SOCKET=-webSocket + fi + + if [ -n "$JENKINS_PROTOCOLS" ]; then + PROTOCOLS="-protocols $JENKINS_PROTOCOLS" + fi + + if [ -n "$JENKINS_DIRECT_CONNECTION" ]; then + DIRECT="-direct $JENKINS_DIRECT_CONNECTION" + fi + + if [ -n "$JENKINS_INSTANCE_IDENTITY" ]; then + INSTANCE_IDENTITY="-instanceIdentity $JENKINS_INSTANCE_IDENTITY" + fi + + if [ "$JENKINS_JAVA_BIN" ]; then + JAVA_BIN="$JENKINS_JAVA_BIN" + else + # if java home is defined, use it + JAVA_BIN="java" + if [ "$JAVA_HOME" ]; then + JAVA_BIN="$JAVA_HOME/bin/java" + fi + fi + + if [ "$JENKINS_JAVA_OPTS" ]; then + JAVA_OPTIONS="$JENKINS_JAVA_OPTS" + else + # if JAVA_OPTS is defined, use it + if [ "$JAVA_OPTS" ]; then + JAVA_OPTIONS="$JAVA_OPTS" + fi + fi + + # if both required options are defined, do not pass the parameters + if [ -n "$JENKINS_SECRET" ]; then + case "$@" in + *"${JENKINS_SECRET}"*) echo "Warning: SECRET is defined twice in command-line arguments and the environment variable" ;; + *) + SECRET="-secret ${JENKINS_SECRET}" ;; + esac + fi + + if [ -n "$JENKINS_AGENT_NAME" ]; then + case "$@" in + *"${JENKINS_AGENT_NAME}"*) echo "Warning: AGENT_NAME is defined twice in command-line arguments and the environment variable" ;; + *) + AGENT_NAME="-name ${JENKINS_AGENT_NAME}" ;; + esac + fi + + #TODO: Handle the case when the command-line and Environment variable contain different values. + #It is fine it blows up for now since it should lead to an error anyway. + + exec $JAVA_BIN $JAVA_OPTIONS -jar /usr/share/jenkins/agent.jar $SECRET $AGENT_NAME $TUNNEL $URL $WORKDIR $WEB_SOCKET $DIRECT $PROTOCOLS $INSTANCE_IDENTITY "$@" + +fi diff --git a/jenkins-agent.ps1 b/jenkins-agent.ps1 new file mode 100644 index 000000000..6dbf62f8b --- /dev/null +++ b/jenkins-agent.ps1 @@ -0,0 +1,154 @@ +# 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. + +[CmdletBinding()] +Param( + $Cmd = '', # this must be specified explicitly + $Url = $( if([System.String]::IsNullOrWhiteSpace($Cmd) -and [System.String]::IsNullOrWhiteSpace($env:JENKINS_URL) -and [System.String]::IsNullOrWhiteSpace($env:JENKINS_DIRECT_CONNECTION)) { throw ("Url is required") } else { '' } ), + [Parameter(Position=0)]$Secret = $( if([System.String]::IsNullOrWhiteSpace($Cmd) -and [System.String]::IsNullOrWhiteSpace($env:JENKINS_SECRET)) { throw ("Secret is required") } else { '' } ), + [Parameter(Position=1)]$Name = $( if([System.String]::IsNullOrWhiteSpace($Cmd) -and [System.String]::IsNullOrWhiteSpace($env:JENKINS_AGENT_NAME)) { throw ("Name is required") } else { '' } ), + $Tunnel = '', + $WorkDir = '', + [switch] $WebSocket = $false, + $DirectConnection = '', + $InstanceIdentity = '', + $Protocols = '', + $JenkinsJavaBin = '', + $JavaHome = $env:JAVA_HOME, + $JenkinsJavaOpts = '' +) + +# Usage jenkins-agent.ps1 [options] -Url http://jenkins -Secret [SECRET] -Name [AGENT_NAME] +# Optional environment variables : +# * JENKINS_JAVA_BIN : Java executable to use instead of the default in PATH or obtained from JAVA_HOME +# * JENKINS_JAVA_OPTS : Java Options to use for the remoting process, otherwise obtained from JAVA_OPTS +# * JENKINS_TUNNEL : HOST:PORT for a tunnel to route TCP traffic to jenkins host, when jenkins can't be directly accessed over network +# * JENKINS_URL : alternate jenkins URL +# * JENKINS_SECRET : agent secret, if not set as an argument +# * JENKINS_AGENT_NAME : agent name, if not set as an argument +# * JENKINS_AGENT_WORKDIR : agent work directory, if not set by optional parameter -workDir +# * JENKINS_WEB_SOCKET : true if the connection should be made via WebSocket rather than TCP +# * JENKINS_DIRECT_CONNECTION: Connect directly to this TCP agent port, skipping the HTTP(S) connection parameter download. +# Value: ":" +# * JENKINS_INSTANCE_IDENTITY: The base64 encoded InstanceIdentity byte array of the Jenkins controller. When this is set, +# the agent skips connecting to an HTTP(S) port for connection info. +# * JENKINS_PROTOCOLS: Specify the remoting protocols to attempt when instanceIdentity is provided. + +if(![System.String]::IsNullOrWhiteSpace($Cmd)) { + Invoke-Expression "$Cmd" +} else { + + # this maps the variable name from the CmdletBinding to environment variables + $ParamMap = @{ + 'JenkinsJavaBin' = 'JENKINS_JAVA_BIN'; + 'JenkinsJavaOpts' = 'JENKINS_JAVA_OPTS'; + 'Tunnel' = 'JENKINS_TUNNEL'; + 'Url' = 'JENKINS_URL'; + 'Secret' = 'JENKINS_SECRET'; + 'Name' = 'JENKINS_AGENT_NAME'; + 'WorkDir' = 'JENKINS_AGENT_WORKDIR'; + 'WebSocket' = 'JENKINS_WEB_SOCKET'; + 'DirectConnection' = 'JENKINS_DIRECT_CONNECTION'; + 'InstanceIdentity' = 'JENKINS_INSTANCE_IDENTITY'; + 'Protocols' = 'JENKINS_PROTOCOLS'; + } + + # this does some trickery to update the variable from the CmdletBinding + # with the value of the + foreach($p in $ParamMap.Keys) { + $var = Get-Variable $p + $envVar = Get-ChildItem -Path "env:$($ParamMap[$p])" -ErrorAction 'SilentlyContinue' + + if(($null -ne $envVar) -and ((($envVar.Value -is [System.String]) -and (![System.String]::IsNullOrWhiteSpace($envVar.Value))) -or ($null -ne $envVar.Value))) { + if(($null -ne $var) -and ((($var.Value -is [System.String]) -and (![System.String]::IsNullOrWhiteSpace($var.Value))))) { + Write-Warning "${p} is defined twice; in command-line arguments (-${p}) and in the environment variable ${envVar.Name}" + } + if($var.Value -is [System.String]) { + $var.Value = $envVar.Value + } elseif($var.Value -is [System.Management.Automation.SwitchParameter]) { + $var.Value = [bool]$envVar.Value + } + } + if($var.Value -is [System.String]) { + $var.Value = $var.Value.Trim() + } + } + + $AgentArguments = @() + + if(![System.String]::IsNullOrWhiteSpace($JenkinsJavaOpts)) { + # this magic will basically process the $JenkinsJavaOpts like a command line + # and split into an array, the command line processing follows the PowerShell + # commnd line processing, which means for things like -Dsomething.something=something, + # you need to quote the string like this: "-Dsomething.something=something" or else it + # will get parsed incorrectly. + $AgentArguments += Invoke-Expression "echo $JenkinsJavaOpts" + } + + $AgentArguments += @("-jar", "C:/ProgramData/Jenkins/agent.jar") + $AgentArguments += @("-secret", $Secret) + $AgentArguments += @("-name", $Name) + + if(![System.String]::IsNullOrWhiteSpace($Tunnel)) { + $AgentArguments += @("-tunnel", "`"$Tunnel`"") + } + + if(![System.String]::IsNullOrWhiteSpace($WorkDir)) { + $AgentArguments += @("-workDir", "`"$WorkDir`"") + } else { + $AgentArguments += @("-workDir", "`"C:/Users/jenkins/Work`"") + } + + if($WebSocket) { + $AgentArguments += @("-webSocket") + } + + if(![System.String]::IsNullOrWhiteSpace($Url)) { + $AgentArguments += @("-url", "`"$Url`"") + } + + if(![System.String]::IsNullOrWhiteSpace($DirectConnection)) { + $AgentArguments += @('-direct', $DirectConnection) + } + + if(![System.String]::IsNullOrWhiteSpace($InstanceIdentity)) { + $AgentArguments += @('-instanceIdentity', $InstanceIdentity) + } + + if(![System.String]::IsNullOrWhiteSpace($Protocols)) { + $AgentArguments += @('-protocols', $Protocols) + } + + if(![System.String]::IsNullOrWhiteSpace($JenkinsJavaBin)) { + $JAVA_BIN = $JenkinsJavaBin + } else { + # if java home is defined, use it + $JAVA_BIN = "java.exe" + if (![System.String]::IsNullOrWhiteSpace($JavaHome)) { + $JAVA_BIN = "$JavaHome/bin/java.exe" + } + } + + #TODO: Handle the case when the command-line and Environment variable contain different values. + #It is fine it blows up for now since it should lead to an error anyway. + Start-Process -FilePath $JAVA_BIN -Wait -NoNewWindow -ArgumentList $AgentArguments +} diff --git a/tests/inbound-agent.Tests.ps1 b/tests/inbound-agent.Tests.ps1 new file mode 100644 index 000000000..a8e5d5d38 --- /dev/null +++ b/tests/inbound-agent.Tests.ps1 @@ -0,0 +1,268 @@ +Import-Module -DisableNameChecking -Force $PSScriptRoot/test_helpers.psm1 + +$global:AGENT_IMAGE = Get-EnvOrDefault 'AGENT_IMAGE' '' +$global:BUILD_CONTEXT = Get-EnvOrDefault 'BUILD_CONTEXT' '' +$global:version = Get-EnvOrDefault 'VERSION' '' + +$items = $global:AGENT_IMAGE.Split("-") + +# Remove the 'jdk' prefix (3 first characters) +$global:JAVAMAJORVERSION = $items[0].Remove(0,3) +$global:WINDOWSFLAVOR = $items[1] +$global:WINDOWSVERSIONTAG = $items[2] + +# TODO: make this name unique for concurency +$global:CONTAINERNAME = 'pester-jenkins-inbound-agent-{0}' -f $global:AGENT_IMAGE + +$global:CONTAINERSHELL="powershell.exe" +if($global:WINDOWSFLAVOR -eq 'nanoserver') { + $global:CONTAINERSHELL = "pwsh.exe" +} + +# # Uncomment to help debugging when working on this script +# Write-Host "= DEBUG: global vars" +# Get-Variable -Scope Global | ForEach-Object { Write-Host "$($_.Name) = $($_.Value)" } +# Write-Host "= DEBUG: env vars" +# Get-ChildItem Env: | ForEach-Object { Write-Host "$($_.Name) = $($_.Value)" } + +Cleanup($global:CONTAINERNAME) +Cleanup("nmap") +CleanupNetwork("jnlp-network") + +BuildNcatImage($global:WINDOWSVERSIONTAG) + +Describe "[$global:AGENT_IMAGE] build image" { + It 'builds image' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg version=${global:version} --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg JAVA_MAJOR_VERSION=${global:JAVAMAJORVERSION} --tag=${global:AGENT_IMAGE} --file ./windows/${global:WINDOWSFLAVOR}/Dockerfile ${global:BUILD_CONTEXT}" + $exitCode | Should -Be 0 + } +} + +Describe "[$global:AGENT_IMAGE] check default user account" { + BeforeAll { + docker run --detach --tty --name "$global:CONTAINERNAME" "$global:AGENT_IMAGE" -Cmd "$global:CONTAINERSHELL" + $LASTEXITCODE | Should -Be 0 + Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue + } + + It 'has a password that never expires' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if((net user jenkins | Select-String -Pattern 'Password expires') -match 'Never') { exit 0 } else { net user jenkins ; exit -1 }`"" + $exitCode | Should -Be 0 + } + + It 'has password policy of "not required"' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if((net user jenkins | Select-String -Pattern 'Password required') -match 'No') { exit 0 } else { net user jenkins ; exit -1 }`"" + $exitCode | Should -Be 0 + } + + AfterAll { + Cleanup($global:CONTAINERNAME) + } +} + +Describe "[$global:AGENT_IMAGE] image has jenkins-agent.ps1 in the correct location" { + BeforeAll { + docker run --detach --tty --name "$global:CONTAINERNAME" "$global:AGENT_IMAGE" -Cmd "$global:CONTAINERSHELL" + $LASTEXITCODE | Should -Be 0 + Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue + } + + It 'has jenkins-agent.ps1 in C:/ProgramData/Jenkins' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -C `"if(Test-Path 'C:/ProgramData/Jenkins/jenkins-agent.ps1') { exit 0 } else { exit 1 }`"" + $exitCode | Should -Be 0 + } + + AfterAll { + Cleanup($global:CONTAINERNAME) + } +} + +Describe "[$global:AGENT_IMAGE] image starts jenkins-agent.ps1 correctly (slow test)" { + It 'connects to the nmap container' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "network create --driver nat jnlp-network" + # Launch the netcat utility, listening at port 5000 for 30 sec + # bats will capture the output from netcat and compare the first line + # of the header of the first HTTP request with the expected one + $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name nmap --network=jnlp-network nmap:latest ncat.exe -w 30 -l 5000" + $exitCode | Should -Be 0 + Is-ContainerRunning "nmap" | Should -BeTrue + + # get the ip address of the nmap container + $exitCode, $stdout, $stderr = Run-Program 'docker' "inspect --format `"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}`" nmap" + $exitCode | Should -Be 0 + $nmap_ip = $stdout.Trim() + + # run Jenkins agent which tries to connect to the nmap container at port 5000 + $secret = "aaa" + $name = "bbb" + $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --network=jnlp-network --name $global:CONTAINERNAME $global:AGENT_IMAGE -Url http://${nmap_ip}:5000 $secret $name" + $exitCode | Should -Be 0 + Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue + + $exitCode, $stdout, $stderr = Run-Program 'docker' 'wait nmap' + $exitCode, $stdout, $stderr = Run-Program 'docker' 'logs nmap' + $exitCode | Should -Be 0 + $stdout | Should -Match "GET /tcpSlaveAgentListener/ HTTP/1.1`r" + } + + AfterAll { + Cleanup($global:CONTAINERNAME) + Cleanup("nmap") + CleanupNetwork("jnlp-network") + } +} + +Describe "[$global:AGENT_IMAGE] custom build args" { + BeforeAll { + Push-Location -StackName 'agent' -Path "$PSScriptRoot/.." + # Old version used to test overriding the build arguments. + # This old version must have the same tag suffixes as the current windows images (`-jdk17-nanoserver` etc.), and the same Windows version (2019, 2022, etc.) + $TEST_VERSION = "3148.v532a_7e715ee3" + $PARENT_IMAGE_VERSION_SUFFIX = "12" + $ARG_TEST_VERSION = "${TEST_VERSION}-${PARENT_IMAGE_VERSION_SUFFIX}" + $customImageName = "custom-${global:AGENT_IMAGE}" + } + + It 'builds image with arguments' { + $exitCode, $stdout, $stderr = Run-Program 'docker' "build --build-arg version=${ARG_TEST_VERSION} --build-arg `"WINDOWS_VERSION_TAG=${global:WINDOWSVERSIONTAG}`" --build-arg JAVA_MAJOR_VERSION=${global:JAVAMAJORVERSION} --build-arg WINDOWS_FLAVOR=${global:WINDOWSFLAVOR} --build-arg CONTAINER_SHELL=${global:CONTAINERSHELL} --tag=${customImageName} --file=./windows/${global:WINDOWSFLAVOR}/Dockerfile ${global:BUILD_CONTEXT}" + $exitCode | Should -Be 0 + + $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name $global:CONTAINERNAME $customImageName -Cmd $global:CONTAINERSHELL" + $exitCode | Should -Be 0 + Is-ContainerRunning "$global:CONTAINERNAME" | Should -BeTrue + } + + It "has the correct agent.jar version" { + $exitCode, $stdout, $stderr = Run-Program 'docker' "exec $global:CONTAINERNAME $global:CONTAINERSHELL -c `"java -cp C:/ProgramData/Jenkins/agent.jar hudson.remoting.jnlp.Main -version`"" + $exitCode | Should -Be 0 + $stdout | Should -Match $TEST_VERSION + } + + AfterAll { + Cleanup($global:CONTAINERNAME) + Pop-Location -StackName 'agent' + } +} + +# === TODO: uncomment test later, see error log below +# === this test passes on a Windows machine + +# Describe "[$global:AGENT_IMAGE] passing JVM options (slow test)" { +# It "shows the java version ${global:JAVAMAJORVERSION} with --show-version" { +# $exitCode, $stdout, $stderr = Run-Program 'docker' "network create --driver nat jnlp-network" +# # Launch the netcat utility, listening at port 5000 for 30 sec +# # bats will capture the output from netcat and compare the first line +# # of the header of the first HTTP request with the expected one +# $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --name nmap --network=jnlp-network nmap:latest ncat.exe -w 30 -l 5000" +# $exitCode | Should -Be 0 +# Is-ContainerRunning "nmap" | Should -BeTrue + +# # get the ip address of the nmap container +# $exitCode, $stdout, $stderr = Run-Program 'docker' "inspect --format `"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}`" nmap" +# $exitCode | Should -Be 0 +# $nmap_ip = $stdout.Trim() + +# # run Jenkins agent which tries to connect to the nmap container at port 5000 +# $secret = "aaa" +# $name = "bbb" +# $exitCode, $stdout, $stderr = Run-Program 'docker' "run --detach --tty --network=jnlp-network --name $global:CONTAINERNAME $global:AGENT_IMAGE -Url http://${nmap_ip}:5000 -JenkinsJavaOpts `"--show-version`" $secret $name" +# $exitCode | Should -Be 0 +# Is-ContainerRunning $global:CONTAINERNAME | Should -BeTrue +# $exitCode, $stdout, $stderr = Run-Program 'docker' "logs $global:CONTAINERNAME" +# $exitCode | Should -Be 0 +# $stdout | Should -Match "OpenJDK Runtime Environment Temurin-${global:JAVAMAJORVERSION}" +# } + +# AfterAll { +# Cleanup($global:CONTAINERNAME) +# Cleanup("nmap") +# CleanupNetwork("jnlp-network") +# } +# } + + +# === Corresponding error log: + +# Running tests from 'inboundAgent.Tests.ps1' +# Describing [jdk17-windowsservercore-1809] build image +# cmd = docker, params = build --build-arg version=3148.v532a_7e715ee3-3 --build-arg "WINDOWS_VERSION_TAG=1809" --build-arg JAVA_MAJOR_VERSION=17 --tag=jdk17-windowsservercore-1809 --file ./windows/windowsservercore/Dockerfile C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396 +# [+] builds image 572ms (378ms|195ms) +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 + +# Describing [jdk17-windowsservercore-1809] check default user account +# cmd = docker, params = exec pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 powershell.exe -C "if((net user jenkins | Select-String -Pattern 'Password expires') -match 'Never') { exit 0 } else { net user jenkins ; exit -1 }" +# [+] has a password that never expires 4.55s (4.55s|5ms) +# cmd = docker, params = exec pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 powershell.exe -C "if((net user jenkins | Select-String -Pattern 'Password required') -match 'No') { exit 0 } else { net user jenkins ; exit -1 }" +# [+] has password policy of "not required" 2.74s (2.73s|3ms) +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 + +# Describing [jdk17-windowsservercore-1809] image has jenkins-agent.ps1 in the correct location +# cmd = docker, params = exec pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 powershell.exe -C "if(Test-Path 'C:/ProgramData/Jenkins/jenkins-agent.ps1') { exit 0 } else { exit 1 }" +# [+] has jenkins-agent.ps1 in C:/ProgramData/Jenkins 4.35s (4.27s|85ms) + +# Describing [jdk17-windowsservercore-1809] image starts jenkins-agent.ps1 correctly (slow test) +# cmd = docker, params = network create --driver nat jnlp-network +# cmd = docker, params = run --detach --tty --name nmap --network=jnlp-network nmap:latest ncat.exe -w 30 -l 5000 +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" nmap +# cmd = docker, params = inspect --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" nmap +# cmd = docker, params = run --detach --tty --network=jnlp-network --name pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 jdk17-windowsservercore-1809 -Url http://172.23.176.67:5000 aaa bbb +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 +# cmd = docker, params = wait nmap +# cmd = docker, params = logs nmap +# [+] connects to the nmap container 89.43s (89.43s|8ms) + +# Describing [jdk17-windowsservercore-1809] custom build args +# cmd = docker, params = build --build-arg version=3148.v532a_7e715ee3-3 --build-arg "WINDOWS_VERSION_TAG=1809" --build-arg JAVA_MAJOR_VERSION=17 --build-arg WINDOWS_FLAVOR=windowsservercore --build-arg CONTAINER_SHELL=powershell.exe --tag=custom-jdk17-windowsservercore-1809 --file=./windows/windowsservercore/Dockerfile C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396 +# cmd = docker, params = run --detach --tty --name pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 custom-jdk17-windowsservercore-1809 -Cmd powershell.exe +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 +# [+] builds image with arguments 11.3s (11.3s|5ms) +# cmd = docker, params = exec pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 powershell.exe -c "java -cp C:/ProgramData/Jenkins/agent.jar hudson.remoting.jnlp.Main -version" +# [+] has the correct agent.jar version 2.25s (2.25s|5ms) + +# Describing [jdk17-windowsservercore-1809] passing JVM options (slow test) +# cmd = docker, params = network create --driver nat jnlp-network +# cmd = docker, params = run --detach --tty --name nmap --network=jnlp-network nmap:latest ncat.exe -w 30 -l 5000 +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" nmap +# cmd = docker, params = inspect --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" nmap +# cmd = docker, params = run --detach --tty --network=jnlp-network --name pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 jdk17-windowsservercore-1809 -Url http://172.23.254.19:5000 -JenkinsJavaOpts "--show-version" aaa bbb +# cmd = docker.exe, params = inspect -f "{{.State.Running}}" pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 +# cmd = docker, params = logs pester-jenkins-inbound-agent-jdk17-windowsservercore-1809 +# [-] shows the java version 17 with --show-version 24.36s (24.35s|11ms) +# Expected regular expression 'OpenJDK Runtime Environment Temurin-17' to match ' +# ', but it did not match. +# at $stdout | Should -Match "OpenJDK Runtime Environment Temurin-${global:JAVAMAJORVERSION}", C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396\tests\inboundAgent.Tests.ps1:173 +# at , C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396\tests\inboundAgent.Tests.ps1:173 +# Tests completed in 219.22s +# Tests Passed: 7, Failed: 1, Skipped: 0 NotRun: 0 +# System.Management.Automation.MethodInvocationException: Exception calling "WriteAttributeString" with "2" argument(s): "'exadecimal value 0x1B, is an invalid character." ---> System.ArgumentException: 'exadecimal value 0x1B, is an invalid character. +# at System.Xml.XmlUtf8RawTextWriter.InvalidXmlChar(Int32 ch, Byte* pDst, Boolean entitize) +# at System.Xml.XmlUtf8RawTextWriter.WriteAttributeTextBlock(Char* pSrc, Char* pSrcEnd) +# at System.Xml.XmlUtf8RawTextWriter.WriteString(String text) +# at System.Xml.XmlWellFormedWriter.WriteString(String text) +# at System.Xml.XmlWriter.WriteAttributeString(String localName, String value) +# at CallSite.Target(Closure , CallSite , XmlWriter , String , Object ) +# --- End of inner exception stack trace --- +# at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception) +# at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame) +# at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) +# at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame) +# at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame) +# at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0) +# at System.Management.Automation.PSScriptCmdlet.RunClause(Action`1 clause, Object dollarUnderbar, Object inputToProcess) +# at System.Management.Automation.PSScriptCmdlet.DoEndProcessing() +# at System.Management.Automation.CommandProcessorBase.Complete() +# at Write-JUnitTestCaseMessageElements, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16460 +# at Write-JUnitTestCaseAttributes, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16452 +# at Write-JUnitTestCaseElements, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16424 +# at Write-JUnitTestSuiteElements, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16385 +# at Write-JUnitReport, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16269 +# at Export-XmlReport, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 16005 +# at Export-PesterResults, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 15863 +# at Invoke-Pester, C:\Program Files\WindowsPowerShell\Modules\Pester\5.3.3\Pester.psm1: line 5263 +# at Test-Image, C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396\build.ps1: line 130 +# at , C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396\build.ps1: line 176 +# at , C:\Jenkins\agent\workspace\ging_docker-inbound-agent_PR-396@tmp\durable-513b70db\powershellScript.ps1: line 1 +# at , : line 1 +# at , : line 1 + +# === end of error log diff --git a/tests/netcat-helper/Dockerfile b/tests/netcat-helper/Dockerfile new file mode 100644 index 000000000..6313e4a4e --- /dev/null +++ b/tests/netcat-helper/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine:3.11 + +RUN apk update --no-cache \ + && apk add --no-cache \ + coreutils \ + netcat-openbsd diff --git a/tests/netcat-helper/Dockerfile-windows b/tests/netcat-helper/Dockerfile-windows new file mode 100644 index 000000000..38e3a09d6 --- /dev/null +++ b/tests/netcat-helper/Dockerfile-windows @@ -0,0 +1,40 @@ +# 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. + +# Available tags: https://mcr.microsoft.com/v2/windows/servercore/tags/list +ARG WINDOWS_VERSION_TAG=1809 +FROM mcr.microsoft.com/windows/servercore:"${WINDOWS_VERSION_TAG}" + +SHELL ["powershell.exe", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +ARG NMAP_VERSION=7.80 +ENV NMAP_VERSION $NMAP_VERSION + +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` + $url = $('https://nmap.org/dist/nmap-{0}-setup.exe' -f $env:NMAP_VERSION) ; ` + Write-Host "Retrieving $url..." ; ` + Invoke-WebRequest $url -OutFile 'nmap-install.exe' -UseBasicParsing ; ` + $proc = Start-Process "C:\nmap-install.exe" -PassThru -ArgumentList '/S' ; ` + $proc.WaitForExit() ; ` + Remove-Item -Path nmap-install.exe diff --git a/tests/tests_inbound-agent.bats b/tests/tests_inbound-agent.bats new file mode 100755 index 000000000..30dc0bbab --- /dev/null +++ b/tests/tests_inbound-agent.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bats + +AGENT_CONTAINER=bats-jenkins-jnlp-agent + +load test_helpers + +buildNetcatImage + +SUT_IMAGE="$(get_sut_image)" + +@test "[${SUT_IMAGE}] image has installed jenkins-agent in PATH" { + local sut_cid + sut_cid="$(docker run -d -it -P "${SUT_IMAGE}" /bin/bash)" + + is_agent_container_running "${sut_cid}" + + run docker exec "${sut_cid}" which jenkins-agent + [ "/usr/local/bin/jenkins-agent" = "${lines[0]}" ] + + run docker exec "${sut_cid}" which jenkins-agent + [ "/usr/local/bin/jenkins-agent" = "${lines[0]}" ] + + cleanup "${sut_cid}" +} + +@test "[${SUT_IMAGE}] image starts jenkins-agent correctly (slow test)" { + local netcat_cid sut_cid + # Spin off a helper image which launches the netcat utility, listening at port 5000 for 30 sec + netcat_cid="$(docker run -d -it netcat-helper:latest /bin/sh -c "timeout 30s nc -l 5000")" + + # Run jenkins agent which tries to connect to the netcat-helper container at port 5000 + sut_cid="$(docker run -d --link "${netcat_cid}" "${SUT_IMAGE}" -url "http://${netcat_cid}:5000" aaa bbb)" + + # Wait for the whole process to take place (in resource-constrained environments it can take 100s of milliseconds) + sleep 5 + + # Capture the logs output from netcat and check the header of the first HTTP request with the expected one + run docker logs "${netcat_cid}" + echo "${output}" | grep 'GET /tcpSlaveAgentListener/ HTTP/1.1' + + cleanup "${netcat_cid}" + cleanup "${sut_cid}" +} + +@test "[${SUT_IMAGE}] use build args correctly" { + cd "${BATS_TEST_DIRNAME}"/.. || false + + local TEST_VERSION PARENT_IMAGE_VERSION_SUFFIX ARG_TEST_VERSION TEST_USER sut_image sut_cid + + # Old version used to test overriding the build arguments. + # This old version must have the same tag suffixes as the ones defined in the docker-bake file (`-jdk17`, `jdk11`, etc.) + TEST_VERSION="3180.v3dd999d24861" + PARENT_IMAGE_VERSION_SUFFIX="2" + + ARG_TEST_VERSION="${TEST_VERSION}-${PARENT_IMAGE_VERSION_SUFFIX}" + TEST_USER="root" + + sut_image="${SUT_IMAGE}-tests-${BATS_TEST_NUMBER}" + + docker buildx bake \ + --set "${IMAGE}".args.version="${ARG_TEST_VERSION}" \ + --set "${IMAGE}".args.user="${TEST_USER}" \ + --set "${IMAGE}".platform=linux/"${ARCH}" \ + --set "${IMAGE}".tags="${sut_image}" \ + --load \ + "${IMAGE}" + + sut_cid="$(docker run -d -it --name "${AGENT_CONTAINER}" -P "${sut_image}" /bin/sh)" + + is_agent_container_running "${sut_cid}" + + run docker exec "${sut_cid}" sh -c "java -cp /usr/share/jenkins/agent.jar hudson.remoting.jnlp.Main -version" + [ "${TEST_VERSION}" = "${lines[0]}" ] + + run docker exec "${AGENT_CONTAINER}" sh -c "id -u -n ${TEST_USER}" + [ "${TEST_USER}" = "${lines[0]}" ] + + cleanup "${sut_cid}" +}