fix(groovy): default the Docker runner to root for Commands tasks - #414
Conversation
On the Docker task runner, the working directory can be created with an
owner that differs from the container image's default user. On a non-root
image such as `groovy:jdk21` (`USER groovy`, uid 1000), a `groovy.Commands`
task could not write into its own working directory, so `outputFiles`
failed with `Permission denied` and captured 0 files.
`groovy.Script` already forces `user("root")` on its Docker runner and was
therefore unaffected; `Commands` had no equivalent.
Default the Docker runner's user to `root` when the user did not configure
one explicitly, on both the `taskRunner` path and the deprecated `docker`
property path (`CommandsWrapper.getTaskRunner()` rebuilds the runner from
`dockerOptions`, so `withTaskRunner()` alone is discarded there). Unlike
`Script`, the runner is not replaced wholesale, so any other Docker options
the user configured are preserved.
Closes #411
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📦 Artifacts
🧪 Java Unit Tests
🔁 Unreleased Commits7 commits since
|
Tests report quick summary:success ✅ > tests: 246, success: 246, skipped: 0, failed: 0 unfold for details
|
There was a problem hiding this comment.
Kestra Plugin Code Review
Business Requirements — partial
Issue #411: Docker runner creates the working directory as root while Commands runs as the image's non-root user, breaking outputFiles (matches Script, which already forces root). The fix covers both the modern taskRunner: path (run()) and the deprecated docker: path (injectDefaults()), and includes two regression tests. Scope was verified against bun/deno (unaffected), which is good discipline.
However: the linked issue's reporter environment is Kestra 2.0.0-rc10. Decompiling script-2.0.0-SNAPSHOT.jar vs the currently-pinned script-1.3.19.jar (this repo's gradle.properties) confirms AbstractExecScript no longer declares a docker field / getDocker() in 2.0.0-SNAPSHOT — only the vestigial injectDefaults(DockerOptions) overload remains. That means the legacy-path half of this fix (injectDefaults() override, Commands.java lines 94-105) and its dedicated test outputFilesOnNonRootImageLegacyDockerProperty() (which calls .docker(DockerOptions...), CommandsTest.java line 125) will not compile once this plugin bumps kestraVersion toward the 2.0 line the issue was actually filed against. Worth confirming which core version this fix is meant to ship against before merge.
Kestra Guidelines — 3 findings
- 🟠
Commands.javaline ~120: Undocumented behavior change — Docker now defaults torootfor everycontainerImage/taskRunnercombination used byCommands(not justgroovy:jdk21), with no@Schemaor plugin-doc (io.kestra.plugin.scripts.groovy.md) update explaining the new default. - 🟡
CommandsTest.javaline 151 (buildNonRootTestImage): test infra overengineering — a new test-only dependency (docker-java-api) plus building/pushing/removing a throwaway custom image just to get a non-root uid, whenDockerOptions.user/Docker.useris a plain numeric-uid-capable String —.user("5000")on the stockgroovy:jdk21image would reproduce the same bug without any of this. - 🟢
CommandsTest.javaline ~75-79: the 5-line comment blocks above both new test methods exceed the "one short line max" comment guideline; trim to a single line + issue link.
Security (OWASP Top 10:2025 + KPS) — 1 finding
- 🟠 A02:2025 Security Misconfiguration (
Commands.javaline ~120): forcing every Docker-basedCommandsexecution to run asrootby default broadens the blast radius of the fully flow-author-controlledcommands:property (arbitrary shell/groovy commands) for any custom image the user supplies, not just the reported one. This is the same trade-off already made inScript.java, but silently extending it toCommands.javawithout documentation removes the user's ability to know they need to opt out viauser. The PR description itself invites this feedback ("Happy to instead scope it behind something more explicit") — recommend either documenting the new default prominently, or narrowing the fix to actually chown/chmod the working directory to the container's runtime user instead of running the whole command as root.
Performance — 1 finding
- 🟡
CommandsTest.javaline 163-164:buildImageCmd(...).start().awaitImageId()has no timeout; a stalled Docker build/daemon would hang the test run indefinitely. Not a production code path, low severity, and avoidable entirely per the test-simplification suggestion above.
Verdict: REQUEST CHANGES
(Note: this review was submitted via the API in a degraded form — a diagnostic single-comment call was accidentally submitted as a review before the full multi-comment review could be posted; this body was then updated in place to carry the complete findings since GitHub does not allow converting an already-submitted review's event/state after the fact. Please treat the verdict above, not the "Commented" badge, as authoritative.)
Address PR #414 review: - document the Docker root default on `Commands` (class `@Schema` and the plugin doc page) so users can opt out via `taskRunner.user`; - correct the plugin doc, which claimed `Script` runs in-process — it runs on a task runner, Docker by default; - bound the test image build with a timeout so a stalled daemon cannot hang the test run; - trim the oversized comment blocks in `Commands` and `CommandsTest`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review — pushed b0693ea addressing most of it. Point-by-point: 🟠 Root-by-default undocumented — fixedAgreed, this was the weakest part of the PR. Now documented in two places:
I kept the root default rather than switching to a While in that doc I also corrected an unrelated inaccuracy it asserted: it claimed 🟡
|
Closes #411
Problem
On the Docker task runner, the working directory can end up owned by a user other than the container image's default user. On a non-root image such as
groovy:jdk21(USER groovy, uid 1000), agroovy.Commandstask cannot write into its own working directory, sooutputFilesfails withFileNotFoundException: out.txt (Permission denied)and captures 0 files.groovy.Scriptalready forcesuser("root")on its Docker runner and is therefore unaffected —Commandshad no equivalent, which is the inconsistency reported in the issue.Fix
Default the Docker runner's
usertorootwhen the user has not configured one explicitly, on both config paths:taskRunner:path, inCommands.run();docker:property path, inCommands.injectDefaults()— necessary becauseCommandsWrapper.getTaskRunner()rebuilds the runner fromdockerOptionson every call and discards anywithTaskRunner(...)override.Unlike
Script(which replaces the task runner wholesale and drops any custom Docker options), this preserves every other option the user configured — volumes, memory, cpu, pull policy, credentials — and never overrides an explicitly setuser.Scope
Checked the other modules with a non-root-looking default image; both
oven/bunanddenoland/denocreate a non-root account but neverUSER-switch to it, so they still run as root (docker run --rm oven/bun id→uid=0(root), same fordenoland/deno). Groovy is the only affected module.Tests
plugin-script-groovy/src/test/java/io/kestra/plugin/scripts/groovy/CommandsTest.java:outputFilesOnNonRootImage()— moderntaskRunner:pathoutputFilesOnNonRootImageLegacyDockerProperty()— deprecateddocker:pathBoth build a throwaway image
FROM groovy:jdk21with a user at uid 5000, so the reproduction does not depend on the host uid coincidentally matching the image's default uid (withgroovy:jdk21's own uid 1000 the bug reproduces only on some hosts/engines). Verified TDD-style: without the fix both fail withuid=5000andPermission denied; with it both loguid=0(root)and captureout.txt../gradlew :plugin-script-groovy:test— BUILD SUCCESSFUL, 3 tests, 0 failures.Note for reviewers
Running containers as root by default is a real behavior change for anyone relying on the image's non-root user. It matches what
groovy.Scripthas always done, and settingtaskRunner.userexplicitly opts out. Happy to instead scope it behind something more explicit if you'd prefer.🤖 Generated with Claude Code