feat(vars): admin CRUD API + pic vars CLI

Completes the vars half of Phase 3 end-to-end:
- vars_repo: VarOwner{Group|App} upsert/delete/list (owner-kind-specific
  SQL; ON CONFLICT restates the partial-index predicate).
- vars_api: GET/PUT/DELETE under /apps/{id}/vars and /groups/{id}/vars,
  resolve-then-require gated on App/GroupVars{Read,Write}, secrets-style
  error mapping + key/env-scope validation.
- pic vars ls/set/rm (--group|--app, --env, --json, --tombstone).
- journey test: a group var is inherited by a descendant app's
  vars::get(), and an app-level value overrides it (proximity) — green.

386 manager-core lib tests + the vars journey pass; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-24 21:11:46 +02:00
parent 343f6d3b4d
commit 9ee85993d8
11 changed files with 996 additions and 20 deletions

View File

@@ -144,6 +144,14 @@ enum Cmd {
cmd: MembersCmd,
},
/// Config vars (Phase 3) — set / list / delete group- or app-owned
/// env-scoped vars. Values inherit down the group tree; an app value
/// overrides an inherited one (proximity wins).
Vars {
#[command(subcommand)]
cmd: VarsCmd,
},
/// Files inspection — list a collection's blobs, download bytes, or
/// delete a file. Read + delete only; writes go through scripts.
Files {
@@ -1162,6 +1170,53 @@ enum SecretsCmd {
},
}
#[derive(Subcommand)]
enum VarsCmd {
/// List the owner's OWN vars (not the resolved/inherited view).
Ls {
/// Owning group (slug or id). Mutually exclusive with `--app`.
#[arg(long)]
group: Option<String>,
/// Owning app (slug or id). Mutually exclusive with `--group`.
#[arg(long)]
app: Option<String>,
},
/// Set a var. The value is stored as a JSON string by default; pass
/// `--json` to parse it as raw JSON. `--tombstone` writes a deletion
/// marker that suppresses an inherited key.
Set {
key: String,
/// Ignored (but accepted) when `--tombstone` is set.
#[arg(default_value = "")]
value: String,
#[arg(long)]
group: Option<String>,
#[arg(long)]
app: Option<String>,
/// Environment scope (`*` = env-agnostic, the default).
#[arg(long)]
env: Option<String>,
/// Parse `value` as raw JSON instead of a string literal.
#[arg(long)]
json: bool,
/// Write a tombstone (suppress an inherited key) instead of a value.
#[arg(long)]
tombstone: bool,
},
/// Delete a var by key (optionally scoped to one environment).
Rm {
key: String,
#[arg(long)]
group: Option<String>,
#[arg(long)]
app: Option<String>,
#[arg(long)]
env: Option<String>,
},
}
#[derive(Subcommand)]
enum AdminsCmd {
/// List admin accounts.
@@ -1761,6 +1816,41 @@ async fn main() -> ExitCode {
Cmd::Members {
cmd: MembersCmd::Rm { app, user_id },
} => cmds::members::rm(&app, &user_id).await,
Cmd::Vars {
cmd: VarsCmd::Ls { group, app },
} => cmds::vars::ls(group.as_deref(), app.as_deref(), mode).await,
Cmd::Vars {
cmd:
VarsCmd::Set {
key,
value,
group,
app,
env,
json,
tombstone,
},
} => {
cmds::vars::set(
group.as_deref(),
app.as_deref(),
&key,
&value,
env.as_deref(),
json,
tombstone,
)
.await
}
Cmd::Vars {
cmd:
VarsCmd::Rm {
key,
group,
app,
env,
},
} => cmds::vars::rm(group.as_deref(), app.as_deref(), &key, env.as_deref()).await,
Cmd::Files {
cmd:
FilesCmd::Ls {