feat(compose): add --configs-path and --ignore-volume-source overrides - #555
feat(compose): add --configs-path and --ignore-volume-source overrides#555smrutisenapati wants to merge 4 commits into
Conversation
|
🤖 Pull Request Artifacts (#30248815959) 🎉 |
There was a problem hiding this comment.
Pull request overview
Adds a --configs-path override to the rio compose workflow so users can redirect the host-side bind mount that normally sources from /opt/rapyuta/configs, without changing container-side mount paths.
Changes:
- Added
--configs-pathflag torio compose generateandrio compose up, passing the override through to compose generation. - Reworked default volume mount handling to support a configurable host-side configs directory (
get_default_volume_mounts,CONFIGS_DIR). - Updated compose population logic to rewrite host-side
volumes[].subPathentries that are under/opt/rapyuta/configs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
riocli/compose/up.py |
Adds --configs-path option and forwards it into compose generation. |
riocli/compose/generate.py |
Adds --configs-path option and threads it through generate_compose_file() into populate(). |
riocli/compose/populate.py |
Implements host-path substitution for configs mounts and applies it to default/custom mounts and fixperms volume handling. |
riocli/compose/defaults.py |
Introduces CONFIGS_DIR and a helper to generate default mounts with an optional host-side override. |
Comments suppressed due to low confidence (1)
riocli/compose/populate.py:83
- With the new --configs-path override, host-side volume strings may contain additional ':' segments (notably Windows drive letters like "C:/..."). The current volume-target detection uses vol.split(":")[1], which mis-parses such mounts and can prevent init-fixperms dependencies from being applied. Parse from the right (rsplit) so the target path is correctly extracted regardless of ':' in the source.
fixup_vols = get_volumes_requiring_fixup(processed_deployments, configs_path)
if fixup_vols:
fix_cmds = [_build_fixup_cmd(entry) for entry in fixup_vols]
fixperms_vols = [
f"{entry['host']}:{entry['container']}:rw" for entry in fixup_vols
…gs mounts Allows rio compose generate/up to bind-mount a local directory in place of /opt/rapyuta/configs on the host side of generated volume mounts, so compose manifests referencing that path can be tested locally without requiring the real device path to exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Annotate configs_path as Path | None in the up/generate CLI options to match the actual Click runtime value when the flag is omitted. - Fix init-fixperms dependency detection to parse volume strings from the right, so host paths containing extra ':' (e.g. Windows drive letters) no longer shift the container-path field position. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds gitignore-style patterns (relative to /opt/rapyuta/configs) for binds that should be dropped entirely instead of rewritten under --configs-path, for paths that have no local equivalent to point at. Patterns evaluate in order with last-match-wins, and a leading '!' re-includes a path an earlier broader pattern excluded (e.g. dropping everything under a directory except one specific file). Requires --configs-path; a UsageError catches the ignore-without-override case early. Also adds unit test coverage for the existing --configs-path substitution logic itself, which had none. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fea87bd to
722d6ff
Compare
…urce Drops the --configs-path dependency and rescopes matching from a CONFIGS_DIR-relative path to a volume's full host-side path, so a bind can be dropped for any absolute path a deployment declares as a volume source -- not just ones under /opt/rapyuta/configs, and whether or not --configs-path is also given. Ignoring and redirecting are independent operations on the same bind: ignore_patterns is checked first in _substitute_configs_path, then the configs_path rewrite (if any) applies to what's left. Updates test coverage to match: patterns are now full absolute paths, the "requires --configs-path" tests are replaced with tests confirming the flag is accepted standalone, and _is_ignored_volume_source / _substitute_configs_path get direct coverage for the no-configs_path case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ankitrgadiya
left a comment
There was a problem hiding this comment.
Verification
Ran in a clean worktree at f5b7199:
uv run pytest tests/unit/— 246 passeduv run ruff check ./ruff format --check .— cleanrio compose generate --help— the long--ignore-volume-sourcestring and the backslash-continued docstring examples render fine- End-to-end
rio compose generateagainst a hand-written Package + Deployment with--configs-pathand--ignore-volume-source: default mount rewritten, customsubPaths rewritten, the ignored bind dropped,/var/spool/print/csvleft alone. Behaves as the description says. - Probed
_get_volume_targetdirectly against 8 volume-string shapes — it returns the container path correctly for every shapebuild_volume_mountscan emit.
Blocking
- With
--configs-path,init-fixpermsnow runschown -R/chmodas root against the developer's local directory (populate.py:88). Verified by generating the compose file and reading the emittedinit-fixpermsservice. Either skip the fixups whenconfigs_pathis set, or say so in the flag help — right now nothing warns the user. rio compose downdid not get the two new flags, so the third caller ofgenerate_compose_file()can write a compose file with un-overridden/opt/rapyuta/configspaths (up.py:157).
Question
Is Windows support planned for rio compose? It decides whether _get_volume_target is load-bearing or dead defensive code — see the comment on populate.py:163.
Please confirm before merge
- A
subPathwith no local counterpart._build_fixup_cmd's own docstring documents that Docker pre-creates a directory at a missing bind source. With--configs-paththat now happens inside the user's tree. Please runrio compose up --configs-path <dir>where one declaredsubPathis absent locally, and confirm what ends up on disk.
| configs_path=configs_path.as_posix() if configs_path else None, | ||
| ignore_volume_source=ignore_volume_source, |
There was a problem hiding this comment.
issue (blocking): rio compose down is the third caller of generate_compose_file() and did not get either flag.
The description gives "it calls the shared generate_compose_file() to regenerate the compose doc on every invocation" as the reason up needed the same treatment. down.py:111 shares that property — it regenerates and then write_compose_yamls the result whenever the compose file is missing or empty — but it still calls generate_compose_file() with only ctx/values/secrets/files.
Concrete effect: rio compose down in a directory with no docker-compose.yaml writes one whose binds all point at /opt/rapyuta/configs, regardless of what the services were actually started with. Anything run against that file afterwards by hand — docker compose -f docker-compose.yaml ps/up/logs — uses the device paths rather than the local ones.
To be straight about the severity: I could not make down itself misbehave, because docker compose down identifies containers by project and service name rather than by volume, and up regenerates unconditionally. So this is the flag surface and the file it leaves on disk, not the teardown. Adding the two options to down and forwarding them keeps the three entry points consistent.
| fixup_vols = get_volumes_requiring_fixup( | ||
| processed_deployments, configs_path, ignore_volume_source | ||
| ) |
There was a problem hiding this comment.
issue (blocking): With --configs-path, init-fixperms runs chown -R and chmod as root against the developer's local directory.
Threading configs_path into get_volumes_requiring_fixup rewrites entry["host"], so the init-fixperms service (user: "0:0") binds the local path instead of the device's /opt/rapyuta/configs. Generated from a manifest declaring subPath: /opt/rapyuta/configs/wms/settings.yaml, uid: 1000, gid: 1000, perm: 644 with --configs-path ./local-configs:
init-fixperms:
user: 0:0
command:
- sh
- -c
- if [ -f /app/settings.yaml ]; then chown 1000:1000 /app/settings.yaml && chmod
644 /app/settings.yaml; else mkdir -p /app/settings.yaml && chown -R 1000:1000
/app/settings.yaml && chmod 644 /app/settings.yaml; fi
volumes:
- <abs>/local-configs/wms/settings.yaml:/app/settings.yaml:rwBind mounts share the inode, so rio compose up --configs-path ./local-configs rewrites ownership and mode of the developer's own files. Point the flag at a git checkout of config templates and the checkout comes back owned by 1000:1000 (or 0:0, for a manifest declaring uid: 0) and needs sudo to edit again. Worse when a declared subPath has no local counterpart: per _build_fixup_cmd's docstring, Docker pre-creates a directory at the missing bind source, and the else branch then chown -Rs it — so root-owned junk directories appear inside the local config tree.
On a real device that side effect is the point. On a developer's laptop it is not, and neither the flag help nor the PR description mentions it. Two options that both work: skip the fixups entirely when configs_path is set (the local files are the developer's to own), or keep them and say so in the --configs-path help so the choice is informed.
| """ | ||
| # Start with default volume mounts | ||
| service_volumes = DEFAULT_VOLUME_MOUNTS.copy() | ||
| service_volumes = get_default_volume_mounts(configs_path) |
There was a problem hiding this comment.
suggestion: --ignore-volume-source silently cannot drop any of the five default mounts.
get_default_volume_mounts() never sees ignore_volume_source, so the patterns only ever apply to deployment-declared volumes. test_default_top_level_mount_unaffected_by_ignore pins that for /opt/rapyuta/configs and the reasoning there is sound — but the same is true of /var/log/riouser, /var/log/rapyuta/deployments, /var/lib/docker/containers and /dev, and those are exactly the paths the flag's stated purpose ("paths that have no local equivalent") describes on a dev machine. --ignore-volume-source '/var/lib/docker/containers' is accepted and does nothing.
The help at generate.py:94 says "matched against a volume's full host-side path -- drops the bind entirely" with no scoping caveat, so there is nothing to tell the user which volumes are eligible. Either filter the defaults through _is_ignored_volume_source too, or add "applies to volumes declared by a deployment; the default mounts are not affected" to the help.
| help="gitignore-style pattern matched against a volume's full host-side path -- drops " | ||
| "the bind entirely instead of mounting it. Repeatable; evaluated in order, last match " | ||
| "wins; prefix with '!' to re-include a path an earlier pattern excluded (e.g. " | ||
| "--ignore-volume-source '/opt/rapyuta/configs/station/*' --ignore-volume-source " | ||
| "'!/opt/rapyuta/configs/station/sim-nginx.conf.template'). Independent of --configs-path " | ||
| "-- applies whether or not that flag is also given.", |
There was a problem hiding this comment.
suggestion (non-blocking): Say that patterns match the manifest's subPath, not the rewritten path.
_substitute_configs_path checks the ignore patterns before applying configs_path, so a pattern must be written against the path as the manifest declares it (/opt/rapyuta/configs/auth/*) even when --configs-path ./local-configs is in play. "a volume's full host-side path" reads the other way — with --configs-path given, the host side of the generated bind is ./local-configs/auth/..., and a pattern written against that never matches.
The examples happen to show the right form, but only implicitly. One clause — "matched against the volume's subPath as declared in the manifest, before any --configs-path rewrite" — makes it explicit. Same in up.py:87.
| def _get_volume_target(vol: str) -> str | None: | ||
| """Extracts the container-side mount path from a compose volume string. | ||
|
|
||
| Parses from the right so host paths containing extra ':' (e.g. Windows | ||
| drive letters like "C:/...") don't shift the field positions. The trailing | ||
| segment is treated as a mode ("rw", "ro", "rslave", ...) rather than the | ||
| container path whenever it doesn't look like an absolute path. | ||
| """ | ||
| parts = vol.split(":") | ||
| if len(parts) < 2: | ||
| return None | ||
| if len(parts) >= 3 and not parts[-1].startswith("/"): | ||
| return parts[-2] | ||
| return parts[-1] |
There was a problem hiding this comment.
question: Is Windows support planned for rio compose? If not, what does this function protect against?
The docstring gives Windows drive letters as the motivation, and that is the only case where it differs from the vol.split(":")[1] it replaces. I checked every volume-string shape build_volume_mounts can emit on a POSIX host and the two parses agree on all of them:
| volume string | _get_volume_target |
vol.split(":")[1] |
|---|---|---|
/host:/container |
/container |
/container |
/host:/container:rw |
/container |
/container |
myvol:/data:ro |
/data |
/data |
/data |
None |
None |
C:/local/x:/container:rw |
/container |
/local/x |
Only the last row diverges. The nearest POSIX equivalent is a host path containing a literal colon — legal on Linux, and newly reachable now that --configs-path lets the user name any directory — but Compose's short volume syntax cannot express it either way, so it is not really a case this rescues.
So: if Windows is on the roadmap, this is groundwork and worth keeping (please say so in the docstring, and the .as_posix() calls at generate.py:189 / up.py:157 become part of the same story). If it is not, then the init-fixperms wiring bug being fixed here is unreachable, and the change is hardening with no failure behind it — still fine to keep, but the docstring should not imply otherwise.
| isinstance(vol, str) | ||
| and len(vol.split(":")) >= 2 | ||
| and vol.split(":")[1] in affected_paths | ||
| isinstance(vol, str) and _get_volume_target(vol) in affected_paths |
There was a problem hiding this comment.
todo (non-blocking): Nothing tests the init-fixperms depends_on wiring.
No test under tests/unit/compose/ references _get_volume_target, and none asserts that a service declaring a uid/gid/perm volume ends up with depends_on: {init-fixperms: service_completed_successfully}. So this whole block is unverified by the suite — break the match here and all 246 tests still pass, while containers start before the permission fixup has run.
The 248 new lines in test_populate.py all exercise the helpers directly; one test that drives populate() end to end and asserts the depends_on edge would cover the part with real consequences.
Summary
--configs-pathtorio compose generateandrio compose up, letting users bind-mount a local directory in place of/opt/rapyuta/configson the host side of generated volume mounts (both the default volume mount and any deploymentvolumes[].subPathunder that prefix).--ignore-volume-source(repeatable, gitignore-style, matched against a volume's full host-side path) to drop specific binds entirely instead of mounting them, for paths that have no local equivalent. Patterns evaluate in order with last-match-wins, and a leading!re-includes a path an earlier, broader pattern excluded (e.g. drop everything under a directory except one file). Not scoped to/opt/rapyuta/configsand independent of--configs-path-- it applies to any absolute host path a deployment declares as a volume source, whether or not--configs-pathis also given./opt/rapyuta/configsare left unchanged by both flags, so app behavior inside the container is unaffected -- only the host source of the bind mount changes.upneeded the same changes since it calls the sharedgenerate_compose_file()to regenerate the compose doc on every invocation.init-fixpermsdependency-wiring check to parse volume strings from the right (_get_volume_target) instead ofvol.split(":")[1], so host paths containing extra:(e.g. Windows drive letters) no longer shift the container-path field position -- found during review of the--configs-pathoverride.Test plan
uv run pytest tests/unit/-- 246 passeduv run ruff check ./uv run ruff format --check .-- clean--configs-path/--ignore-volume-sourcefor a dummy Package/Deployment manifest, confirmed viadocker inspectthat the default mount, custom volume subPaths, ignore/re-include patterns (with and without--configs-path), and full-path matching all behave as documentedrio compose up --configs-path ./local-configsfollowed bydocker exec ... cat /opt/rapyuta/configs/server/hello.txtreturned content from the local override file, confirming the mount worked end-to-end--configs-path, ignore-drop, glob-drop, negate-reinclude, and standalone use of--ignore-volume-sourcewithout--configs-path), run against the AppImage build of this branch (commitf5b7199, rebased onto currentdevel) -- all pass, no regressions intest_compose.py/test_compose_negative.py(1 pre-existing, unrelated Click-message-format failure reproduces identically against plain devel)devel🤖 Generated with Claude Code