Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down Expand Up @@ -94,14 +98,22 @@ 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();
}

@Override
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<String, String> inputFiles = FilesService.inputFiles(runContext, commands.getTaskRunner().additionalVars(runContext, commands), this.getInputFiles());
Path relativeScriptPath = runContext.workingDir().path().relativize(runContext.workingDir().createTempFile(".groovy"));
inputFiles.put(
Expand All @@ -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(
Expand All @@ -131,4 +137,4 @@ public ScriptOutput run(RunContext runContext) throws Exception {
.withTargetOS(os)
.run();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TaskRunner<?>> 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();
}
}