Skip to content

feat(compose): add --configs-path and --ignore-volume-source overrides - #555

Open
smrutisenapati wants to merge 4 commits into
develfrom
feat/compose-configs-path-override
Open

feat(compose): add --configs-path and --ignore-volume-source overrides#555
smrutisenapati wants to merge 4 commits into
develfrom
feat/compose-configs-path-override

Conversation

@smrutisenapati

@smrutisenapati smrutisenapati commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds --configs-path to rio compose generate and rio compose up, letting users bind-mount a local directory in place of /opt/rapyuta/configs on the host side of generated volume mounts (both the default volume mount and any deployment volumes[].subPath under that prefix).
  • Adds --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/configs and independent of --configs-path -- it applies to any absolute host path a deployment declares as a volume source, whether or not --configs-path is also given.
  • Container-side mount paths and environment variable values referencing /opt/rapyuta/configs are left unchanged by both flags, so app behavior inside the container is unaffected -- only the host source of the bind mount changes.
  • up needed the same changes since it calls the shared generate_compose_file() to regenerate the compose doc on every invocation.
  • Fixed the init-fixperms dependency-wiring check to parse volume strings from the right (_get_volume_target) instead of vol.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-path override.

Test plan

  • uv run pytest tests/unit/ -- 246 passed
  • uv run ruff check . / uv run ruff format --check . -- clean
  • Manual verification against a dev environment: generated compose with/without --configs-path/--ignore-volume-source for a dummy Package/Deployment manifest, confirmed via docker inspect that the default mount, custom volume subPaths, ignore/re-include patterns (with and without --configs-path), and full-path matching all behave as documented
  • rio compose up --configs-path ./local-configs followed by docker exec ... cat /opt/rapyuta/configs/server/hello.txt returned content from the local override file, confirming the mount worked end-to-end
  • Cross-repo CLI test coverage added in rapyuta-io-tests#592 (6 tests: base --configs-path, ignore-drop, glob-drop, negate-reinclude, and standalone use of --ignore-volume-source without --configs-path), run against the AppImage build of this branch (commit f5b7199, rebased onto current devel) -- all pass, no regressions in test_compose.py/test_compose_negative.py (1 pre-existing, unrelated Click-message-format failure reproduces identically against plain devel)
  • Rebased onto current devel

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 15, 2026 07:36
@smrutisenapati
smrutisenapati requested review from a team as code owners July 15, 2026 07:36
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🤖 Pull Request Artifacts (#30248815959) 🎉

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-path flag to rio compose generate and rio 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[].subPath entries 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

Comment thread riocli/compose/up.py Outdated
Comment thread riocli/compose/generate.py Outdated
smrutisenapati and others added 3 commits July 27, 2026 16:20
…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>
@smrutisenapati
smrutisenapati force-pushed the feat/compose-configs-path-override branch from fea87bd to 722d6ff Compare July 27, 2026 07:22
@smrutisenapati smrutisenapati changed the title feat(compose): add --configs-path flag to override /opt/rapyuta/configs mounts feat(compose): add --configs-path and --configs-path-ignore overrides Jul 27, 2026
…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>
@smrutisenapati smrutisenapati changed the title feat(compose): add --configs-path and --configs-path-ignore overrides feat(compose): add --configs-path and --ignore-volume-source overrides Jul 27, 2026

@ankitrgadiya ankitrgadiya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification

Ran in a clean worktree at f5b7199:

  • uv run pytest tests/unit/ — 246 passed
  • uv run ruff check . / ruff format --check . — clean
  • rio compose generate --help — the long --ignore-volume-source string and the backslash-continued docstring examples render fine
  • End-to-end rio compose generate against a hand-written Package + Deployment with --configs-path and --ignore-volume-source: default mount rewritten, custom subPaths rewritten, the ignored bind dropped, /var/spool/print/csv left alone. Behaves as the description says.
  • Probed _get_volume_target directly against 8 volume-string shapes — it returns the container path correctly for every shape build_volume_mounts can emit.

Blocking

  • With --configs-path, init-fixperms now runs chown -R / chmod as root against the developer's local directory (populate.py:88). Verified by generating the compose file and reading the emitted init-fixperms service. Either skip the fixups when configs_path is set, or say so in the flag help — right now nothing warns the user.
  • rio compose down did not get the two new flags, so the third caller of generate_compose_file() can write a compose file with un-overridden /opt/rapyuta/configs paths (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 subPath with no local counterpart. _build_fixup_cmd's own docstring documents that Docker pre-creates a directory at a missing bind source. With --configs-path that now happens inside the user's tree. Please run rio compose up --configs-path <dir> where one declared subPath is absent locally, and confirm what ends up on disk.

Comment thread riocli/compose/up.py
Comment on lines +157 to +158
configs_path=configs_path.as_posix() if configs_path else None,
ignore_volume_source=ignore_volume_source,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +88 to +90
fixup_vols = get_volumes_requiring_fixup(
processed_deployments, configs_path, ignore_volume_source
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:rw

Bind 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +94 to +99
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.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +163 to +176
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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants