From d3f226d8a8c12ebc63f09fd8edb4e21e973812ef Mon Sep 17 00:00:00 2001 From: Lionel ROUBEYRIE Date: Tue, 24 Dec 2024 15:40:20 +0100 Subject: [PATCH 01/30] Add rootless support --- .../docker/workflow/client/DockerClient.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java index b36b6fd91..323f18e93 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -31,6 +31,7 @@ import hudson.FilePath; import hudson.Launcher; import hudson.model.Node; +import hudson.model.TaskListener; import hudson.util.ArgumentListBuilder; import hudson.util.VersionNumber; import java.io.BufferedReader; @@ -318,6 +319,12 @@ private LaunchResult launch(@NonNull EnvVars launchEnv, boolean quiet, FilePath return result; } + private String executeCommand(String... command) throws IOException, InterruptedException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + launcher.launch().cmds(command).quiet(true).stdout(output).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()); + return output.toString(Charset.defaultCharset()).trim(); + } + /** * Who is executing this {@link DockerClient} instance. * @@ -328,15 +335,28 @@ public String whoAmI() throws IOException, InterruptedException { // Windows does not support username return ""; } - ByteArrayOutputStream userId = new ByteArrayOutputStream(); - launcher.launch().cmds("id", "-u").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()); - ByteArrayOutputStream groupId = new ByteArrayOutputStream(); - launcher.launch().cmds("id", "-g").quiet(true).stdout(groupId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()); + TaskListener listener = launcher.getListener(); + final String rootlessId = "0:0"; + // First, check if under the hood it's Podman or Docker + String engine = executeCommand("docker", "--version"); + if (engine.toLowerCase().contains("podman")) { + listener.getLogger().println("Container engine is Podman with build in rootless mode"); + return rootlessId; + } + else { + String rootless = executeCommand("docker", "info", "-f", "{{.SecurityOptions}}" ); + if (rootless.toLowerCase().contains("rootless")) { + listener.getLogger().println("Container engine is Docker with rootless mode"); + return rootlessId; + } + } - final String charsetName = Charset.defaultCharset().name(); - return String.format("%s:%s", userId.toString(charsetName).trim(), groupId.toString(charsetName).trim()); + // Else not rootless, return the current user/group ids + String userId = executeCommand("id", "-u"); + String groupId = executeCommand("id", "-g"); + return String.format("%s:%s", userId, groupId); } private static final Pattern hostnameMount = Pattern.compile("/containers/([a-z0-9]{64})/hostname"); From 88284a29a7a9039cb29dee7d8e10e405a996c788 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 1 Nov 2024 12:20:52 -0400 Subject: [PATCH 02/30] Clarifying password masking in `RegistryEndpointStepTest` --- .../workflow/RegistryEndpointStepTest.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java index 60c7e0f9a..4432670da 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -72,12 +72,14 @@ import java.util.Set; import java.util.logging.Level; import org.jenkinsci.plugins.structs.describable.DescribableModel; +import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.LoggerRule; public class RegistryEndpointStepTest { @Rule public JenkinsRule r = new JenkinsRule(); @Rule public LoggerRule logging = new LoggerRule(); + @Rule public BuildWatcher bw = new BuildWatcher(); @Issue("JENKINS-51395") @Test public void configRoundTrip() throws Exception { @@ -123,19 +125,21 @@ public class RegistryEndpointStepTest { public void stepExecutionWithCredentials() throws Exception { assumeNotWindows(); - IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "pass"); + IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); WorkflowJob p = r.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "node {\n" + - " mockDockerLoginWithEcho {\n" + + " mockDockerLogin {\n" + " withDockerRegistry(url: 'https://my-reg:1234', credentialsId: 'registryCreds') {\n" + " }\n" + " }\n" + "}", true)); WorkflowRun b = r.buildAndAssertSuccess(p); - r.assertLogContains("docker login -u me -p pass https://my-reg:1234", r.assertBuildStatusSuccess(r.waitForCompletion(b))); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b); + r.assertLogNotContains("s3cr3t", b); } @Test @@ -151,11 +155,11 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Excep .grant(Item.CONFIGURE).everywhere().to("alice"); r.getInstance().setAuthorizationStrategy(auth); - IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "pass"); + IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); String script = "node {\n" + - " mockDockerLoginWithEcho {\n" + + " mockDockerLogin {\n" + " withDockerRegistry(url: 'https://my-reg:1234', credentialsId: 'registryCreds') {\n" + " }\n" + " }\n" + @@ -172,16 +176,17 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Excep // Alice has Credentials.USE_ITEM permission and should be able to use the credential. WorkflowRun b1 = r.buildAndAssertSuccess(p1); - r.assertLogContains("docker login -u me -p pass https://my-reg:1234", b1); + r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b1); + r.assertLogNotContains("s3cr3t", b1); // Bob does not have Credentials.USE_ITEM permission and should not be able to use the credential. r.assertBuildStatus(Result.FAILURE, p2.scheduleBuild2(0)); } - public static class MockLauncherWithEchoStep extends Step { + public static class MockLauncherStep extends Step { @DataBoundConstructor - public MockLauncherWithEchoStep() {} + public MockLauncherStep() {} @Override public StepExecution start(StepContext stepContext) { @@ -191,9 +196,9 @@ public StepExecution start(StepContext stepContext) { public static class Execution extends StepExecution { private static final long serialVersionUID = 1; - private final transient MockLauncherWithEchoStep step; + private final transient MockLauncherStep step; - Execution(MockLauncherWithEchoStep step, StepContext context) { + Execution(MockLauncherStep step, StepContext context) { super(context); this.step = step; } @@ -215,7 +220,7 @@ private static class Decorator extends LauncherDecorator implements Serializable private static final long serialVersionUID = 1; @NonNull @Override public Launcher decorate(@NonNull Launcher launcher, @NonNull Node node) { - return launcher.decorateByPrefix("echo"); + return launcher.decorateByPrefix("true"); } } @TestExtension public static class DescriptorImpl extends StepDescriptor { @@ -226,11 +231,11 @@ public Set> getRequiredContext() { } @Override public String getFunctionName() { - return "mockDockerLoginWithEcho"; + return "mockDockerLogin"; } @NonNull @Override public String getDisplayName() { - return "Mock Docker Login with Echo"; + return "Mock Docker Login"; } @Override public boolean takesImplicitBlockArgument() { return true; From a5fb32f3766072681d520fed319119e76e8070c4 Mon Sep 17 00:00:00 2001 From: strangelookingnerd <49242855+strangelookingnerd@users.noreply.github.com> Date: Fri, 17 Jan 2025 02:24:42 +0100 Subject: [PATCH 03/30] Use `jenkins.baseline` to reduce bom update mistakes (#328) --- pom.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3332e6085..519131b99 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,9 @@ 999999-SNAPSHOT - 2.361.4 + + 2.361 + ${jenkins.baseline}.4 jenkinsci/${project.artifactId}-plugin @@ -49,7 +51,7 @@ io.jenkins.tools.bom - bom-2.361.x + bom-${jenkins.baseline}.x 2102.v854b_fec19c92 import pom From b784223c1e7455fe5c797dfae81b1512dd506e26 Mon Sep 17 00:00:00 2001 From: Valentin Delaye Date: Fri, 17 Jan 2025 02:26:33 +0100 Subject: [PATCH 04/30] Require 2.452.4 and fix build (#327) --- Jenkinsfile | 13 +++++++--- pom.xml | 10 ++++---- .../DeclarativeDockerUtilsTest.java | 24 ++++++++++++++----- .../workflow/agent-with-docker/Dockerfile | 2 +- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 863a63671..6e1ea3338 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,4 +1,11 @@ -buildPlugin(useContainerAgent: false, configurations: [ - [platform: 'linux', jdk: 21], - [platform: 'maven-17-windows', jdk: 17] // TODO Docker-based tests fail when using Docker on Windows. The maven-windows agents do not have Docker installed so tests that require Docker are skipped. +/* + See the documentation for more options: + https://github.com/jenkins-infra/pipeline-library/ +*/ +buildPlugin( + forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores + useContainerAgent: false, // Set to `false` if you need to use Docker for containerized tests + configurations: [ + [platform: 'linux', jdk: 21], + [platform: 'maven-17-windows', jdk: 17], // TODO Docker-based tests fail when using Docker on Windows. The maven-windows agents do not have Docker installed so tests that require Docker are skipped. ]) diff --git a/pom.xml b/pom.xml index 519131b99..9685bb94f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,8 +4,8 @@ org.jenkins-ci.plugins plugin - 4.86 - + 4.88 + docker-workflow ${changelist} @@ -31,8 +31,8 @@ 999999-SNAPSHOT - 2.361 - ${jenkins.baseline}.4 + 2.452 + ${jenkins.baseline}.4 jenkinsci/${project.artifactId}-plugin @@ -52,7 +52,7 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 2102.v854b_fec19c92 + 3893.v213a_42768d35 import pom diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DeclarativeDockerUtilsTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DeclarativeDockerUtilsTest.java index 18d2f048a..85f1a0eb1 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DeclarativeDockerUtilsTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DeclarativeDockerUtilsTest.java @@ -33,6 +33,7 @@ import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.ExtensionList; import hudson.Functions; +import hudson.model.Descriptor.FormException; import hudson.model.Slave; import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; @@ -51,12 +52,23 @@ * And related configurations like {@link DockerPropertiesProvider}. */ public class DeclarativeDockerUtilsTest extends AbstractModelDefTest { - private static final UsernamePasswordCredentialsImpl globalCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, - "globalCreds", "sample", "bobby", "s3cr37"); - private static final UsernamePasswordCredentialsImpl folderCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, - "folderCreds", "other sample", "andrew", "s0mething"); - private static final UsernamePasswordCredentialsImpl grandParentCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, - "grandParentCreds", "yet another sample", "leopold", "idunno"); + + private static final UsernamePasswordCredentialsImpl globalCred; + private static final UsernamePasswordCredentialsImpl folderCred; + private static final UsernamePasswordCredentialsImpl grandParentCred; + + static { + try { + globalCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, + "globalCreds", "sample", "bobby", "s3cr37"); + folderCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, + "folderCreds", "other sample", "andrew", "s0mething"); + grandParentCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, + "grandParentCreds", "yet another sample", "leopold", "idunno"); + } catch (FormException e) { + throw new RuntimeException(e); + } + } @BeforeClass public static void setup() throws Exception { diff --git a/src/test/resources/org/jenkinsci/plugins/docker/workflow/agent-with-docker/Dockerfile b/src/test/resources/org/jenkinsci/plugins/docker/workflow/agent-with-docker/Dockerfile index e8438aeb2..6c175d594 100644 --- a/src/test/resources/org/jenkinsci/plugins/docker/workflow/agent-with-docker/Dockerfile +++ b/src/test/resources/org/jenkinsci/plugins/docker/workflow/agent-with-docker/Dockerfile @@ -1,4 +1,4 @@ -FROM jenkins/agent:4.10-5-jdk11 +FROM jenkins/agent:3206.3208.v409508a_675ff-1-jdk17 USER root RUN cat /etc/os-release # TODO https://github.com/moby/moby/issues/15717 alas; no curl or wget in image From 43d9e39870e22b0bbdbb88e09cc6415a886c1f05 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 17 Jan 2025 17:10:47 -0800 Subject: [PATCH 05/30] Revive test suite on Linux (#330) --- Jenkinsfile | 5 +++-- .../docker/workflow/DockerDSLTest.java | 14 ++++++------ .../workflow/declarative/DockerAgentTest.java | 22 +++++++++---------- .../agentDockerGlobalThenLabel.groovy | 2 +- .../declarative/dockerPullLocalImage.groovy | 6 ++--- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6e1ea3338..7aed39535 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,9 +3,10 @@ https://github.com/jenkins-infra/pipeline-library/ */ buildPlugin( - forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores useContainerAgent: false, // Set to `false` if you need to use Docker for containerized tests configurations: [ [platform: 'linux', jdk: 21], - [platform: 'maven-17-windows', jdk: 17], // TODO Docker-based tests fail when using Docker on Windows. The maven-windows agents do not have Docker installed so tests that require Docker are skipped. + [platform: 'linux', jdk: 17], + // TODO Windows tests seem to be failing on temporary Windows CI infrastructure from https://github.com/jenkins-infra/helpdesk/issues/4490 + //[platform: 'maven-17-windows', jdk: 17], // TODO Docker-based tests fail when using Docker on Windows. The maven-windows agents do not have Docker installed so tests that require Docker are skipped. ]) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java index bd0344d7f..c121f3ed7 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java @@ -113,7 +113,7 @@ private static void grep(File dir, String text, String prefix, Set match assumeDocker(); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( - "def r = docker.image('httpd:2.4.59').inside {\n" + + "def r = docker.image('httpd:2.4.62').inside {\n" + " semaphore 'wait'\n" + " sh 'cat /usr/local/apache2/conf/extra/httpd-userdir.conf'\n" + " 42\n" + @@ -140,7 +140,7 @@ private static void grep(File dir, String text, String prefix, Set match assumeDocker(); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition( - "docker.image('maven:3.5.3-jdk-8').inside {\n" + + "docker.image('maven:3.9.9-eclipse-temurin-17').inside {\n" + " sh 'mvn -version'\n" + "}", true)); @@ -185,7 +185,7 @@ private static void grep(File dir, String text, String prefix, Set match WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "node {\n" + - " def img = docker.image('httpd:2.4.59')\n" + + " def img = docker.image('httpd:2.4.62')\n" + " img.run().stop()\n" + " img.run('--memory-swap=-1').stop()\n" + " img.withRun {}\n" + @@ -204,7 +204,7 @@ private static void grep(File dir, String text, String prefix, Set match assumeDocker(); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( - "def r = docker.image('httpd:2.4.59').withRun {c ->\n" + + "def r = docker.image('httpd:2.4.62').withRun {c ->\n" + " semaphore 'wait'\n" + " sh \"docker exec ${c.id} cat /usr/local/apache2/conf/extra/httpd-userdir.conf\"\n" + " 42\n" + @@ -230,7 +230,7 @@ private static void grep(File dir, String text, String prefix, Set match assumeDocker(); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( - " docker.image('maven:3.3.9-jdk-8').withRun(\"--entrypoint mvn\", \"-version\") {c ->\n" + + " docker.image('maven:3.9.9-eclipse-temurin-17').withRun(\"--entrypoint mvn\", \"-version\") {c ->\n" + " sh \"docker logs ${c.id}\"" + "}", true)); story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); @@ -346,7 +346,7 @@ private static void grep(File dir, String text, String prefix, Set match WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "docker.withTool('default') {\n" + - " docker.image('httpd:2.4.59').withRun {}\n" + + " docker.image('httpd:2.4.62').withRun {}\n" + " sh 'echo PATH=$PATH'\n" + "}", true)); story.j.assertLogContains("PATH=/usr/bin:", story.j.assertBuildStatusSuccess(p.scheduleBuild2(0))); @@ -437,7 +437,7 @@ private static void grep(File dir, String text, String prefix, Set match WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "node {\n" + - " def img = docker.image('httpd:2.4.59')\n" + + " def img = docker.image('httpd:2.4.62')\n" + " def port = img.withRun('-p 12345:80') { c -> c.port(80) }\n" + " echo \"container running on ${port}\"" + "}", true)); diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java index 57e42bc45..915ef55a6 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java @@ -167,7 +167,7 @@ public void nonExistentDockerImage() throws Exception { public void fromDockerfile() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("Dockerfile", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -185,7 +185,7 @@ public void fromDockerfile() throws Exception { public void userHandbookDockerfile() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM node:16.13.1-alpine\nRUN apk add -U subversion\n"); + sampleRepo.write("Dockerfile", "FROM node:22.13.0-alpine\nRUN apk add -U subversion\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -200,7 +200,7 @@ public void userHandbookDockerfile() throws Exception { public void additionalDockerBuildArgs() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM ubuntu:14.04\n\nARG someArg=thisArgHere\n\nRUN echo \"hi there, $someArg\" > /hi-there\n\n"); + sampleRepo.write("Dockerfile", "FROM ubuntu:noble\n\nARG someArg=thisArgHere\n\nRUN echo \"hi there, $someArg\" > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -219,7 +219,7 @@ public void additionalDockerBuildArgs() throws Exception { public void additionalDockerBuildArgsImageHash() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM ubuntu:14.04\n\nARG someArg=thisArgHere\n\nRUN echo \"hi there, $someArg\" > /hi-there\n\n"); + sampleRepo.write("Dockerfile", "FROM ubuntu:noble\n\nARG someArg=thisArgHere\n\nRUN echo \"hi there, $someArg\" > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -230,11 +230,11 @@ public void additionalDockerBuildArgsImageHash() throws Exception { .withProjectName("parallelImageHashTest") .logContains("[Pipeline] { (foo)", "-v /tmp:/tmp", - "docker build -t 8343c0815beb7a50c3676f09d7175d903a57f11b --build-arg someArg=thisOtherArg", + "docker build -t 02a5b681aa9d457d1a8ebf2d61f4af0061dad300 --build-arg someArg=thisOtherArg", "The answer is 42", "hi there, thisOtherArg", "[Pipeline] { (bar)", - "docker build -t 4f8d74de557925eb9aacdfdf671b5e9de11b6086 --build-arg someArg=thisDifferentArg", + "docker build -t 36193f504228c0f319bb867146b391dd8e04aec6 --build-arg someArg=thisDifferentArg", "The answer is 43", "hi there, thisDifferentArg") .logNotContains("hi there, thisArgHere") @@ -246,7 +246,7 @@ public void additionalDockerBuildArgsImageHash() throws Exception { public void fromDockerfileInOtherDir() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("subdir/Dockerfile", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("subdir/Dockerfile", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "subdir/Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -264,7 +264,7 @@ public void fromDockerfileInOtherDir() throws Exception { public void dirSepInDockerfileName() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("subdir/Dockerfile", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("subdir/Dockerfile", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "subdir/Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -281,7 +281,7 @@ public void dirSepInDockerfileName() throws Exception { public void fromDockerfileNoArgs() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("Dockerfile", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -296,7 +296,7 @@ public void fromDockerfileNoArgs() throws Exception { @Test public void fromAlternateDockerfile() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile.alternate", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("Dockerfile.alternate", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile.alternate"); sampleRepo.git("commit", "--message=Dockerfile"); @@ -326,7 +326,7 @@ public void agentDockerGlobalThenLabel() throws Exception { public void dockerPullLocalImage() throws Exception { DockerTestUtil.assumeDocker(); - sampleRepo.write("Dockerfile", "FROM ubuntu:14.04\n\nRUN echo 'HI THERE' > /hi-there\n\n"); + sampleRepo.write("Dockerfile", "FROM ubuntu:noble\n\nRUN echo 'HI THERE' > /hi-there\n\n"); sampleRepo.git("init"); sampleRepo.git("add", "Dockerfile"); sampleRepo.git("commit", "--message=Dockerfile"); diff --git a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerGlobalThenLabel.groovy b/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerGlobalThenLabel.groovy index 221825ec8..5ad75e32d 100644 --- a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerGlobalThenLabel.groovy +++ b/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerGlobalThenLabel.groovy @@ -29,7 +29,7 @@ pipeline { agent { docker { - image 'maven:3.5.0-jdk-8-alpine' + image 'maven:3.9.9-eclipse-temurin-17-alpine' label 'docker' } } diff --git a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/dockerPullLocalImage.groovy b/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/dockerPullLocalImage.groovy index 5e7592dc6..f9dabc66f 100644 --- a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/dockerPullLocalImage.groovy +++ b/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/dockerPullLocalImage.groovy @@ -27,13 +27,13 @@ pipeline { stages { stage("build image") { steps { - sh 'docker build -t maven:3-jdk-8-slim .' + sh 'docker build -t maven:3-eclipse-temurin-17-alpine .' } } stage("in built image") { agent { docker { - image "maven:3-jdk-8-slim" + image "maven:3-eclipse-temurin-17-alpine" args "-v /tmp:/tmp" reuseNode true } @@ -46,7 +46,7 @@ pipeline { stage("in pulled image") { agent { docker { - image "maven:3-jdk-8-slim" + image "maven:3-eclipse-temurin-17-alpine" alwaysPull true } } From 1b97552884564030367288369ddef4c40eb569e5 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 20 Jan 2025 11:34:17 -0700 Subject: [PATCH 06/30] [JENKINS-75102] Fix Windows Docker running Windows container with spaces in workspace path (#326) --- .../docker/workflow/WithContainerStep.java | 22 ++++- .../docker/workflow/DockerTestUtil.java | 86 +++++++++++++++++++ .../workflow/WithContainerStepTest.java | 25 ++++++ 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java index f9814d124..b3428f7c5 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -277,7 +277,11 @@ private static class Decorator extends LauncherDecorator implements Serializable if (hasWorkdir) { prefix.add("--workdir"); masksPrefixList.add(false); - prefix.add(path); + if (super.isUnix()) { + prefix.add(path); + } else { + prefix.add(WindowsUtil.quoteArgument(path)); + } masksPrefixList.add(false); } else { String safePath = path.replace("'", "'\"'\"'"); @@ -333,8 +337,20 @@ private static class Decorator extends LauncherDecorator implements Serializable originalMasks = new boolean[starter.cmds().size()]; } - // Adapted from decorateByPrefix: - starter.cmds().addAll(0, prefix); + List cmds = new ArrayList<>(); + cmds.addAll(prefix); + + if (!super.isUnix() && starter.cmds().size() >= 3 && "cmd".equals(starter.cmds().get(0)) && "/c".equalsIgnoreCase(starter.cmds().get(1))) { + // JENKINS-75102 Docker exec on Windows processes character escaping differently. + // Modify launch to work with special characters in a way that docker exec can handle. + cmds.addAll(starter.cmds().subList(0, 2)); + cmds.add("call"); + cmds.addAll(starter.cmds().subList(2, starter.cmds().size()).stream() + .map(cmd -> cmd.replaceAll("\"\"(.*)\"\"", "\"$1\"")).collect(Collectors.toList())); + } else { + cmds.addAll(starter.cmds()); + } + starter.cmds(cmds); boolean[] masks = new boolean[originalMasks.length + prefix.size()]; boolean[] masksPrefix = new boolean[masksPrefixList.size()]; diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java index 486a433d5..de4ffdc5f 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java @@ -30,9 +30,16 @@ import hudson.util.VersionNumber; import org.junit.Assume; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.hamcrest.Matchers; import org.jenkinsci.plugins.docker.commons.tools.DockerTool; /** @@ -41,6 +48,17 @@ public class DockerTestUtil { public static String DEFAULT_MINIMUM_VERSION = "1.3"; + // Major Windows kernel versions. See https://hub.docker.com/r/microsoft/windows-nanoserver + private static List MAJOR_WINDOWS_KERNEL_VERSIONS = Arrays.asList( + "10.0.17763.6659", // 1809 + "10.0.18363.1556", // 1909 + "10.0.19041.1415", // 2004 + "10.0.19042.1889", // 20H2 + "10.0.20348.2966", // 2022 + "10.0.26100.2605" // 2025 + ); + + public static void assumeDocker() throws Exception { assumeDocker(new VersionNumber(DEFAULT_MINIMUM_VERSION)); } @@ -61,10 +79,78 @@ public static void assumeDocker(VersionNumber minimumVersion) throws Exception { Assume.assumeFalse("Docker version not < " + minimumVersion.toString(), dockerClient.version().isOlderThan(minimumVersion)); } + /** + * Used to assume docker Windows is running in a particular os mode + * @param os The os [windows, linux] + * @throws Exception + */ + public static void assumeDockerServerOSMode(String os) throws Exception { + Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int status = localLauncher + .launch() + .cmds(DockerTool.getExecutable(null, null, null, null), "version", "-f", "{{.Server.Os}}") + .stdout(out) + .start() + .joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener()); + Assume.assumeTrue("Docker working", status == 0); + Assume.assumeThat("Docker running in " + os + " mode", out.toString().trim(), Matchers.equalToIgnoringCase(os)); + } catch (IOException x) { + Assume.assumeNoException("Docker retrieve OS", x); + } + } + + public static void assumeWindows() throws Exception { + Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows")); + } + public static void assumeNotWindows() throws Exception { Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows")); } + public static String getWindowsKernelVersion() throws Exception { + Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + + int status = localLauncher + .launch() + .cmds("cmd", "/c", "ver") + .stdout(out) + .stderr(err) + .start() + .joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener()); + + if (status != 0) { + throw new RuntimeException(String.format("Failed to obtain Windows kernel version with exit code: %d stdout: %s stderr: %s", status, out, err)); + } + + Matcher matcher = Pattern.compile("Microsoft Windows \\[Version ([^\\]]+)\\]").matcher(out.toString().trim()); + + if (matcher.matches()) { + return matcher.group(1); + } else { + throw new RuntimeException("Unable to obtain Windows kernel version from output: " + out); + } + } + + /** + * @return The image tag of an image with a kernel version corresponding to the closest compatible Windows release + * @throws Exception + */ + public static String getWindowsImageTag() throws Exception { + // Kernel must match when running Windows containers on docker on Windows if < Windows 11 with Server 2022 + String kernelVersion = DockerTestUtil.getWindowsKernelVersion(); + + // Select the highest well known kernel version <= ours since sometimes an image may not exist for our version + Optional wellKnownKernelVersion = MAJOR_WINDOWS_KERNEL_VERSIONS.stream() + .filter(k -> k.compareTo(kernelVersion) <= 0).max(java.util.Comparator.naturalOrder()); + + // Fall back to trying our kernel version + return wellKnownKernelVersion.orElse(kernelVersion); + } + public static EnvVars newDockerLaunchEnv() { // Create the KeyMaterial for connecting to the docker host/server. // E.g. currently need to add something like the following to your env diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java index 9c4544f56..5cce61dcd 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java @@ -491,4 +491,29 @@ private static final class Execution extends SynchronousNonBlockingStepExecution }); } + @Issue("JENKINS-75102") + @Test public void windowsRunningWindowsContainerSpaceInPath() { + // Launching batch scripts through cmd /c in docker exec gets tricky with special characters + // By default, the path of the temporary Jenkins install and workspace have a space in a folder name and a prj@tmp folder + story.addStep(new Statement() { + @Override public void evaluate() throws Throwable { + DockerTestUtil.assumeWindows(); + DockerTestUtil.assumeDocker(); + DockerTestUtil.assumeDockerServerOSMode("windows"); + + // Kernel must match when running Windows containers on docker on Windows + String releaseTag = DockerTestUtil.getWindowsImageTag(); + + WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " withDockerContainer('mcr.microsoft.com/windows/nanoserver:" + releaseTag + "') { \n" + + " bat 'echo ran OK' \n" + + " }\n" + + "}", true)); + WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + story.j.assertLogContains("ran OK", b); + } + }); + } } From 9f1071e5483aeb4e5398118f6031501385de336e Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Mon, 20 Jan 2025 11:13:05 -0800 Subject: [PATCH 07/30] Revive test suite on Windows (#331) Co-authored-by: Mark --- Jenkinsfile | 4 +- .../docker/workflow/DockerDSLTest.java | 2 +- .../docker/workflow/DockerTestUtil.java | 44 ++++++++----------- .../workflow/WithContainerStepTest.java | 5 +-- .../client/WindowsDockerClientTest.java | 8 ++-- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7aed39535..1a995dd3b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,5 @@ buildPlugin( useContainerAgent: false, // Set to `false` if you need to use Docker for containerized tests configurations: [ [platform: 'linux', jdk: 21], - [platform: 'linux', jdk: 17], - // TODO Windows tests seem to be failing on temporary Windows CI infrastructure from https://github.com/jenkins-infra/helpdesk/issues/4490 - //[platform: 'maven-17-windows', jdk: 17], // TODO Docker-based tests fail when using Docker on Windows. The maven-windows agents do not have Docker installed so tests that require Docker are skipped. + [platform: 'windows', jdk: 17], ]) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java index c121f3ed7..9ba5642c1 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java @@ -270,7 +270,7 @@ private static void grep(File dir, String text, String prefix, Set match @Test public void buildWithMultiStage() { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { - assumeDocker(new VersionNumber("17.05")); + assumeDocker(DockerTestUtil.DockerOsMode.LINUX, new VersionNumber("17.05")); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "node {\n" + diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java index de4ffdc5f..445b41b9f 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java @@ -31,6 +31,7 @@ import org.junit.Assume; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -39,7 +40,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.hamcrest.Matchers; import org.jenkinsci.plugins.docker.commons.tools.DockerTool; /** @@ -58,33 +58,20 @@ public class DockerTestUtil { "10.0.26100.2605" // 2025 ); + public enum DockerOsMode { + LINUX, + WINDOWS + } public static void assumeDocker() throws Exception { - assumeDocker(new VersionNumber(DEFAULT_MINIMUM_VERSION)); + assumeDocker(DockerOsMode.LINUX, new VersionNumber(DEFAULT_MINIMUM_VERSION)); } - - public static void assumeDocker(VersionNumber minimumVersion) throws Exception { - Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); - try { - int status = localLauncher - .launch() - .cmds(DockerTool.getExecutable(null, null, null, null), "ps") - .start() - .joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener()); - Assume.assumeTrue("Docker working", status == 0); - } catch (IOException x) { - Assume.assumeNoException("have Docker installed", x); - } - DockerClient dockerClient = new DockerClient(localLauncher, null, null); - Assume.assumeFalse("Docker version not < " + minimumVersion.toString(), dockerClient.version().isOlderThan(minimumVersion)); + + public static void assumeDocker(DockerOsMode osMode) throws Exception { + assumeDocker(osMode, new VersionNumber(DEFAULT_MINIMUM_VERSION)); } - /** - * Used to assume docker Windows is running in a particular os mode - * @param os The os [windows, linux] - * @throws Exception - */ - public static void assumeDockerServerOSMode(String os) throws Exception { + public static void assumeDocker(DockerOsMode osMode, VersionNumber minimumVersion) throws Exception { Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); try { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -94,11 +81,14 @@ public static void assumeDockerServerOSMode(String os) throws Exception { .stdout(out) .start() .joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener()); + DockerOsMode cmdOsMode = DockerOsMode.valueOf(out.toString().trim().toUpperCase()); Assume.assumeTrue("Docker working", status == 0); - Assume.assumeThat("Docker running in " + os + " mode", out.toString().trim(), Matchers.equalToIgnoringCase(os)); + Assume.assumeTrue("Docker os mode " + osMode, osMode == cmdOsMode); } catch (IOException x) { - Assume.assumeNoException("Docker retrieve OS", x); + Assume.assumeNoException("have Docker installed", x); } + DockerClient dockerClient = new DockerClient(localLauncher, null, null); + Assume.assumeFalse("Docker version not < " + minimumVersion.toString(), dockerClient.version().isOlderThan(minimumVersion)); } public static void assumeWindows() throws Exception { @@ -109,6 +99,10 @@ public static void assumeNotWindows() throws Exception { Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows")); } + public static void assumeDrive(char drive) throws Exception { + Assume.assumeTrue(new File(drive + ":/").exists()); + } + public static String getWindowsKernelVersion() throws Exception { Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java index 5cce61dcd..f4cadd884 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java @@ -299,7 +299,7 @@ public class WithContainerStepTest { @Test public void cd() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { - DockerTestUtil.assumeDocker(new VersionNumber("17.12")); + DockerTestUtil.assumeDocker(DockerTestUtil.DockerOsMode.LINUX, new VersionNumber("17.12")); WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj"); p.setDefinition(new CpsFlowDefinition( "node {\n" + @@ -498,8 +498,7 @@ private static final class Execution extends SynchronousNonBlockingStepExecution story.addStep(new Statement() { @Override public void evaluate() throws Throwable { DockerTestUtil.assumeWindows(); - DockerTestUtil.assumeDocker(); - DockerTestUtil.assumeDockerServerOSMode("windows"); + DockerTestUtil.assumeDocker(DockerTestUtil.DockerOsMode.WINDOWS); // Kernel must match when running Windows containers on docker on Windows String releaseTag = DockerTestUtil.getWindowsImageTag(); diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java index 888c2629f..a822c7cb6 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClientTest.java @@ -10,7 +10,6 @@ import org.junit.Before; import org.junit.Test; -import java.io.IOException; import java.util.Collections; public class WindowsDockerClientTest { @@ -18,9 +17,7 @@ public class WindowsDockerClientTest { private DockerClient dockerClient; @Before - public void setup() throws Exception { - DockerTestUtil.assumeDocker(); - + public void setup() { TaskListener taskListener = StreamTaskListener.fromStderr(); Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(taskListener); @@ -28,7 +25,8 @@ public void setup() throws Exception { } @Test - public void test_run() throws IOException, InterruptedException { + public void test_run() throws Exception { + DockerTestUtil.assumeDocker(); EnvVars launchEnv = DockerTestUtil.newDockerLaunchEnv(); String containerId = dockerClient.run( launchEnv, From 52789468c076e0983cf66bd5e6c77436ae0816b1 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Wed, 12 Feb 2025 09:37:22 -0800 Subject: [PATCH 08/30] Migrate from EE 8 to EE 9 (#329) --- pom.xml | 7 +++---- .../docker/workflow/declarative/GlobalConfig.java | 4 ++-- .../docker/workflow/RegistryEndpointStepTest.java | 14 +++++++------- .../docker/workflow/ServerEndpointStepTest.java | 14 ++++++++------ .../declarative/DockerDirectiveGeneratorTest.java | 3 ++- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 9685bb94f..692a87b05 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 4.88 + 5.5 docker-workflow @@ -31,8 +31,8 @@ 999999-SNAPSHOT - 2.452 - ${jenkins.baseline}.4 + 2.479 + ${jenkins.baseline}.1 jenkinsci/${project.artifactId}-plugin @@ -62,7 +62,6 @@ org.jenkins-ci.plugins docker-commons - 419.v8e3cd84ef49c org.jenkins-ci.plugins.workflow diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/GlobalConfig.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/GlobalConfig.java index 0a7aae7c0..7099f423f 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/GlobalConfig.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/GlobalConfig.java @@ -44,7 +44,7 @@ import org.jenkinsci.Symbol; import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; /** * The system config. @@ -94,7 +94,7 @@ public void setRegistry(DockerRegistryEndpoint registry) { } @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException { req.bindJSON(this, json); save(); return true; diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java index 4432670da..d0c6b1ff7 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -39,7 +39,6 @@ import hudson.model.User; import jenkins.model.Jenkins; import jenkins.security.QueueItemAuthenticatorConfiguration; -import org.acegisecurity.Authentication; import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.SnippetizerTester; @@ -67,8 +66,6 @@ import org.kohsuke.stapler.DataBoundConstructor; import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import java.util.logging.Level; import org.jenkinsci.plugins.structs.describable.DescribableModel; @@ -169,10 +166,13 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Excep WorkflowJob p2 = r.createProject(WorkflowJob.class, "prj2"); p2.setDefinition(new CpsFlowDefinition(script, true)); - Map jobsToAuths = new HashMap<>(); - jobsToAuths.put(p1.getFullName(), User.getById("alice", true).impersonate()); - jobsToAuths.put(p2.getFullName(), User.getById("bob", true).impersonate()); - QueueItemAuthenticatorConfiguration.get().getAuthenticators().replace(new MockQueueItemAuthenticator(jobsToAuths)); + QueueItemAuthenticatorConfiguration.get() + .getAuthenticators() + .replace(new MockQueueItemAuthenticator() + .authenticate( + p1.getFullName(), User.getById("alice", true).impersonate2()) + .authenticate( + p2.getFullName(), User.getById("bob", true).impersonate2())); // Alice has Credentials.USE_ITEM permission and should be able to use the credential. WorkflowRun b1 = r.buildAndAssertSuccess(p1); diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java index 596bfd2e8..c62add786 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/ServerEndpointStepTest.java @@ -34,10 +34,8 @@ import hudson.model.Computer; import hudson.model.Item; import hudson.model.User; -import java.util.HashMap; import jenkins.model.Jenkins; import jenkins.security.QueueItemAuthenticatorConfiguration; -import org.acegisecurity.Authentication; import org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials; import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; @@ -161,10 +159,14 @@ public class ServerEndpointStepTest { WorkflowJob p2 = story.j.jenkins.createProject(WorkflowJob.class, "prj2"); p2.setDefinition(new CpsFlowDefinition(script, true)); - Map jobsToAuths = new HashMap<>(); - jobsToAuths.put(p1.getFullName(), User.getById("alice", true).impersonate()); - jobsToAuths.put(p2.getFullName(), User.getById("bob", true).impersonate()); - QueueItemAuthenticatorConfiguration.get().getAuthenticators().replace(new MockQueueItemAuthenticator(jobsToAuths)); + QueueItemAuthenticatorConfiguration.get() + .getAuthenticators() + .replace(new MockQueueItemAuthenticator() + .authenticate( + p1.getFullName(), + User.getById("alice", true).impersonate2()) + .authenticate( + p2.getFullName(), User.getById("bob", true).impersonate2())); // Alice has Credentials.USE_ITEM permission and should be able to use the credential. WorkflowRun b1 = story.j.buildAndAssertSuccess(p1); diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerDirectiveGeneratorTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerDirectiveGeneratorTest.java index e28cff150..4de16b30c 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerDirectiveGeneratorTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerDirectiveGeneratorTest.java @@ -30,6 +30,7 @@ import org.htmlunit.util.NameValuePair; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Describable; +import jakarta.servlet.ServletRequest; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -137,7 +138,7 @@ private void assertGenerateDirective(@NonNull AbstractDirective desc, @NonNull S List params = new ArrayList(); params.add(new NameValuePair("json", staplerJsonForDescr(desc).toString())); // WebClient.addCrumb *replaces* rather than *adds*: - params.add(new NameValuePair(r.jenkins.getCrumbIssuer().getDescriptor().getCrumbRequestField(), r.jenkins.getCrumbIssuer().getCrumb(null))); + params.add(new NameValuePair(r.jenkins.getCrumbIssuer().getDescriptor().getCrumbRequestField(), r.jenkins.getCrumbIssuer().getCrumb((ServletRequest) null))); wrs.setRequestParameters(params); WebResponse response = wc.getPage(wrs).getWebResponse(); assertEquals("text/plain", response.getContentType()); From a1bf2191ee926676d7e85eb4d17e801e14e111b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 09:39:04 -0800 Subject: [PATCH 09/30] Bump org.jenkins-ci.plugins:plugin from 5.5 to 5.7 (#334) Bumps [org.jenkins-ci.plugins:plugin](https://github.com/jenkinsci/plugin-pom) from 5.5 to 5.7. - [Release notes](https://github.com/jenkinsci/plugin-pom/releases) - [Changelog](https://github.com/jenkinsci/plugin-pom/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/plugin-pom/compare/plugin-5.5...plugin-5.7) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins:plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 692a87b05..2407fcebb 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 5.5 + 5.7 docker-workflow From 12aac2d217c74298ff23747aba4db4eb4004da30 Mon Sep 17 00:00:00 2001 From: Mark Waite Date: Tue, 18 Feb 2025 12:15:57 -0700 Subject: [PATCH 10/30] Use `docker ps` to detect absence of docker permissions (#336) The plugin BOM agents seem to have the `docker` command available but the user running the agent is not authorized to use the `docker` command. Previously that was detected by calling `docker ps` and detecting the failure. Restores a change made in * https://github.com/jenkinsci/docker-workflow-plugin/pull/331 Testing done Confirmed that I could see the same failure on a local computer as is seen on https://ci.jenkins.io/job/Tools/job/bom/job/master/3968/testReport/org.jenkinsci.plugins.docker.workflow/DockerDSLTest/ The computer had Docker CE installed by the specific user running the test did not have permission to access Docker. Prior to this change, the tests failed with the message: CANNOT CONNECT TO THE DOCKER DAEMON AT UNIX:///VAR/RUN/DOCKER.SOCK. IS THE DOCKER DAEMON RUNNING? After making this change, the tests pass on that computer with the specific user that does not have permission to access Docker. --- .../jenkinsci/plugins/docker/workflow/DockerTestUtil.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java index 445b41b9f..0469cd727 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java @@ -74,8 +74,14 @@ public static void assumeDocker(DockerOsMode osMode) throws Exception { public static void assumeDocker(DockerOsMode osMode, VersionNumber minimumVersion) throws Exception { Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL); try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); int status = localLauncher + .launch() + .cmds(DockerTool.getExecutable(null, null, null, null), "ps") + .start() + .joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener()); + Assume.assumeTrue("Docker working", status == 0); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + status = localLauncher .launch() .cmds(DockerTool.getExecutable(null, null, null, null), "version", "-f", "{{.Server.Os}}") .stdout(out) From 65683533b16fcff27acc8495f32e03a6a9bc6180 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 25 Feb 2025 13:49:50 -0500 Subject: [PATCH 11/30] Avoid excessive `Closure`s --- pom.xml | 32 ++++++++- .../AbstractDockerPipelineScript.groovy | 36 ++++------ .../DockerPipelineFromDockerfileScript.groovy | 68 +++++++++---------- .../declarative/DockerPipelineScript.groovy | 28 ++++---- 4 files changed, 88 insertions(+), 76 deletions(-) diff --git a/pom.xml b/pom.xml index 2407fcebb..d9dd59ee9 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,9 @@ 999999-SNAPSHOT 2.479 - ${jenkins.baseline}.1 + ${jenkins.baseline}.3 + + 2.2234.v4a_b_13b_8cd590 jenkinsci/${project.artifactId}-plugin @@ -52,10 +54,36 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 3893.v213a_42768d35 + 4228.v0a_71308d905b_ import pom + + org.jenkinsci.plugins + pipeline-model-api + ${pipeline-model-definition.version} + + + org.jenkinsci.plugins + pipeline-model-definition + ${pipeline-model-definition.version} + + + org.jenkinsci.plugins + pipeline-model-definition + ${pipeline-model-definition.version} + tests + + + org.jenkinsci.plugins + pipeline-model-extensions + ${pipeline-model-definition.version} + + + org.jenkinsci.plugins + pipeline-stage-tags-metadata + ${pipeline-model-definition.version} + diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerPipelineScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerPipelineScript.groovy index 9ba63779e..7f5cc00eb 100644 --- a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerPipelineScript.groovy +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerPipelineScript.groovy @@ -26,44 +26,38 @@ package org.jenkinsci.plugins.docker.workflow.declarative import hudson.FilePath -import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript +import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript2 import org.jenkinsci.plugins.workflow.cps.CpsScript -abstract class AbstractDockerPipelineScript> extends DeclarativeAgentScript { +abstract class AbstractDockerPipelineScript> extends DeclarativeAgentScript2 { AbstractDockerPipelineScript(CpsScript s, A a) { super(s, a) } @Override - Closure run(Closure body) { + void run(Closure body) { if (describable.reuseNode && script.getContext(FilePath.class) != null) { - return { - configureRegistry(body).call() - } + configureRegistry(body) } else if (describable.containerPerStageRoot) { - return DeclarativeDockerUtils.getLabelScript(describable, script).run { - body.call() - } + DeclarativeDockerUtils.getLabelScript(describable, script).run(body) } else { - return DeclarativeDockerUtils.getLabelScript(describable, script).run { - configureRegistry(body).call() + DeclarativeDockerUtils.getLabelScript(describable, script).run { + configureRegistry(body) } } } - protected Closure configureRegistry(Closure body) { - return { - DeclarativeDockerUtils.DockerRegistry registry = DeclarativeDockerUtils.DockerRegistry.build(describable.registryUrl, describable.registryCredentialsId) - if (registry.hasData()) { - script.getProperty("docker").withRegistry(registry.registry, registry.credential) { - runImage(body).call() - } - } else { - runImage(body).call() + protected void configureRegistry(Closure body) { + DeclarativeDockerUtils.DockerRegistry registry = DeclarativeDockerUtils.DockerRegistry.build(describable.registryUrl, describable.registryCredentialsId) + if (registry.hasData()) { + script.getProperty("docker").withRegistry(registry.registry, registry.credential) { + runImage(body) } + } else { + runImage(body) } } - protected abstract Closure runImage(Closure body) + protected abstract void runImage(Closure body) } \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy index c50eb70a9..cf76b83c1 100644 --- a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy @@ -37,49 +37,43 @@ class DockerPipelineFromDockerfileScript extends AbstractDockerPipelineScript } @Override - Closure runImage(Closure body) { - return { - if (!Utils.withinAStage() && describable.alwaysPull) { - script.stage(SyntheticStageNames.agentSetup()) { - try { - script.getProperty("docker").image(describable.image).pull() - } catch (Exception e) { - Utils.markStageFailedAndContinued(SyntheticStageNames.agentSetup()) - throw e - } + void runImage(Closure body) { + if (!Utils.withinAStage() && describable.alwaysPull) { + script.stage(SyntheticStageNames.agentSetup()) { + try { + script.getProperty("docker").image(describable.image).pull() + } catch (Exception e) { + Utils.markStageFailedAndContinued(SyntheticStageNames.agentSetup()) + throw e } } - if (Utils.withinAStage() && describable.alwaysPull) { - script.getProperty("docker").image(describable.image).pull() - } - script.getProperty("docker").image(describable.image).inside(describable.args, { - body.call() - }) } + if (Utils.withinAStage() && describable.alwaysPull) { + script.getProperty("docker").image(describable.image).pull() + } + script.getProperty("docker").image(describable.image).inside(describable.args, body) } } From be8f3bfed0788f280d144bf8457f840ed5a94fb1 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 25 Feb 2025 14:05:25 -0500 Subject: [PATCH 12/30] `void` return type was incorrect (but CPS-transformed code does not check) https://github.com/jenkinsci/docker-workflow-plugin/pull/337#discussion_r1970362720 --- .../declarative/DockerPipelineFromDockerfileScript.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy index cf76b83c1..6dd9d3bbf 100644 --- a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/DockerPipelineFromDockerfileScript.groovy @@ -56,7 +56,7 @@ class DockerPipelineFromDockerfileScript extends AbstractDockerPipelineScript Date: Wed, 26 Feb 2025 09:20:42 -0500 Subject: [PATCH 13/30] Skip `agentDockerWithCreds` unless both `docker.username` and `docker.password` are set --- .../workflow/declarative/DockerAgentTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java index 915ef55a6..f715860e0 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java @@ -39,6 +39,7 @@ import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; import org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest; import static org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest.j; +import static org.junit.Assume.assumeTrue; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -52,7 +53,7 @@ public class DockerAgentTest extends AbstractModelDefTest { private static Slave s; private static Slave s2; - private static String password; + private static String username, password; @BeforeClass public static void setUpAgent() throws Exception { s = j.createOnlineSlave(); @@ -68,12 +69,13 @@ public static void setUpAgent() throws Exception { //setup credentials for docker registry CredentialsStore store = CredentialsProvider.lookupStores(j.jenkins).iterator().next(); + username = System.getProperty("docker.username"); password = System.getProperty("docker.password"); - if(password != null) { + if (username != null && password != null) { UsernamePasswordCredentialsImpl globalCred = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, - "dockerhub", "real", "jtaboada", password); + "dockerhub", "real", username, password); store.addCredentials(Domain.global(), globalCred); @@ -87,9 +89,8 @@ public void agentDocker() throws Exception { @Test public void agentDockerWithCreds() throws Exception { - //If there is no password, the test is ignored - if(password != null) - agentDocker("org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds", "-v /tmp:/tmp"); + assumeTrue(username != null && password != null); + agentDocker("org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds", "-v /tmp:/tmp"); } @Test From 927209fe5e2ffeef4dc041a2ad04a83abb5490b7 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 27 Feb 2025 08:34:03 -0500 Subject: [PATCH 14/30] Deleting `agentDockerWithCreds` test --- .../workflow/declarative/DockerAgentTest.java | 27 +---------- .../declarative/agentDockerWithCreds.groovy | 45 ------------------- 2 files changed, 1 insertion(+), 71 deletions(-) delete mode 100644 src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds.groovy diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java index f715860e0..d95f13eff 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java @@ -25,11 +25,6 @@ package org.jenkinsci.plugins.docker.workflow.declarative; import com.cloudbees.hudson.plugins.folder.Folder; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsScope; -import com.cloudbees.plugins.credentials.CredentialsStore; -import com.cloudbees.plugins.credentials.domains.Domain; -import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.model.Result; import hudson.model.Slave; import hudson.slaves.EnvironmentVariablesNodeProperty; @@ -39,7 +34,6 @@ import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; import org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest; import static org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest.j; -import static org.junit.Assume.assumeTrue; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -53,7 +47,6 @@ public class DockerAgentTest extends AbstractModelDefTest { private static Slave s; private static Slave s2; - private static String username, password; @BeforeClass public static void setUpAgent() throws Exception { s = j.createOnlineSlave(); @@ -66,20 +59,6 @@ public static void setUpAgent() throws Exception { s2.setLabelString("other-docker"); s2.getNodeProperties().add(new EnvironmentVariablesNodeProperty(new EnvironmentVariablesNodeProperty.Entry("ONAGENT", "true"), new EnvironmentVariablesNodeProperty.Entry("WHICH_AGENT", "second"))); - //setup credentials for docker registry - CredentialsStore store = CredentialsProvider.lookupStores(j.jenkins).iterator().next(); - - username = System.getProperty("docker.username"); - password = System.getProperty("docker.password"); - - if (username != null && password != null) { - UsernamePasswordCredentialsImpl globalCred = - new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, - "dockerhub", "real", username, password); - - store.addCredentials(Domain.global(), globalCred); - - } } @Test @@ -87,11 +66,7 @@ public void agentDocker() throws Exception { agentDocker("org/jenkinsci/plugins/docker/workflow/declarative/agentDocker", "-v /tmp:/tmp"); } - @Test - public void agentDockerWithCreds() throws Exception { - assumeTrue(username != null && password != null); - agentDocker("org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds", "-v /tmp:/tmp"); - } + // TODO write test of registryCredentialsId, e.g. using a registry in Testcontainers, or MockLauncherStep as in RegistryEndpointStepTest @Test public void agentDockerWithRegistryNoCreds() throws Exception { diff --git a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds.groovy b/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds.groovy deleted file mode 100644 index a09dd5ddd..000000000 --- a/src/test/resources/org/jenkinsci/plugins/docker/workflow/declarative/agentDockerWithCreds.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, 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. - */ - -pipeline { - agent { - docker { - alwaysPull true - registryCredentialsId "dockerhub" - image "jtaboada/httpd:2.4.59" - args "-v /tmp:/tmp" - } - } - stages { - stage("foo") { - steps { - sh 'cat /usr/local/apache2/conf/extra/httpd-userdir.conf' - sh 'echo "The answer is 42"' - } - } - } -} - - - From 1372ed13acbc5d6207145fe2ae8e8124ac2c4198 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 3 Mar 2025 17:58:56 -0500 Subject: [PATCH 15/30] Compatibility for `DockerPipelineScript` --- pom.xml | 4 +- .../declarative/AbstractDockerAgent.java | 17 ++++ .../AbstractDockerPipelineScript.groovy | 69 +++++++++++++++ .../DockerPipelineFromDockerfileScript.groovy | 85 +++++++++++++++++++ .../compat/DockerPipelineScript.groovy | 59 +++++++++++++ 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/AbstractDockerPipelineScript.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineFromDockerfileScript.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineScript.groovy diff --git a/pom.xml b/pom.xml index d9dd59ee9..ee12712d3 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,9 @@ 2.479 ${jenkins.baseline}.3 - 2.2234.v4a_b_13b_8cd590 + + 2.2243.v46879d3a_96b_e + true jenkinsci/${project.artifactId}-plugin diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java index ee9c7e8bf..651e4dbc9 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java @@ -29,8 +29,11 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import java.net.URL; +import java.util.Set; import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgent; import org.jenkinsci.plugins.pipeline.modeldefinition.options.DeclarativeOption; +import org.jenkinsci.plugins.pipeline.modeldefinition.parser.CompatibilityLoader; import org.jenkinsci.plugins.workflow.cps.GroovySourceFileAllowlist; import org.kohsuke.stapler.DataBoundSetter; @@ -139,4 +142,18 @@ public boolean isAllowed(String groovyResourceUrl) { } } + @Extension public static final class Compat implements CompatibilityLoader { + private static final Set CLASSES = Set.of( + "org.jenkinsci.plugins.docker.workflow.declarative.AbstractDockerPipelineScript", + "org.jenkinsci.plugins.docker.workflow.declarative.DockerPipelineScript", + "org.jenkinsci.plugins.docker.workflow.declarative.DockerPipelineFromDockerfileScript"); + @Override public URL loadGroovySource(String clazz) { + if (CLASSES.contains(clazz)) { + return AbstractDockerAgent.class.getResource("compat/" + clazz.replaceFirst(".+[.]", "") + ".groovy"); + } else { + return null; + } + } + } + } diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/AbstractDockerPipelineScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/AbstractDockerPipelineScript.groovy new file mode 100644 index 000000000..9ba63779e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/AbstractDockerPipelineScript.groovy @@ -0,0 +1,69 @@ +/* + * The MIT License + * + * Copyright (c) 2017, 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. + */ + + +package org.jenkinsci.plugins.docker.workflow.declarative + +import hudson.FilePath +import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript +import org.jenkinsci.plugins.workflow.cps.CpsScript + +abstract class AbstractDockerPipelineScript> extends DeclarativeAgentScript { + + AbstractDockerPipelineScript(CpsScript s, A a) { + super(s, a) + } + + @Override + Closure run(Closure body) { + if (describable.reuseNode && script.getContext(FilePath.class) != null) { + return { + configureRegistry(body).call() + } + } else if (describable.containerPerStageRoot) { + return DeclarativeDockerUtils.getLabelScript(describable, script).run { + body.call() + } + } else { + return DeclarativeDockerUtils.getLabelScript(describable, script).run { + configureRegistry(body).call() + } + } + } + + protected Closure configureRegistry(Closure body) { + return { + DeclarativeDockerUtils.DockerRegistry registry = DeclarativeDockerUtils.DockerRegistry.build(describable.registryUrl, describable.registryCredentialsId) + if (registry.hasData()) { + script.getProperty("docker").withRegistry(registry.registry, registry.credential) { + runImage(body).call() + } + } else { + runImage(body).call() + } + } + } + + protected abstract Closure runImage(Closure body) +} \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineFromDockerfileScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineFromDockerfileScript.groovy new file mode 100644 index 000000000..c50eb70a9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineFromDockerfileScript.groovy @@ -0,0 +1,85 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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. + */ + + +package org.jenkinsci.plugins.docker.workflow.declarative + +import org.jenkinsci.plugins.pipeline.modeldefinition.SyntheticStageNames +import org.jenkinsci.plugins.pipeline.modeldefinition.Utils +import org.jenkinsci.plugins.workflow.cps.CpsScript +import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper + +class DockerPipelineFromDockerfileScript extends AbstractDockerPipelineScript { + + DockerPipelineFromDockerfileScript(CpsScript s, DockerPipelineFromDockerfile a) { + super(s, a) + } + + @Override + Closure runImage(Closure body) { + return { + def img = null + if (!Utils.withinAStage()) { + script.stage(SyntheticStageNames.agentSetup()) { + try { + img = buildImage().call() + } catch (Exception e) { + Utils.markStageFailedAndContinued(SyntheticStageNames.agentSetup()) + throw e + } + } + } else { + img = buildImage().call() + } + if (img != null) { + img.inside(describable.args, { + body.call() + }) + } + } + } + + private Closure buildImage() { + return { + boolean isUnix = script.isUnix() + def dockerfilePath = describable.getDockerfilePath(isUnix) + try { + RunWrapper runWrapper = (RunWrapper)script.getProperty("currentBuild") + def additionalBuildArgs = describable.getAdditionalBuildArgs() ? " ${describable.additionalBuildArgs}" : "" + def hash = Utils.stringToSHA1("${runWrapper.fullProjectName}\n${script.readFile("${dockerfilePath}")}\n${additionalBuildArgs}") + def imgName = "${hash}" + def commandLine = "docker build -t ${imgName}${additionalBuildArgs} -f \"${dockerfilePath}\" \"${describable.getActualDir()}\"" + if (isUnix) + script.sh commandLine + else + script.bat commandLine + + return script.getProperty("docker").image(imgName) + } catch (FileNotFoundException f) { + script.error("No Dockerfile found at ${dockerfilePath} in repository - failing.") + return null + } + } + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineScript.groovy b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineScript.groovy new file mode 100644 index 000000000..55e21dbec --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/docker/workflow/declarative/compat/DockerPipelineScript.groovy @@ -0,0 +1,59 @@ +/* + * The MIT License + * + * Copyright (c) 2016, 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. + */ + + +package org.jenkinsci.plugins.docker.workflow.declarative + +import org.jenkinsci.plugins.pipeline.modeldefinition.SyntheticStageNames +import org.jenkinsci.plugins.pipeline.modeldefinition.Utils +import org.jenkinsci.plugins.workflow.cps.CpsScript + +class DockerPipelineScript extends AbstractDockerPipelineScript { + + DockerPipelineScript(CpsScript s, DockerPipeline a) { + super(s, a) + } + + @Override + Closure runImage(Closure body) { + return { + if (!Utils.withinAStage() && describable.alwaysPull) { + script.stage(SyntheticStageNames.agentSetup()) { + try { + script.getProperty("docker").image(describable.image).pull() + } catch (Exception e) { + Utils.markStageFailedAndContinued(SyntheticStageNames.agentSetup()) + throw e + } + } + } + if (Utils.withinAStage() && describable.alwaysPull) { + script.getProperty("docker").image(describable.image).pull() + } + script.getProperty("docker").image(describable.image).inside(describable.args, { + body.call() + }) + } + } +} From 4a503ed3d066311f80665510f967f52be5011424 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 4 Mar 2025 16:54:49 -0500 Subject: [PATCH 16/30] Dep released --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ee12712d3..d112839ef 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,7 @@ 2.479 ${jenkins.baseline}.3 - - 2.2243.v46879d3a_96b_e + 2.2247.va_423189a_7dff true jenkinsci/${project.artifactId}-plugin From 89f018193f4ebf4f68906acb80503a4992cecf31 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 4 Mar 2025 16:55:29 -0500 Subject: [PATCH 17/30] Make extension optional so it will be easier to delete the API later --- .../docker/workflow/declarative/AbstractDockerAgent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java index 651e4dbc9..f4d769455 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/declarative/AbstractDockerAgent.java @@ -142,7 +142,7 @@ public boolean isAllowed(String groovyResourceUrl) { } } - @Extension public static final class Compat implements CompatibilityLoader { + @Extension(optional = true) public static final class Compat implements CompatibilityLoader { private static final Set CLASSES = Set.of( "org.jenkinsci.plugins.docker.workflow.declarative.AbstractDockerPipelineScript", "org.jenkinsci.plugins.docker.workflow.declarative.DockerPipelineScript", From 4aeeef3b78b858e33bc985f9b1faf8bd1b77db20 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 3 Mar 2025 19:47:14 -0500 Subject: [PATCH 18/30] Suppressing `DockerAgentTest.userHandbookDockerfile` in CI --- .../plugins/docker/workflow/declarative/DockerAgentTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java index d95f13eff..ed44e650f 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/declarative/DockerAgentTest.java @@ -34,10 +34,13 @@ import org.jenkinsci.plugins.docker.workflow.DockerTestUtil; import org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest; import static org.jenkinsci.plugins.pipeline.modeldefinition.AbstractModelDefTest.j; +import static org.junit.Assume.assumeThat; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.jvnet.hudson.test.Issue; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; /** * Adapted from {@link org.jenkinsci.plugins.pipeline.modeldefinition.AgentTest}. @@ -159,6 +162,7 @@ public void fromDockerfile() throws Exception { @Issue("https://github.com/jenkinsci/docker-workflow-plugin/pull/57#issuecomment-1507755385") @Test public void userHandbookDockerfile() throws Exception { + assumeThat("TODO currently failing", System.getenv("CI"), not(is("true"))); DockerTestUtil.assumeDocker(); sampleRepo.write("Dockerfile", "FROM node:22.13.0-alpine\nRUN apk add -U subversion\n"); From 66f6e222b8d1c96bdf55e21cf8719f9a57e7f239 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:29:51 +0000 Subject: [PATCH 19/30] Bump org.jenkins-ci.plugins:plugin from 5.7 to 5.9 (#344) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d112839ef..21230ed81 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 5.7 + 5.9 docker-workflow From 72a65cc6e5623fa5f85bf349c367bf90e758f3fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:20:55 -0700 Subject: [PATCH 20/30] Bump io.jenkins.tools.bom:bom-2.479.x from 4228.v0a_71308d905b_ to 4488.v7fe26526366e (#345) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 21230ed81..a2814acf0 100644 --- a/pom.xml +++ b/pom.xml @@ -33,8 +33,6 @@ 2.479 ${jenkins.baseline}.3 - - 2.2247.va_423189a_7dff true jenkinsci/${project.artifactId}-plugin @@ -55,36 +53,10 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 4228.v0a_71308d905b_ + 4488.v7fe26526366e import pom - - org.jenkinsci.plugins - pipeline-model-api - ${pipeline-model-definition.version} - - - org.jenkinsci.plugins - pipeline-model-definition - ${pipeline-model-definition.version} - - - org.jenkinsci.plugins - pipeline-model-definition - ${pipeline-model-definition.version} - tests - - - org.jenkinsci.plugins - pipeline-model-extensions - ${pipeline-model-definition.version} - - - org.jenkinsci.plugins - pipeline-stage-tags-metadata - ${pipeline-model-definition.version} - From 0206b2a718986929c7b5c76ca5334099b271c27c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 05:04:40 +0000 Subject: [PATCH 21/30] Bump org.jenkins-ci.plugins:plugin from 5.9 to 5.12 Bumps [org.jenkins-ci.plugins:plugin](https://github.com/jenkinsci/plugin-pom) from 5.9 to 5.12. - [Release notes](https://github.com/jenkinsci/plugin-pom/releases) - [Changelog](https://github.com/jenkinsci/plugin-pom/blob/master/CHANGELOG.md) - [Commits](https://github.com/jenkinsci/plugin-pom/compare/plugin-5.9...plugin-5.12) --- updated-dependencies: - dependency-name: org.jenkins-ci.plugins:plugin dependency-version: '5.12' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2814acf0..f00f44bfe 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 5.9 + 5.12 docker-workflow From e24684f271eac60d4095adfe033599629e16bb21 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 28 May 2025 14:57:46 -0400 Subject: [PATCH 22/30] [JENKINS-75679] Reproduced `FilePathPickle` usage in test --- .../workflow/RegistryEndpointStepTest.java | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java index d0c6b1ff7..5788dbc39 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -59,28 +59,38 @@ import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; -import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.MockQueueItemAuthenticator; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.DataBoundConstructor; import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Set; import java.util.logging.Level; +import org.apache.commons.text.StringEscapeUtils; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import org.jenkinsci.plugins.structs.describable.DescribableModel; +import org.jenkinsci.plugins.workflow.support.pickles.FilePathPickle; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.ClassRule; import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsSessionRule; import org.jvnet.hudson.test.LoggerRule; public class RegistryEndpointStepTest { - @Rule public JenkinsRule r = new JenkinsRule(); + @Rule public final JenkinsSessionRule rr = new JenkinsSessionRule(); @Rule public LoggerRule logging = new LoggerRule(); - @Rule public BuildWatcher bw = new BuildWatcher(); + @ClassRule public static BuildWatcher bw = new BuildWatcher(); @Issue("JENKINS-51395") - @Test public void configRoundTrip() throws Exception { + @Test public void configRoundTrip() throws Throwable { logging.record(DescribableModel.class, Level.FINE); + rr.then(r -> { { // Recommended syntax. SnippetizerTester st = new SnippetizerTester(r); RegistryEndpointStep step = new RegistryEndpointStep(new DockerRegistryEndpoint("https://myreg/", null)); @@ -116,12 +126,14 @@ public class RegistryEndpointStepTest { assertEquals("registryCreds", registry.getCredentialsId()); // TODO check toolName } + }); } @Test - public void stepExecutionWithCredentials() throws Exception { + public void stepExecutionWithCredentials() throws Throwable { assumeNotWindows(); + rr.then(r -> { IdCredentials registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); @@ -137,12 +149,14 @@ public void stepExecutionWithCredentials() throws Exception { r.assertBuildStatusSuccess(r.waitForCompletion(b)); r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b); r.assertLogNotContains("s3cr3t", b); + }); } @Test - public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Exception { + public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Throwable { assumeNotWindows(); + rr.then(r -> { r.getInstance().setSecurityRealm(r.createDummySecurityRealm()); MockAuthorizationStrategy auth = new MockAuthorizationStrategy() .grant(Jenkins.READ).everywhere().to("alice", "bob") @@ -181,6 +195,39 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Excep // Bob does not have Credentials.USE_ITEM permission and should not be able to use the credential. r.assertBuildStatus(Result.FAILURE, p2.scheduleBuild2(0)); + }); + } + + @Issue("JENKINS-75679") + @Test public void noFilePathPickle() throws Throwable { + assumeNotWindows(); + rr.then(r -> { + var registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); + var p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + """ + node { + mockDockerLogin { + withDockerRegistry(url: 'https://my-reg:1234', credentialsId: 'registryCreds') { + semaphore 'wait' + } + } + } + """, true)); + var b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + }); + @SuppressWarnings("deprecation") + var verboten = FilePathPickle.class.getName(); + assertThat(StringEscapeUtils.escapeJava(Files.readString(rr.getHome().toPath().resolve("jobs/p/builds/1/program.dat"), StandardCharsets.ISO_8859_1)), not(containsString(verboten))); + rr.then(r -> { + var b = r.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1); + SemaphoreStep.success("wait/1", null); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b); + r.assertLogNotContains("s3cr3t", b); + }); } public static class MockLauncherStep extends Step { From 63f6900e719c488379f649c958936de22a3e0427 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 29 May 2025 16:43:23 -0400 Subject: [PATCH 23/30] More compelling test, using a containerized agent --- pom.xml | 11 +++ .../workflow/RegistryEndpointStepTest.java | 81 ++++++++++++++----- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index f00f44bfe..4870ba838 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,17 @@ 2.2 test + + org.jenkins-ci.plugins + ssh-slaves + test + + + org.testcontainers + testcontainers + 1.21.0 + test + diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java index 5788dbc39..e1863c113 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -23,6 +23,7 @@ */ package org.jenkinsci.plugins.docker.workflow; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.common.IdCredentials; @@ -37,6 +38,9 @@ import hudson.model.Node; import hudson.model.Result; import hudson.model.User; +import hudson.plugins.sshslaves.SSHLauncher; +import hudson.slaves.DumbSlave; +import java.io.ByteArrayOutputStream; import jenkins.model.Jenkins; import jenkins.security.QueueItemAuthenticatorConfiguration; import org.jenkinsci.plugins.docker.commons.credentials.DockerRegistryEndpoint; @@ -70,9 +74,13 @@ import java.util.Set; import java.util.logging.Level; import org.apache.commons.text.StringEscapeUtils; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter; +import org.apache.sshd.common.keyprovider.KeyPairProvider; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; +import static org.jenkinsci.plugins.docker.workflow.DockerTestUtil.assumeDocker; import org.jenkinsci.plugins.structs.describable.DescribableModel; import org.jenkinsci.plugins.workflow.support.pickles.FilePathPickle; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; @@ -80,6 +88,7 @@ import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.JenkinsSessionRule; import org.jvnet.hudson.test.LoggerRule; +import org.testcontainers.containers.GenericContainer; public class RegistryEndpointStepTest { @@ -200,14 +209,17 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Throw @Issue("JENKINS-75679") @Test public void noFilePathPickle() throws Throwable { - assumeNotWindows(); - rr.then(r -> { - var registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); - CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); - var p = r.createProject(WorkflowJob.class, "p"); - p.setDefinition(new CpsFlowDefinition( + assumeDocker(); + try (var agent = new SSHAgentContainer()) { + agent.start(); + rr.then(r -> { + agent.register("remote"); + var registryCredentials = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "registryCreds", null, "me", "s3cr3t"); + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), registryCredentials); + var p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( """ - node { + node('remote') { mockDockerLogin { withDockerRegistry(url: 'https://my-reg:1234', credentialsId: 'registryCreds') { semaphore 'wait' @@ -215,21 +227,50 @@ public void stepExecutionWithCredentialsAndQueueItemAuthenticator() throws Throw } } """, true)); - var b = p.scheduleBuild2(0).waitForStart(); - SemaphoreStep.waitForStart("wait/1", b); - }); - @SuppressWarnings("deprecation") - var verboten = FilePathPickle.class.getName(); - assertThat(StringEscapeUtils.escapeJava(Files.readString(rr.getHome().toPath().resolve("jobs/p/builds/1/program.dat"), StandardCharsets.ISO_8859_1)), not(containsString(verboten))); - rr.then(r -> { - var b = r.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1); - SemaphoreStep.success("wait/1", null); - r.assertBuildStatusSuccess(r.waitForCompletion(b)); - r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b); - r.assertLogNotContains("s3cr3t", b); - }); + var b = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait/1", b); + }); + @SuppressWarnings("deprecation") + var verboten = FilePathPickle.class.getName(); + assertThat(StringEscapeUtils.escapeJava(Files.readString(rr.getHome().toPath().resolve("jobs/p/builds/1/program.dat"), StandardCharsets.ISO_8859_1)), not(containsString(verboten))); + rr.then(r -> { + var b = r.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1); + SemaphoreStep.success("wait/1", null); + r.assertBuildStatusSuccess(r.waitForCompletion(b)); + r.assertLogContains("docker login -u me -p ******** https://my-reg:1234", b); + r.assertLogNotContains("s3cr3t", b); + }); + } } + private static final class SSHAgentContainer extends GenericContainer { + private final String priv; + SSHAgentContainer() { + super("jenkins/ssh-agent:6.17.0"); + try { + var kp = KeyUtils.generateKeyPair(KeyPairProvider.SSH_RSA, 2048); + var kprw = new OpenSSHKeyPairResourceWriter(); + var baos = new ByteArrayOutputStream(); + kprw.writePublicKey(kp, null, baos); + var pub = baos.toString(StandardCharsets.US_ASCII); + baos.reset(); + kprw.writePrivateKey(kp, null, null, baos); + priv = baos.toString(StandardCharsets.US_ASCII); + withEnv("JENKINS_AGENT_SSH_PUBKEY", pub); + withExposedPorts(22); + } catch (Exception x) { + throw new AssertionError(x); + } + } + void register(String name) throws Exception{ + var creds = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, null, "jenkins", new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(priv), null, null); + CredentialsProvider.lookupStores(Jenkins.get()).iterator().next().addCredentials(Domain.global(), creds); + var port = getMappedPort(22); + Jenkins.get().addNode(new DumbSlave(name, "/home/jenkins/agent", new SSHLauncher(getHost(), port, creds.getId()))); + } + } + + public static class MockLauncherStep extends Step { @DataBoundConstructor From 7ac264bb58bb0e4e29216c9640595d90d9c5fe9b Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 29 May 2025 18:22:32 -0400 Subject: [PATCH 24/30] Fixed using `KeyMaterial2` --- pom.xml | 6 ++ .../AbstractEndpointStepExecution2.java | 59 ++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 4870ba838..6288c05fa 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,12 @@ import pom + + + org.jenkins-ci.plugins + docker-commons + 454.v2171c32a_8cdd + diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution2.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution2.java index 5e0d0ef3b..3eb3c48c3 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution2.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/AbstractEndpointStepExecution2.java @@ -23,11 +23,14 @@ */ package org.jenkinsci.plugins.docker.workflow; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; +import hudson.FilePath; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial; +import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterial2; import org.jenkinsci.plugins.docker.commons.credentials.KeyMaterialFactory; import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander; import org.jenkinsci.plugins.workflow.steps.GeneralNonBlockingStepExecution; @@ -50,19 +53,19 @@ protected AbstractEndpointStepExecution2(StepContext context) { private void doStart() throws Exception { KeyMaterialFactory keyMaterialFactory = newKeyMaterialFactory(); - KeyMaterial material = keyMaterialFactory.materialize(); + KeyMaterial2 material = keyMaterialFactory.materialize2(); getContext().newBodyInvoker(). - withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), new Expander(material))). - withCallback(new Callback(material)). + withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), new Expander2(material))). + withCallback(new Callback2(material)). start(); } - private static class Expander extends EnvironmentExpander { + private static class Expander2 extends EnvironmentExpander { private static final long serialVersionUID = 1; - private final KeyMaterial material; + private final KeyMaterial2 material; - Expander(KeyMaterial material) { + Expander2(KeyMaterial2 material) { this.material = material; } @@ -72,15 +75,53 @@ private static class Expander extends EnvironmentExpander { } - private class Callback extends TailCall { + private class Callback2 extends TailCall { private static final long serialVersionUID = 1; - private final KeyMaterial material; + private final KeyMaterial2 material; - Callback(KeyMaterial material) { + Callback2(KeyMaterial2 material) { this.material = material; } + @Override protected void finished(StepContext context) throws Exception { + try { + material.close(context.get(FilePath.class).getChannel()); + } catch (IOException | InterruptedException x) { + Logger.getLogger(AbstractEndpointStepExecution2.class.getName()).log(Level.WARNING, null, x); + } + } + + } + + @SuppressFBWarnings(value = {"NP_UNWRITTEN_FIELD", "UWF_NULL_FIELD"}) + @Deprecated + private static class Expander extends EnvironmentExpander { + + private static final long serialVersionUID = 1; + private final KeyMaterial material = null; + + private Expander() { + assert false : "only deserialized"; + } + + @Override public void expand(EnvVars env) throws IOException, InterruptedException { + env.putAll(material.env()); + } + + } + + @SuppressFBWarnings(value = {"NP_UNWRITTEN_FIELD", "UWF_NULL_FIELD"}) + @Deprecated + private class Callback extends TailCall { + + private static final long serialVersionUID = 1; + private final KeyMaterial material = null; + + private Callback() { + assert false : "only deserialized"; + } + @Override protected void finished(StepContext context) throws Exception { try { material.close(); From f3cfea9f40292d79fbd260515324130d24d360f8 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 29 May 2025 18:43:43 -0400 Subject: [PATCH 25/30] `DockerDSLTest.firstDoNoHarm` flaky just like https://github.com/jenkinsci/credentials-binding-plugin/pull/279 --- .../plugins/docker/workflow/DockerDSLTest.java | 13 +++++++++++-- .../docker/workflow/RegistryEndpointStepTest.java | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java index 9ba5642c1..fed2135a9 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/DockerDSLTest.java @@ -46,7 +46,10 @@ import org.jvnet.hudson.test.RestartableJenkinsRule; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; import java.util.Collections; import java.util.Set; import java.util.TreeSet; @@ -100,8 +103,14 @@ private static void grep(File dir, String text, String prefix, Set match String qualifiedName = prefix + kid.getName(); if (kid.isDirectory()) { grep(kid, text, qualifiedName + "/", matches); - } else if (kid.isFile() && FileUtils.readFileToString(kid).contains(text)) { - matches.add(qualifiedName); + } else { + try { + if (FileUtils.readFileToString(kid, StandardCharsets.UTF_8).contains(text)) { + matches.add(qualifiedName); + } + } catch (FileNotFoundException | NoSuchFileException x) { + // ignore, e.g. tmp file + } } } } diff --git a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java index e1863c113..61771e8c8 100644 --- a/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/docker/workflow/RegistryEndpointStepTest.java @@ -270,7 +270,6 @@ void register(String name) throws Exception{ } } - public static class MockLauncherStep extends Step { @DataBoundConstructor From 86f1bde2b81898fa7d4823ee5b64a27f74cb2cf9 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 30 May 2025 08:17:38 -0400 Subject: [PATCH 26/30] Dep released --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6288c05fa..3eda141ca 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ org.jenkins-ci.plugins docker-commons - 454.v2171c32a_8cdd + 457.v0f62a_94f11a_3 From d005a70fcb03bf9d695f36aa0d6b83e7832d4aac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:11:25 +0000 Subject: [PATCH 27/30] Bump org.jenkins-ci.plugins:plugin from 5.12 to 5.18 (#358) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3eda141ca..c069610a8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.jenkins-ci.plugins plugin - 5.12 + 5.18 docker-workflow From fa2f5ea2ea4b452f7d7ccb6e2d9d703b7d11e7b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:11:34 +0000 Subject: [PATCH 28/30] Bump org.testcontainers:testcontainers from 1.21.0 to 1.21.3 (#357) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c069610a8..caaeb88e8 100644 --- a/pom.xml +++ b/pom.xml @@ -164,7 +164,7 @@ org.testcontainers testcontainers - 1.21.0 + 1.21.3 test - - org.jenkins-ci.plugins - docker-commons - 457.v0f62a_94f11a_3 - From d8bf5e55a599573f99b8343bdf92ee2e1896ef17 Mon Sep 17 00:00:00 2001 From: Lionel ROUBEYRIE Date: Fri, 18 Jul 2025 14:42:34 +0200 Subject: [PATCH 30/30] Add userns=keep-id option for podman Remove UID:GID in rootless mode --- .../docker/workflow/WithContainerStep.java | 11 ++++++- .../docker/workflow/client/DockerClient.java | 32 +++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java index b3428f7c5..6a4b09a12 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -197,7 +197,16 @@ public Execution() { } String command = launcher.isUnix() ? "cat" : "cmd.exe"; - container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command); + String whoAmI = dockerClient.whoAmI(); + if (dockerClient.isRootless()) { + listener.getLogger().println("Running in rootless mode."); + whoAmI = ""; + if (dockerClient.getEngine().equals("podman")) { + listener.getLogger().println("Podman detected."); + step.args = step.args == null ? "--userns=keep-id" : "--userns=keep-id " + step.args; + } + } + container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, whoAmI, /* expected to hang until killed */ command); final List ps = dockerClient.listProcess(env, container); if (!ps.contains(command)) { listener.error( diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java index 323f18e93..26dfd4fc0 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -336,27 +336,33 @@ public String whoAmI() throws IOException, InterruptedException { return ""; } - TaskListener listener = launcher.getListener(); - final String rootlessId = "0:0"; - // First, check if under the hood it's Podman or Docker - String engine = executeCommand("docker", "--version"); - if (engine.toLowerCase().contains("podman")) { - listener.getLogger().println("Container engine is Podman with build in rootless mode"); - return rootlessId; + String userId = executeCommand("id", "-u"); + String groupId = executeCommand("id", "-g"); + + return String.format("%s:%s", userId, groupId); + } + + public String getEngine() throws IOException, InterruptedException { + + String engine = "docker"; + + String cmd = executeCommand("docker", "--version"); + if (cmd.toLowerCase().contains("podman")) { + engine = "podman"; } else { String rootless = executeCommand("docker", "info", "-f", "{{.SecurityOptions}}" ); if (rootless.toLowerCase().contains("rootless")) { - listener.getLogger().println("Container engine is Docker with rootless mode"); - return rootlessId; + engine = "docker-rootless"; } } - // Else not rootless, return the current user/group ids - String userId = executeCommand("id", "-u"); - String groupId = executeCommand("id", "-g"); + return engine; + } - return String.format("%s:%s", userId, groupId); + public boolean isRootless() throws IOException, InterruptedException { + String engine = getEngine(); + return engine.equals("docker-rootless") || engine.equals("podman"); } private static final Pattern hostnameMount = Pattern.compile("/containers/([a-z0-9]{64})/hostname");