diff --git a/src/ria_toolkit_oss/app/cli.py b/src/ria_toolkit_oss/app/cli.py index c70eb16..6cd0c1c 100644 --- a/src/ria_toolkit_oss/app/cli.py +++ b/src/ria_toolkit_oss/app/cli.py @@ -32,10 +32,11 @@ _LABEL_HARDWARE = "ria.hardware" _LABEL_APP = "ria.app" -def _engine() -> str: +def _engine(cfg: _config.AppConfig, sudo_override: bool = False) -> list[str]: for exe in ("docker", "podman"): if shutil.which(exe): - return exe + use_sudo = sudo_override or cfg.sudo + return (["sudo", exe] if use_sudo else [exe]) print("error: neither 'docker' nor 'podman' found on PATH", file=sys.stderr) sys.exit(2) @@ -62,10 +63,10 @@ def _container_name(ref: str) -> str: return f"ria-app-{name}" -def _inspect_labels(engine: str, ref: str) -> dict: +def _inspect_labels(engine: list[str], ref: str) -> dict: try: out = subprocess.check_output( - [engine, "image", "inspect", "--format", "{{json .Config.Labels}}", ref], + [*engine, "image", "inspect", "--format", "{{json .Config.Labels}}", ref], stderr=subprocess.DEVNULL, ) except subprocess.CalledProcessError: @@ -102,35 +103,38 @@ def _cmd_configure(args: argparse.Namespace) -> int: cfg.registry = args.registry if args.namespace: cfg.namespace = args.namespace + if args.sudo is not None: + cfg.sudo = args.sudo path = _config.save(cfg) print(f"Saved app config to {path}") print(f" registry: {cfg.registry or '(unset)'}") print(f" namespace: {cfg.namespace or '(unset)'}") + print(f" sudo: {cfg.sudo}") return 0 def _cmd_pull(args: argparse.Namespace) -> int: - engine = _engine() cfg = _config.load() + engine = _engine(cfg, args.sudo) ref = _resolve_ref(args.app, cfg) print(f"Pulling {ref}") - return subprocess.call([engine, "pull", ref]) + return subprocess.call([*engine, "pull", ref]) def _cmd_run(args: argparse.Namespace) -> int: - engine = _engine() cfg = _config.load() + engine = _engine(cfg, args.sudo) ref = _resolve_ref(args.app, cfg) if not _inspect_labels(engine, ref): - rc = subprocess.call([engine, "pull", ref]) + rc = subprocess.call([*engine, "pull", ref]) if rc != 0: return rc labels = _inspect_labels(engine, ref) hw_flags = _hardware_flags(labels) - cmd = [engine, "run", "--rm"] + cmd = [*engine, "run", "--rm"] if not args.foreground: cmd += ["-d"] cmd += ["--name", args.name or _container_name(ref)] @@ -161,11 +165,12 @@ def _cmd_run(args: argparse.Namespace) -> int: return subprocess.call(cmd) -def _cmd_list(_args: argparse.Namespace) -> int: - engine = _engine() +def _cmd_list(args: argparse.Namespace) -> int: + cfg = _config.load() + engine = _engine(cfg, args.sudo) return subprocess.call( [ - engine, + *engine, "images", "--filter", f"label={_LABEL_APP}", @@ -176,15 +181,17 @@ def _cmd_list(_args: argparse.Namespace) -> int: def _cmd_stop(args: argparse.Namespace) -> int: - engine = _engine() - name = args.name or _container_name(_resolve_ref(args.app, _config.load())) - return subprocess.call([engine, "stop", name]) + cfg = _config.load() + engine = _engine(cfg, args.sudo) + name = args.name or _container_name(_resolve_ref(args.app, cfg)) + return subprocess.call([*engine, "stop", name]) def _cmd_logs(args: argparse.Namespace) -> int: - engine = _engine() - name = args.name or _container_name(_resolve_ref(args.app, _config.load())) - cmd = [engine, "logs"] + cfg = _config.load() + engine = _engine(cfg, args.sudo) + name = args.name or _container_name(_resolve_ref(args.app, cfg)) + cmd = [*engine, "logs"] if args.follow: cmd += ["-f"] cmd += [name] @@ -193,11 +200,19 @@ def _cmd_logs(args: argparse.Namespace) -> int: def main() -> None: parser = argparse.ArgumentParser(prog="ria-app") + parser.add_argument("--sudo", action="store_true", default=False, help="Run docker/podman via sudo") sub = parser.add_subparsers(dest="command", required=True) p_cfg = sub.add_parser("configure", help="Set default registry/namespace") p_cfg.add_argument("--registry", default=None, help="Default container registry (e.g. registry.riahub.ai)") p_cfg.add_argument("--namespace", default=None, help="Default namespace (e.g. qoherent)") + p_cfg.add_argument( + "--sudo", + dest="sudo", + action=argparse.BooleanOptionalAction, + default=None, + help="Persist sudo default (--sudo / --no-sudo)", + ) p_pull = sub.add_parser("pull", help="Pull an app image") p_pull.add_argument("app", help="App name or image reference") diff --git a/src/ria_toolkit_oss/app/config.py b/src/ria_toolkit_oss/app/config.py index 2594761..8bff807 100644 --- a/src/ria_toolkit_oss/app/config.py +++ b/src/ria_toolkit_oss/app/config.py @@ -22,6 +22,7 @@ _DEFAULT_PATH = Path(os.environ.get("RIA_TOOLKIT_CONFIG", str(Path.home() / ".ri class AppConfig: registry: str = "" namespace: str = "" + sudo: bool = False def default_path() -> Path: @@ -39,6 +40,7 @@ def load(path: Path | None = None) -> AppConfig: return AppConfig( registry=data.get("registry", "") or os.environ.get("RIA_REGISTRY", ""), namespace=data.get("namespace", "") or os.environ.get("RIA_NAMESPACE", ""), + sudo=bool(data.get("sudo", False)) or os.environ.get("RIA_DOCKER_SUDO", "") not in ("", "0", "false"), )