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>
85 lines
2.9 KiB
Rust
85 lines
2.9 KiB
Rust
//! 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")
|
|
}
|