Closes the audit's Critical "operator can't deploy a serverless endpoint
end-to-end from the CLI" finding. Two new subcommand families bring the
CLI coverage from 14 commands to ~25:
- pic routes {ls, create, rm, check, match}: full route CRUD plus the
dry-run conflict checker and the URL matcher already exposed by the
dashboard. The create form accepts --path-kind, --host-kind, and
--dispatch (sync|async) so async routes can finally be created
headlessly. The ls output adds a dispatch column.
- pic admins {ls, create, show, set, rm}: per-instance admin user
management. Create reads passwords from stdin via --password - so
shell history never sees the cleartext. Set is a JSON-Merge-Patch
shape that lets operators deactivate accounts or rotate roles
without touching the dashboard.
Six new ignored integration tests follow the established #[ignore]
pattern (DATABASE_URL gates them). They cover the happy-path round
trip, the async-dispatch persistence path, the password-required
error path, and the capability gate (a Member sees HTTP 403). All
pass against the local dev stack with PICLOUD_DEV_MODE=true +
PICLOUD_DEV_INSECURE_KEY=i-understand-this-is-insecure.
The `pic members` and `pic domains` subcommands the audit mentioned
are deferred — the apps_api shape may shift in v1.2 with per-app
roles and rebuilding the CLI surface twice would be churn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
1.6 KiB
Rust
66 lines
1.6 KiB
Rust
//! RAII guards that delete server-side resources on `Drop`.
|
|
//!
|
|
//! Each guard owns the minimum it needs to issue a single DELETE: the
|
|
//! base URL, an admin bearer token, and the resource identifier.
|
|
//! Failures are swallowed because Drop runs during teardown — a panic
|
|
//! here would just mask the real failure that the test was reporting.
|
|
|
|
pub struct AppGuard {
|
|
url: String,
|
|
token: String,
|
|
slug: String,
|
|
}
|
|
|
|
impl AppGuard {
|
|
pub fn new(url: &str, token: &str, slug: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
slug: slug.to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn slug(&self) -> &str {
|
|
&self.slug
|
|
}
|
|
}
|
|
|
|
impl Drop for AppGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!(
|
|
"{}/api/v1/admin/apps/{}?force=true",
|
|
self.url, self.slug
|
|
))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|
|
|
|
pub struct UserGuard {
|
|
url: String,
|
|
token: String,
|
|
user_id: String,
|
|
}
|
|
|
|
impl UserGuard {
|
|
pub fn new(url: &str, token: &str, user_id: &str) -> Self {
|
|
Self {
|
|
url: url.to_string(),
|
|
token: token.to_string(),
|
|
user_id: user_id.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for UserGuard {
|
|
fn drop(&mut self) {
|
|
let client = reqwest::blocking::Client::new();
|
|
let _ = client
|
|
.delete(format!("{}/api/v1/admin/admins/{}", self.url, self.user_id))
|
|
.bearer_auth(&self.token)
|
|
.send();
|
|
}
|
|
}
|