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:
@@ -692,6 +692,54 @@ impl Client {
|
||||
decode_status(resp).await
|
||||
}
|
||||
|
||||
// ---------- vars (Phase 3 config) ----------
|
||||
|
||||
/// `GET /api/v1/admin/{apps,groups}/{id}/vars` — the owner's OWN vars
|
||||
/// (not the resolved/inherited view).
|
||||
pub async fn vars_list(&self, owner: VarOwnerArg<'_>) -> Result<VarListDto> {
|
||||
let resp = self
|
||||
.request(Method::GET, &format!("{}/vars", owner.base_path()))
|
||||
.send()
|
||||
.await?;
|
||||
decode(resp).await
|
||||
}
|
||||
|
||||
/// `PUT /api/v1/admin/{apps,groups}/{id}/vars`
|
||||
pub async fn vars_set(
|
||||
&self,
|
||||
owner: VarOwnerArg<'_>,
|
||||
key: &str,
|
||||
value: serde_json::Value,
|
||||
env: Option<&str>,
|
||||
tombstone: bool,
|
||||
) -> Result<()> {
|
||||
let mut body = serde_json::json!({ "key": key, "value": value, "tombstone": tombstone });
|
||||
if let Some(env) = env {
|
||||
body["env"] = serde_json::Value::String(env.to_string());
|
||||
}
|
||||
let resp = self
|
||||
.request(Method::PUT, &format!("{}/vars", owner.base_path()))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
decode_status(resp).await
|
||||
}
|
||||
|
||||
/// `DELETE /api/v1/admin/{apps,groups}/{id}/vars/{key}`
|
||||
pub async fn vars_delete(
|
||||
&self,
|
||||
owner: VarOwnerArg<'_>,
|
||||
key: &str,
|
||||
env: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let mut path = format!("{}/vars/{}", owner.base_path(), seg(key));
|
||||
if let Some(env) = env {
|
||||
path.push_str(&format!("?env={}", seg(env)));
|
||||
}
|
||||
let resp = self.request(Method::DELETE, &path).send().await?;
|
||||
decode_status(resp).await
|
||||
}
|
||||
|
||||
// ---------- domains ----------
|
||||
|
||||
/// `GET /api/v1/admin/apps/{id_or_slug}/domains`
|
||||
@@ -1394,6 +1442,37 @@ pub struct DeadLetterDto {
|
||||
pub resolution: Option<String>,
|
||||
}
|
||||
|
||||
/// Which owner a `pic vars` call targets. Selects the `apps` vs `groups`
|
||||
/// admin path prefix; the identifier travels in the path (encoded).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum VarOwnerArg<'a> {
|
||||
App(&'a str),
|
||||
Group(&'a str),
|
||||
}
|
||||
|
||||
impl VarOwnerArg<'_> {
|
||||
fn base_path(&self) -> String {
|
||||
match self {
|
||||
Self::App(ident) => format!("/api/v1/admin/apps/{}", seg(ident)),
|
||||
Self::Group(ident) => format!("/api/v1/admin/groups/{}", seg(ident)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VarListDto {
|
||||
pub vars: Vec<VarItemDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VarItemDto {
|
||||
pub key: String,
|
||||
pub env: String,
|
||||
pub value: serde_json::Value,
|
||||
pub is_tombstone: bool,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SecretListDto {
|
||||
pub secrets: Vec<SecretItemDto>,
|
||||
|
||||
Reference in New Issue
Block a user