From 7e0216e5ecb48ea8a0cc4ce6b7302ef9a11c2b43 Mon Sep 17 00:00:00 2001 From: sanha Date: Fri, 25 Sep 2026 15:15:21 +0900 Subject: [PATCH] fix(groovy): preserve the configured Script task runner --- .../kestra/plugin/scripts/groovy/Script.java | 24 ++-- .../scripts/groovy/ScriptTaskRunnerTest.java | 105 ++++++++++++++++++ 2 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 plugin-script-groovy/src/test/java/io/kestra/plugin/scripts/groovy/ScriptTaskRunnerTest.java diff --git a/plugin-script-groovy/src/main/java/io/kestra/plugin/scripts/groovy/Script.java b/plugin-script-groovy/src/main/java/io/kestra/plugin/scripts/groovy/Script.java index e545f191..5e8f5a7b 100644 --- a/plugin-script-groovy/src/main/java/io/kestra/plugin/scripts/groovy/Script.java +++ b/plugin-script-groovy/src/main/java/io/kestra/plugin/scripts/groovy/Script.java @@ -11,6 +11,7 @@ import io.kestra.core.models.property.Property; import io.kestra.core.models.tasks.RunnableTask; import io.kestra.core.models.tasks.runners.TargetOS; +import io.kestra.core.models.tasks.runners.TaskRunner; import io.kestra.core.runners.FilesService; import io.kestra.core.runners.RunContext; import io.kestra.plugin.scripts.exec.AbstractExecScript; @@ -31,7 +32,10 @@ @NoArgsConstructor @Schema( title = "Execute a Groovy script inline with your Flow Code", - description = "Runs an inline Groovy script in the JVM and captures its output." + description = """ + Runs an inline Groovy script in the JVM and captures its output. + + On the Docker task runner, the container runs as `root` unless `taskRunner.user` is set explicitly, so it can read the mounted script. Other task runner settings are preserved.""" ) @Plugin( examples = { @@ -94,7 +98,9 @@ protected DockerOptions injectDefaults(RunContext runContext, DockerOptions orig builder.image(runContext.render(this.getContainerImage()).as(String.class).orElse(null)); } - builder.user("root"); + if (original.getUser() == null) { + builder.user("root"); + } return builder.build(); } @@ -102,6 +108,12 @@ protected DockerOptions injectDefaults(RunContext runContext, DockerOptions orig public ScriptOutput run(RunContext runContext) throws Exception { CommandsWrapper commands = this.commands(runContext); + // The Groovy image user may not be able to read the mounted script. + TaskRunner taskRunner = commands.getTaskRunner(); + if (taskRunner instanceof Docker docker && docker.getUser() == null) { + commands = commands.withTaskRunner(docker.toBuilder().user("root").build()); + } + Map inputFiles = FilesService.inputFiles(runContext, commands.getTaskRunner().additionalVars(runContext, commands), this.getInputFiles()); Path relativeScriptPath = runContext.workingDir().path().relativize(runContext.workingDir().createTempFile(".groovy")); inputFiles.put( @@ -115,12 +127,6 @@ public ScriptOutput run(RunContext runContext) throws Exception { .withInterpreter(this.interpreter) .withBeforeCommands(beforeCommands) .withBeforeCommandsWithOptions(true) - .withTaskRunner( - // because of, we are mounting a volume and the uid running Docker is not 1000, so it should run as user root (-u root). - Docker.builder() - .user("root") - .build() - ) .withCommands( Property.ofValue( List.of( @@ -131,4 +137,4 @@ public ScriptOutput run(RunContext runContext) throws Exception { .withTargetOS(os) .run(); } -} \ No newline at end of file +} diff --git a/plugin-script-groovy/src/test/java/io/kestra/plugin/scripts/groovy/ScriptTaskRunnerTest.java b/plugin-script-groovy/src/test/java/io/kestra/plugin/scripts/groovy/ScriptTaskRunnerTest.java new file mode 100644 index 00000000..88e97c76 --- /dev/null +++ b/plugin-script-groovy/src/test/java/io/kestra/plugin/scripts/groovy/ScriptTaskRunnerTest.java @@ -0,0 +1,105 @@ +package io.kestra.plugin.scripts.groovy; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import io.kestra.core.junit.annotations.KestraTest; +import io.kestra.core.models.property.Property; +import io.kestra.core.models.tasks.runners.TaskRunner; +import io.kestra.core.runners.RunContext; +import io.kestra.core.runners.RunContextFactory; +import io.kestra.core.utils.TestsUtils; +import io.kestra.plugin.core.runner.Process; +import io.kestra.plugin.scripts.exec.scripts.models.DockerOptions; +import io.kestra.plugin.scripts.exec.scripts.runners.CommandsWrapper; +import io.kestra.plugin.scripts.runner.docker.Docker; +import io.kestra.plugin.scripts.runner.docker.PullPolicy; + +import jakarta.inject.Inject; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@KestraTest +class ScriptTaskRunnerTest { + @Inject + RunContextFactory runContextFactory; + + @Test + void preservesDockerOptionsWhenDefaultingToRoot() throws Exception { + Docker runner = Docker.builder().type(Docker.class.getName()) + .host("unix:///run/user/1000/podman/podman.sock") + .networkMode("host") + .pullPolicy(Property.ofValue(PullPolicy.NEVER)) + .build(); + + Docker effective = (Docker) runWith(runner); + + assertThat(effective.getHost(), is(runner.getHost())); + assertThat(effective.getNetworkMode(), is(runner.getNetworkMode())); + assertThat(effective.getPullPolicy(), is(runner.getPullPolicy())); + assertThat(effective.getUser(), is("root")); + assertThat(runner.getUser(), nullValue()); + } + + @Test + void defaultsToRootWithoutTaskRunner() throws Exception { + assertThat(((Docker) runWith(null)).getUser(), is("root")); + } + + @Test + void preservesExplicitDockerUser() throws Exception { + Docker runner = Docker.builder().type(Docker.class.getName()).user("1000:1000").build(); + + assertThat(runWith(runner), sameInstance(runner)); + } + + @Test + void preservesProcessRunner() throws Exception { + Process runner = Process.builder().type(Process.class.getName()).build(); + + assertThat(runWith(runner), sameInstance(runner)); + } + + @Test + void defaultsLegacyDockerUserWithoutOverridingExplicitUser() throws Exception { + Script script = Script.builder().id("groovy-script-" + UUID.randomUUID()).type(Script.class.getName()).script(Property.ofValue("println 'hello'")).build(); + RunContext runContext = TestsUtils.mockRunContext(runContextFactory, script, Map.of()); + + assertThat(script.injectDefaults(runContext, DockerOptions.builder().build()).getUser(), is("root")); + assertThat(script.injectDefaults(runContext, DockerOptions.builder().user("1000:1000").build()).getUser(), is("1000:1000")); + } + + private TaskRunner runWith(TaskRunner runner) throws Exception { + Script script = Script.builder() + .id("groovy-script-" + UUID.randomUUID()) + .type(Script.class.getName()) + .script(Property.ofValue("println 'hello'")) + .taskRunner(runner) + .build(); + RunContext runContext = TestsUtils.mockRunContext(runContextFactory, script, Map.of()); + AtomicReference> effective = new AtomicReference<>(runner); + try (var construction = mockConstruction(CommandsWrapper.class, withSettings().defaultAnswer(RETURNS_SELF), (commands, context) -> + { + when(commands.getWorkingDirectory()).thenReturn(runContext.workingDir().path()); + when(commands.getTaskRunner()).thenAnswer(invocation -> effective.get()); + when(commands.withTaskRunner(any())).thenAnswer(invocation -> + { + effective.set(invocation.getArgument(0)); + return commands; + }); + when(commands.render(any(), any())).thenReturn("println 'hello'"); + })) { + script.run(runContext); + verify(construction.constructed().getFirst()).run(); + } + return effective.get(); + } +}