Pre-unlock wave U3 (highest-ROI). Task 7 from CONVERGENCE-PLAN — consolidate 2 provision-*.sh scripts into Rust via Backend trait. Old shells (provision-hetzner.sh, provision-vultr.sh) had identical 6-subcommand surface (create|status|destroy|list), log/die/check_deps helpers, idempotency contract. Sole delta: hcloud vs vultr-cli. RULE 0.2 says Rust-first when >50 LOC + growth expected. New crate _primitives/_rust/kei-provision/: - src/backend.rs (58 LOC) — Backend trait: create/status/destroy/list; CreateOpts and ServerInfo structs - src/backends/hetzner.rs (143 LOC) — shells to `hcloud server ...` --output=json, parses JSON response, honors HCLOUD_TOKEN env (RULE 0.8) - src/backends/vultr.rs (189 LOC) — same pattern, `vultr-cli instance`, honors VULTR_API_KEY env - src/exec.rs (100 LOC) — Command runner + PATH-aware env preservation - src/b64.rs (49 LOC) — minimal user-data base64 encoder; zero transitive deps - src/main.rs (141 LOC) — clap CLI `kei-provision <backend> <cmd>` - tests/backend_smoke.rs (184 LOC) — tempdir PATH-inject fake hcloud + fake vultr-cli, no real cloud. Mutex-serialized (Rust test parallelism). Tests: 11/11 (3 b64 unit + 8 backend_smoke integration). Coverage: hetzner status present/absent/list, vultr status found/absent/destroy idempotent, unknown-backend error, CreateOpts default. Old shells kept with superseded-v0.17 header — install.sh still copies them, legacy scripts still work. New users get kei-provision binary. harden-base.sh untouched (different lifecycle — runs on target VPS). Backend trait factored to accept aws/doctl/linode follow-ups without re-architecture. Workspace Cargo.toml: +kei-provision member (1 line). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.1 KiB
Rust
58 lines
2.1 KiB
Rust
//! Backend trait + shared data types for the unified provisioner.
|
|
//!
|
|
//! A `Backend` shells out to an external CLI (hcloud / vultr-cli / future
|
|
//! aws / doctl / linode-cli). All IO is through the `Backend` methods;
|
|
//! `main.rs` never touches `std::process::Command` directly.
|
|
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::PathBuf;
|
|
|
|
/// Opts passed to `Backend::create`. Fields are `Option` because every
|
|
/// backend has different defaults (hetzner = cx22/fsn1/debian-12; vultr =
|
|
/// vc2-1c-1gb/ams/resolve-Debian-12). Backend fills blanks.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct CreateOpts {
|
|
/// e.g. `cx22` (hetzner), `vc2-1c-1gb` (vultr).
|
|
pub server_type: Option<String>,
|
|
/// e.g. `fsn1` (hetzner), `ams` (vultr).
|
|
pub location: Option<String>,
|
|
/// e.g. `debian-12` (hetzner), `2136` (vultr os-id).
|
|
pub image: Option<String>,
|
|
/// SSH key id/name (backend-specific).
|
|
pub ssh_key: Option<String>,
|
|
/// Firewall name (hetzner) / group id (vultr).
|
|
pub firewall: Option<String>,
|
|
/// Cloud-init user-data file path.
|
|
pub user_data_path: Option<PathBuf>,
|
|
}
|
|
|
|
/// Normalized server info across all backends.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerInfo {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub ipv4: Option<String>,
|
|
pub status: String,
|
|
/// Raw backend JSON for details the normalized fields drop
|
|
/// (region, plan, power status, datacenter, etc).
|
|
pub backend_specific: serde_json::Value,
|
|
}
|
|
|
|
/// Implemented by each cloud provider adapter.
|
|
pub trait Backend {
|
|
/// Short identifier — `"hetzner"`, `"vultr"`.
|
|
fn name(&self) -> &'static str;
|
|
|
|
/// Create a server with `name` or return existing (idempotent).
|
|
fn create(&self, name: &str, opts: &CreateOpts) -> Result<ServerInfo>;
|
|
|
|
/// `Ok(None)` if absent; never fails on absence.
|
|
fn status(&self, name: &str) -> Result<Option<ServerInfo>>;
|
|
|
|
/// Idempotent: absent server = Ok(()). `force` skips confirm prompt.
|
|
fn destroy(&self, name: &str, force: bool) -> Result<()>;
|
|
|
|
/// All servers owned by the current token.
|
|
fn list(&self) -> Result<Vec<ServerInfo>>;
|
|
}
|