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:
@@ -37,3 +37,4 @@ mod scripts;
|
||||
mod secrets;
|
||||
mod staleness;
|
||||
mod triggers;
|
||||
mod vars;
|
||||
|
||||
4
crates/picloud-cli/tests/fixtures/read-var.rhai
vendored
Normal file
4
crates/picloud-cli/tests/fixtures/read-var.rhai
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Phase-3 vars journey fixture: returns the resolved `region` config var
|
||||
// verbatim so the test can assert on inheritance (group value) vs an app
|
||||
// proximity override.
|
||||
vars::get("region")
|
||||
84
crates/picloud-cli/tests/vars.rs
Normal file
84
crates/picloud-cli/tests/vars.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! Phase-3 config `vars`, end to end via `pic`: a group-owned var is
|
||||
//! inherited by an app underneath it, and an app-owned var of the same key
|
||||
//! overrides the inherited value (proximity wins, §3).
|
||||
//!
|
||||
//! Drives the real resolution path: a script does `vars::get("region")`
|
||||
//! and returns it; we invoke it via `/api/v1/execute/{id}` and assert on
|
||||
//! the body. First the group value flows down (inheritance), then an app
|
||||
//! value shadows it (override).
|
||||
|
||||
use crate::common;
|
||||
use crate::common::cleanup::{AppGuard, GroupGuard};
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn group_var_is_inherited_then_app_value_overrides() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let acme = common::unique_slug("v-acme");
|
||||
let app = common::unique_slug("v-app");
|
||||
|
||||
// Group `acme`, then a `region` var on it (a JSON string "eu").
|
||||
let _g_acme = GroupGuard::new(&env.url, &env.token, &acme);
|
||||
common::pic_as(&env)
|
||||
.args(["groups", "create", &acme])
|
||||
.assert()
|
||||
.success();
|
||||
common::pic_as(&env)
|
||||
.args(["vars", "set", "region", "eu", "--group", &acme])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// App under acme with a script that reads + returns the resolved var.
|
||||
let _app = AppGuard::new(&env.url, &env.token, &app);
|
||||
common::pic_as(&env)
|
||||
.args(["apps", "create", &app, "--group", &acme])
|
||||
.assert()
|
||||
.success();
|
||||
let fixture = common::fixture_path("read-var.rhai");
|
||||
common::pic_as(&env)
|
||||
.args([
|
||||
"scripts",
|
||||
"deploy",
|
||||
fixture.to_str().unwrap(),
|
||||
"--app",
|
||||
&app,
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Resolve the deployed script's id.
|
||||
let ls = common::pic_as(&env)
|
||||
.args(["scripts", "ls", "--app", &app])
|
||||
.output()
|
||||
.expect("scripts ls");
|
||||
let id = common::parse_first_id(std::str::from_utf8(&ls.stdout).unwrap())
|
||||
.expect("scripts ls should produce one row");
|
||||
|
||||
// Inherited: the app has no own `region`, so the group's "eu" resolves.
|
||||
assert_eq!(invoke_body(&env, &id), serde_json::json!("eu"), "inherited");
|
||||
|
||||
// Proximity override: an app-owned `region` shadows the group value.
|
||||
common::pic_as(&env)
|
||||
.args(["vars", "set", "region", "us", "--app", &app])
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(invoke_body(&env, &id), serde_json::json!("us"), "override");
|
||||
}
|
||||
|
||||
/// Invoke a script via `pic scripts invoke <id>` (→ `/api/v1/execute/{id}`)
|
||||
/// and parse its JSON body.
|
||||
fn invoke_body(env: &common::TestEnv, id: &str) -> serde_json::Value {
|
||||
let out = common::pic_as(env)
|
||||
.args(["scripts", "invoke", id])
|
||||
.output()
|
||||
.expect("scripts invoke");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"invoke failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
serde_json::from_slice(&out.stdout).expect("invoke body is JSON")
|
||||
}
|
||||
Reference in New Issue
Block a user