refactor: migrate all modules from anyhow to SunbeamError
Replace anyhow::{bail, Context, Result} with crate::error::{Result,
SunbeamError, ResultExt} across all modules. Each module uses the
appropriate error variant (Kube, Secrets, Build, Identity, etc).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Service-level health checks — functional probes beyond pod readiness.
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::error::Result;
|
||||
use base64::Engine;
|
||||
use hmac::{Hmac, Mac};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
@@ -87,7 +87,7 @@ async fn http_get(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
headers: Option<&[(&str, &str)]>,
|
||||
) -> Result<(u16, Vec<u8>), String> {
|
||||
) -> std::result::Result<(u16, Vec<u8>), String> {
|
||||
let mut req = client.get(url);
|
||||
if let Some(hdrs) = headers {
|
||||
for (k, v) in hdrs {
|
||||
|
||||
10
src/cli.rs
10
src/cli.rs
@@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Result};
|
||||
use crate::error::{Result, SunbeamError};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
|
||||
/// Sunbeam local dev stack manager.
|
||||
@@ -309,7 +309,7 @@ pub enum UserAction {
|
||||
},
|
||||
}
|
||||
|
||||
fn validate_date(s: &str) -> Result<String, String> {
|
||||
fn validate_date(s: &str) -> std::result::Result<String, String> {
|
||||
if s.is_empty() {
|
||||
return Ok(s.to_string());
|
||||
}
|
||||
@@ -672,10 +672,10 @@ pub async fn dispatch() -> Result<()> {
|
||||
Env::Production => {
|
||||
let host = crate::config::get_production_host();
|
||||
if host.is_empty() {
|
||||
bail!(
|
||||
return Err(SunbeamError::config(
|
||||
"Production host not configured. \
|
||||
Use `sunbeam config set --host` or set SUNBEAM_SSH_HOST."
|
||||
);
|
||||
Use `sunbeam config set --host` or set SUNBEAM_SSH_HOST.",
|
||||
));
|
||||
}
|
||||
Some(host)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Pure K8s implementation: no Lima VM operations.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::error::{Result, ResultExt, SunbeamError};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const GITEA_ADMIN_USER: &str = "gitea_admin";
|
||||
@@ -36,10 +36,10 @@ async fn ensure_cert_manager() -> Result<()> {
|
||||
// Download and apply cert-manager YAML
|
||||
let body = reqwest::get(CERT_MANAGER_URL)
|
||||
.await
|
||||
.context("Failed to download cert-manager manifest")?
|
||||
.ctx("Failed to download cert-manager manifest")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read cert-manager manifest body")?;
|
||||
.ctx("Failed to read cert-manager manifest body")?;
|
||||
|
||||
crate::kube::kube_apply(&body).await?;
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn ensure_linkerd() -> Result<()> {
|
||||
crate::output::ok("Installing Gateway API CRDs...");
|
||||
let gateway_body = reqwest::get(GATEWAY_API_CRDS_URL)
|
||||
.await
|
||||
.context("Failed to download Gateway API CRDs")?
|
||||
.ctx("Failed to download Gateway API CRDs")?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
@@ -86,11 +86,11 @@ async fn ensure_linkerd() -> Result<()> {
|
||||
.args(["install", "--crds"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run `linkerd install --crds`")?;
|
||||
.ctx("Failed to run `linkerd install --crds`")?;
|
||||
|
||||
if !crds_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&crds_output.stderr);
|
||||
bail!("linkerd install --crds failed: {stderr}");
|
||||
return Err(SunbeamError::tool("linkerd", format!("install --crds failed: {stderr}")));
|
||||
}
|
||||
let crds = String::from_utf8_lossy(&crds_output.stdout);
|
||||
crate::kube::kube_apply(&crds).await?;
|
||||
@@ -101,11 +101,11 @@ async fn ensure_linkerd() -> Result<()> {
|
||||
.args(["install"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run `linkerd install`")?;
|
||||
.ctx("Failed to run `linkerd install`")?;
|
||||
|
||||
if !cp_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&cp_output.stderr);
|
||||
bail!("linkerd install failed: {stderr}");
|
||||
return Err(SunbeamError::tool("linkerd", format!("install failed: {stderr}")));
|
||||
}
|
||||
let cp = String::from_utf8_lossy(&cp_output.stdout);
|
||||
crate::kube::kube_apply(&cp).await?;
|
||||
@@ -141,24 +141,25 @@ async fn ensure_tls_cert(domain: &str) -> Result<()> {
|
||||
|
||||
crate::output::ok(&format!("Generating wildcard cert for *.{domain}..."));
|
||||
std::fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("Failed to create secrets dir: {}", dir.display()))?;
|
||||
.with_ctx(|| format!("Failed to create secrets dir: {}", dir.display()))?;
|
||||
|
||||
let subject_alt_names = vec![format!("*.{domain}")];
|
||||
let mut params = rcgen::CertificateParams::new(subject_alt_names)
|
||||
.context("Failed to create certificate params")?;
|
||||
.map_err(|e| SunbeamError::kube(format!("Failed to create certificate params: {e}")))?;
|
||||
params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, format!("*.{domain}"));
|
||||
|
||||
let key_pair = rcgen::KeyPair::generate().context("Failed to generate key pair")?;
|
||||
let key_pair = rcgen::KeyPair::generate()
|
||||
.map_err(|e| SunbeamError::kube(format!("Failed to generate key pair: {e}")))?;
|
||||
let cert = params
|
||||
.self_signed(&key_pair)
|
||||
.context("Failed to generate self-signed certificate")?;
|
||||
.map_err(|e| SunbeamError::kube(format!("Failed to generate self-signed certificate: {e}")))?;
|
||||
|
||||
std::fs::write(&cert_path, cert.pem())
|
||||
.with_context(|| format!("Failed to write {}", cert_path.display()))?;
|
||||
.with_ctx(|| format!("Failed to write {}", cert_path.display()))?;
|
||||
std::fs::write(&key_path, key_pair.serialize_pem())
|
||||
.with_context(|| format!("Failed to write {}", key_path.display()))?;
|
||||
.with_ctx(|| format!("Failed to write {}", key_path.display()))?;
|
||||
|
||||
crate::output::ok(&format!("Cert generated. Domain: {domain}"));
|
||||
Ok(())
|
||||
@@ -176,9 +177,9 @@ async fn ensure_tls_secret(domain: &str) -> Result<()> {
|
||||
|
||||
let dir = secrets_dir();
|
||||
let cert_pem =
|
||||
std::fs::read_to_string(dir.join("tls.crt")).context("Failed to read tls.crt")?;
|
||||
std::fs::read_to_string(dir.join("tls.crt")).ctx("Failed to read tls.crt")?;
|
||||
let key_pem =
|
||||
std::fs::read_to_string(dir.join("tls.key")).context("Failed to read tls.key")?;
|
||||
std::fs::read_to_string(dir.join("tls.key")).ctx("Failed to read tls.key")?;
|
||||
|
||||
// Create TLS secret via kube-rs
|
||||
let client = crate::kube::get_client().await?;
|
||||
@@ -211,7 +212,7 @@ async fn ensure_tls_secret(domain: &str) -> Result<()> {
|
||||
let pp = kube::api::PatchParams::apply("sunbeam").force();
|
||||
api.patch("pingora-tls", &pp, &kube::api::Patch::Apply(secret_obj))
|
||||
.await
|
||||
.context("Failed to create TLS secret")?;
|
||||
.ctx("Failed to create TLS secret")?;
|
||||
|
||||
crate::output::ok("Done.");
|
||||
Ok(())
|
||||
@@ -289,7 +290,7 @@ async fn wait_rollout(ns: &str, deployment: &str, timeout_secs: u64) -> Result<(
|
||||
|
||||
loop {
|
||||
if Instant::now() > deadline {
|
||||
bail!("Timed out waiting for deployment {ns}/{deployment}");
|
||||
return Err(SunbeamError::kube(format!("Timed out waiting for deployment {ns}/{deployment}")));
|
||||
}
|
||||
|
||||
match api.get_opt(deployment).await? {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crate::error::{Result, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn load_config() -> SunbeamConfig {
|
||||
pub fn save_config(config: &SunbeamConfig) -> Result<()> {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
std::fs::create_dir_all(parent).with_ctx(|| {
|
||||
format!(
|
||||
"Failed to create config directory: {}",
|
||||
parent.display()
|
||||
@@ -56,7 +56,7 @@ pub fn save_config(config: &SunbeamConfig) -> Result<()> {
|
||||
}
|
||||
let content = serde_json::to_string_pretty(config)?;
|
||||
std::fs::write(&path, content)
|
||||
.with_context(|| format!("Failed to save config to {}", path.display()))?;
|
||||
.with_ctx(|| format!("Failed to save config to {}", path.display()))?;
|
||||
crate::output::ok(&format!("Configuration saved to {}", path.display()));
|
||||
Ok(())
|
||||
}
|
||||
@@ -114,7 +114,7 @@ pub fn clear_config() -> Result<()> {
|
||||
let path = config_path();
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)
|
||||
.with_context(|| format!("Failed to remove {}", path.display()))?;
|
||||
.with_ctx(|| format!("Failed to remove {}", path.display()))?;
|
||||
crate::output::ok(&format!(
|
||||
"Configuration cleared from {}",
|
||||
path.display()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Gitea bootstrap -- admin setup, org creation, OIDC auth source configuration.
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::error::Result;
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::api::{Api, ListParams};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Image building, mirroring, and pushing to Gitea registry.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::error::{Result, ResultExt, SunbeamError};
|
||||
use base64::Engine;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -86,7 +86,7 @@ async fn get_build_env() -> Result<BuildEnv> {
|
||||
"password",
|
||||
)
|
||||
.await
|
||||
.context("gitea-admin-credentials secret not found -- run seed first.")?;
|
||||
.ctx("gitea-admin-credentials secret not found -- run seed first.")?;
|
||||
|
||||
let platform = if is_prod {
|
||||
"linux/amd64".to_string()
|
||||
@@ -131,7 +131,7 @@ async fn buildctl_build_and_push(
|
||||
) -> Result<()> {
|
||||
// Find a free local port for port-forward
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0")
|
||||
.context("Failed to bind ephemeral port")?;
|
||||
.ctx("Failed to bind ephemeral port")?;
|
||||
let local_port = listener.local_addr()?.port();
|
||||
drop(listener);
|
||||
|
||||
@@ -144,10 +144,10 @@ async fn buildctl_build_and_push(
|
||||
}
|
||||
});
|
||||
|
||||
let tmpdir = tempfile::TempDir::new().context("Failed to create temp dir")?;
|
||||
let tmpdir = tempfile::TempDir::new().ctx("Failed to create temp dir")?;
|
||||
let cfg_path = tmpdir.path().join("config.json");
|
||||
std::fs::write(&cfg_path, serde_json::to_string(&docker_cfg)?)
|
||||
.context("Failed to write docker config")?;
|
||||
.ctx("Failed to write docker config")?;
|
||||
|
||||
// Start port-forward to buildkitd
|
||||
let ctx_arg = format!("--context={}", crate::kube::context());
|
||||
@@ -165,14 +165,14 @@ async fn buildctl_build_and_push(
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed to start buildkitd port-forward")?;
|
||||
.ctx("Failed to start buildkitd port-forward")?;
|
||||
|
||||
// Wait for port-forward to become ready
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(15);
|
||||
loop {
|
||||
if tokio::time::Instant::now() > deadline {
|
||||
pf.kill().await.ok();
|
||||
bail!("buildkitd port-forward on :{local_port} did not become ready within 15s");
|
||||
return Err(SunbeamError::tool("buildctl", format!("buildkitd port-forward on :{local_port} did not become ready within 15s")));
|
||||
}
|
||||
if tokio::net::TcpStream::connect(format!("127.0.0.1:{local_port}"))
|
||||
.await
|
||||
@@ -247,8 +247,8 @@ async fn buildctl_build_and_push(
|
||||
|
||||
match result {
|
||||
Ok(status) if status.success() => Ok(()),
|
||||
Ok(status) => bail!("buildctl exited with status {status}"),
|
||||
Err(e) => bail!("Failed to run buildctl: {e}"),
|
||||
Ok(status) => return Err(SunbeamError::tool("buildctl", format!("exited with status {status}"))),
|
||||
Err(e) => return Err(SunbeamError::tool("buildctl", format!("failed to run: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ async fn get_node_addresses() -> Result<Vec<String>> {
|
||||
let node_list = api
|
||||
.list(&kube::api::ListParams::default())
|
||||
.await
|
||||
.context("Failed to list nodes")?;
|
||||
.ctx("Failed to list nodes")?;
|
||||
|
||||
let mut addresses = Vec::new();
|
||||
for node in &node_list.items {
|
||||
@@ -387,7 +387,7 @@ async fn ctr_pull_on_nodes(env: &BuildEnv, images: &[String]) -> Result<()> {
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => ok(&format!("Pulled {img} on {node_ip}")),
|
||||
_ => bail!("ctr pull failed on {node_ip} for {img}"),
|
||||
_ => return Err(SunbeamError::tool("ctr", format!("pull failed on {node_ip} for {img}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,7 +440,7 @@ async fn wait_deployment_ready(ns: &str, deployment: &str, timeout_secs: u64) ->
|
||||
|
||||
loop {
|
||||
if Instant::now() > deadline {
|
||||
bail!("Timed out waiting for deployment {ns}/{deployment}");
|
||||
return Err(SunbeamError::build(format!("Timed out waiting for deployment {ns}/{deployment}")));
|
||||
}
|
||||
|
||||
if let Some(dep) = api.get_opt(deployment).await? {
|
||||
@@ -477,10 +477,10 @@ async fn docker_hub_token(repo: &str) -> Result<String> {
|
||||
);
|
||||
let resp: DockerAuthToken = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to fetch Docker Hub token")?
|
||||
.ctx("Failed to fetch Docker Hub token")?
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to parse Docker Hub token response")?;
|
||||
.ctx("Failed to parse Docker Hub token response")?;
|
||||
Ok(resp.token)
|
||||
}
|
||||
|
||||
@@ -502,18 +502,18 @@ async fn fetch_manifest_index(
|
||||
.header("Accept", accept)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch manifest from Docker Hub")?;
|
||||
.ctx("Failed to fetch manifest from Docker Hub")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
bail!(
|
||||
return Err(SunbeamError::build(format!(
|
||||
"Docker Hub returned {} for {repo}:{tag}",
|
||||
resp.status()
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
resp.json()
|
||||
.await
|
||||
.context("Failed to parse manifest index JSON")
|
||||
.ctx("Failed to parse manifest index JSON")
|
||||
}
|
||||
|
||||
/// Build an OCI tar archive containing a patched index that maps both
|
||||
@@ -729,7 +729,7 @@ pub async fn cmd_mirror() -> Result<()> {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn ssh for ctr import")?;
|
||||
.ctx("Failed to spawn ssh for ctr import")?;
|
||||
|
||||
if let Some(mut stdin) = import_cmd.stdin.take() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -854,7 +854,7 @@ async fn build_proxy(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let proxy_dir = crate::config::get_repo_root().join("proxy");
|
||||
if !proxy_dir.is_dir() {
|
||||
bail!("Proxy source not found at {}", proxy_dir.display());
|
||||
return Err(SunbeamError::build(format!("Proxy source not found at {}", proxy_dir.display())));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/proxy:latest", env.registry);
|
||||
@@ -883,7 +883,7 @@ async fn build_tuwunel(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let tuwunel_dir = crate::config::get_repo_root().join("tuwunel");
|
||||
if !tuwunel_dir.is_dir() {
|
||||
bail!("Tuwunel source not found at {}", tuwunel_dir.display());
|
||||
return Err(SunbeamError::build(format!("Tuwunel source not found at {}", tuwunel_dir.display())));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/tuwunel:latest", env.registry);
|
||||
@@ -916,10 +916,10 @@ async fn build_integration(push: bool, deploy: bool) -> Result<()> {
|
||||
let dockerignore = integration_service_dir.join(".dockerignore");
|
||||
|
||||
if !dockerfile.exists() {
|
||||
bail!(
|
||||
return Err(SunbeamError::build(format!(
|
||||
"integration-service Dockerfile not found at {}",
|
||||
dockerfile.display()
|
||||
);
|
||||
)));
|
||||
}
|
||||
if !sunbeam_dir
|
||||
.join("integration")
|
||||
@@ -927,11 +927,11 @@ async fn build_integration(push: bool, deploy: bool) -> Result<()> {
|
||||
.join("widgets")
|
||||
.is_dir()
|
||||
{
|
||||
bail!(
|
||||
return Err(SunbeamError::build(format!(
|
||||
"integration repo not found at {} -- \
|
||||
run: cd sunbeam && git clone https://github.com/suitenumerique/integration.git",
|
||||
sunbeam_dir.join("integration").display()
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/integration:latest", env.registry);
|
||||
@@ -974,10 +974,10 @@ async fn build_kratos_admin(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let kratos_admin_dir = crate::config::get_repo_root().join("kratos-admin");
|
||||
if !kratos_admin_dir.is_dir() {
|
||||
bail!(
|
||||
return Err(SunbeamError::build(format!(
|
||||
"kratos-admin source not found at {}",
|
||||
kratos_admin_dir.display()
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/kratos-admin-ui:latest", env.registry);
|
||||
@@ -1006,7 +1006,7 @@ async fn build_meet(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let meet_dir = crate::config::get_repo_root().join("meet");
|
||||
if !meet_dir.is_dir() {
|
||||
bail!("meet source not found at {}", meet_dir.display());
|
||||
return Err(SunbeamError::build(format!("meet source not found at {}", meet_dir.display())));
|
||||
}
|
||||
|
||||
let backend_image = format!("{}/studio/meet-backend:latest", env.registry);
|
||||
@@ -1031,10 +1031,10 @@ async fn build_meet(push: bool, deploy: bool) -> Result<()> {
|
||||
step(&format!("Building meet-frontend -> {frontend_image} ..."));
|
||||
let frontend_dockerfile = meet_dir.join("src").join("frontend").join("Dockerfile");
|
||||
if !frontend_dockerfile.exists() {
|
||||
bail!(
|
||||
return Err(SunbeamError::build(format!(
|
||||
"meet frontend Dockerfile not found at {}",
|
||||
frontend_dockerfile.display()
|
||||
);
|
||||
)));
|
||||
}
|
||||
|
||||
let mut build_args = HashMap::new();
|
||||
@@ -1070,14 +1070,14 @@ async fn build_people(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let people_dir = crate::config::get_repo_root().join("people");
|
||||
if !people_dir.is_dir() {
|
||||
bail!("people source not found at {}", people_dir.display());
|
||||
return Err(SunbeamError::build(format!("people source not found at {}", people_dir.display())));
|
||||
}
|
||||
|
||||
let workspace_dir = people_dir.join("src").join("frontend");
|
||||
let app_dir = workspace_dir.join("apps").join("desk");
|
||||
let dockerfile = workspace_dir.join("Dockerfile");
|
||||
if !dockerfile.exists() {
|
||||
bail!("Dockerfile not found at {}", dockerfile.display());
|
||||
return Err(SunbeamError::build(format!("Dockerfile not found at {}", dockerfile.display())));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/people-frontend:latest", env.registry);
|
||||
@@ -1090,9 +1090,9 @@ async fn build_people(push: bool, deploy: bool) -> Result<()> {
|
||||
.current_dir(&workspace_dir)
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run yarn install")?;
|
||||
.ctx("Failed to run yarn install")?;
|
||||
if !yarn_status.success() {
|
||||
bail!("yarn install failed");
|
||||
return Err(SunbeamError::tool("yarn", "install failed"));
|
||||
}
|
||||
|
||||
// cunningham design tokens
|
||||
@@ -1106,9 +1106,9 @@ async fn build_people(push: bool, deploy: bool) -> Result<()> {
|
||||
.current_dir(&app_dir)
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run cunningham")?;
|
||||
.ctx("Failed to run cunningham")?;
|
||||
if !cunningham_status.success() {
|
||||
bail!("cunningham failed");
|
||||
return Err(SunbeamError::tool("cunningham", "design token generation failed"));
|
||||
}
|
||||
|
||||
let mut build_args = HashMap::new();
|
||||
@@ -1177,7 +1177,7 @@ async fn build_messages(what: &str, push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let messages_dir = crate::config::get_repo_root().join("messages");
|
||||
if !messages_dir.is_dir() {
|
||||
bail!("messages source not found at {}", messages_dir.display());
|
||||
return Err(SunbeamError::build(format!("messages source not found at {}", messages_dir.display())));
|
||||
}
|
||||
|
||||
let components: Vec<_> = if what == "messages" {
|
||||
@@ -1278,10 +1278,10 @@ async fn build_la_suite_frontend(
|
||||
let dockerfile = repo_dir.join(dockerfile_rel);
|
||||
|
||||
if !repo_dir.is_dir() {
|
||||
bail!("{app} source not found at {}", repo_dir.display());
|
||||
return Err(SunbeamError::build(format!("{app} source not found at {}", repo_dir.display())));
|
||||
}
|
||||
if !dockerfile.exists() {
|
||||
bail!("Dockerfile not found at {}", dockerfile.display());
|
||||
return Err(SunbeamError::build(format!("Dockerfile not found at {}", dockerfile.display())));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/{image_name}:latest", env.registry);
|
||||
@@ -1293,9 +1293,9 @@ async fn build_la_suite_frontend(
|
||||
.current_dir(&workspace_dir)
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run yarn install")?;
|
||||
.ctx("Failed to run yarn install")?;
|
||||
if !yarn_status.success() {
|
||||
bail!("yarn install failed");
|
||||
return Err(SunbeamError::tool("yarn", "install failed"));
|
||||
}
|
||||
|
||||
ok("Regenerating cunningham design tokens (yarn build-theme)...");
|
||||
@@ -1304,9 +1304,9 @@ async fn build_la_suite_frontend(
|
||||
.current_dir(&app_dir)
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run yarn build-theme")?;
|
||||
.ctx("Failed to run yarn build-theme")?;
|
||||
if !theme_status.success() {
|
||||
bail!("yarn build-theme failed");
|
||||
return Err(SunbeamError::tool("yarn", "build-theme failed"));
|
||||
}
|
||||
|
||||
let mut build_args = HashMap::new();
|
||||
@@ -1338,7 +1338,7 @@ async fn patch_dockerfile_uv(
|
||||
platform: &str,
|
||||
) -> Result<(PathBuf, Vec<PathBuf>)> {
|
||||
let content = std::fs::read_to_string(dockerfile_path)
|
||||
.context("Failed to read Dockerfile for uv patching")?;
|
||||
.ctx("Failed to read Dockerfile for uv patching")?;
|
||||
|
||||
// Match COPY --from=ghcr.io/astral-sh/uv@sha256:... /uv /uvx /bin/
|
||||
let original_copy = content
|
||||
@@ -1408,7 +1408,7 @@ async fn patch_dockerfile_uv(
|
||||
// Download tarball
|
||||
let response = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to download uv release")?;
|
||||
.ctx("Failed to download uv release")?;
|
||||
let tarball_bytes = response.bytes().await?;
|
||||
|
||||
// Extract uv and uvx from tarball
|
||||
@@ -1456,7 +1456,7 @@ async fn build_projects(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let projects_dir = crate::config::get_repo_root().join("projects");
|
||||
if !projects_dir.is_dir() {
|
||||
bail!("projects source not found at {}", projects_dir.display());
|
||||
return Err(SunbeamError::build(format!("projects source not found at {}", projects_dir.display())));
|
||||
}
|
||||
|
||||
let image = format!("{}/studio/projects:latest", env.registry);
|
||||
@@ -1485,7 +1485,7 @@ async fn build_calendars(push: bool, deploy: bool) -> Result<()> {
|
||||
let env = get_build_env().await?;
|
||||
let cal_dir = crate::config::get_repo_root().join("calendars");
|
||||
if !cal_dir.is_dir() {
|
||||
bail!("calendars source not found at {}", cal_dir.display());
|
||||
return Err(SunbeamError::build(format!("calendars source not found at {}", cal_dir.display())));
|
||||
}
|
||||
|
||||
let backend_dir = cal_dir.join("src").join("backend");
|
||||
|
||||
60
src/kube.rs
60
src/kube.rs
@@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::error::{Result, SunbeamError, ResultExt};
|
||||
use base64::Engine;
|
||||
use k8s_openapi::api::apps::v1::Deployment;
|
||||
use k8s_openapi::api::core::v1::{Namespace, Secret};
|
||||
@@ -71,7 +71,7 @@ pub async fn ensure_tunnel() -> Result<()> {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.context("Failed to spawn SSH tunnel")?;
|
||||
.ctx("Failed to spawn SSH tunnel")?;
|
||||
|
||||
// Wait for tunnel to become available
|
||||
for _ in 0..20 {
|
||||
@@ -98,15 +98,15 @@ pub async fn get_client() -> Result<&'static Client> {
|
||||
.get_or_try_init(|| async {
|
||||
ensure_tunnel().await?;
|
||||
|
||||
let kubeconfig = Kubeconfig::read().context("Failed to read kubeconfig")?;
|
||||
let kubeconfig = Kubeconfig::read().map_err(|e| SunbeamError::kube(format!("Failed to read kubeconfig: {e}")))?;
|
||||
let options = KubeConfigOptions {
|
||||
context: Some(context().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let config = Config::from_custom_kubeconfig(kubeconfig, &options)
|
||||
.await
|
||||
.context("Failed to build kube config from kubeconfig")?;
|
||||
Client::try_from(config).context("Failed to create kube client")
|
||||
.map_err(|e| SunbeamError::kube(format!("Failed to build kube config from kubeconfig: {e}")))?;
|
||||
Client::try_from(config).ctx("Failed to create kube client")
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -129,7 +129,7 @@ pub async fn kube_apply(manifest: &str) -> Result<()> {
|
||||
|
||||
// Parse the YAML to a DynamicObject so we can route it
|
||||
let obj: serde_yaml::Value =
|
||||
serde_yaml::from_str(doc).context("Failed to parse YAML document")?;
|
||||
serde_yaml::from_str(doc).ctx("Failed to parse YAML document")?;
|
||||
|
||||
let api_version = obj
|
||||
.get("apiVersion")
|
||||
@@ -164,15 +164,15 @@ pub async fn kube_apply(manifest: &str) -> Result<()> {
|
||||
let patch: serde_json::Value = serde_json::from_str(
|
||||
&serde_json::to_string(
|
||||
&serde_yaml::from_str::<serde_json::Value>(doc)
|
||||
.context("Failed to parse YAML to JSON")?,
|
||||
.ctx("Failed to parse YAML to JSON")?,
|
||||
)
|
||||
.context("Failed to serialize to JSON")?,
|
||||
.ctx("Failed to serialize to JSON")?,
|
||||
)
|
||||
.context("Failed to parse JSON")?;
|
||||
.ctx("Failed to parse JSON")?;
|
||||
|
||||
api.patch(name, &ssapply, &Patch::Apply(patch))
|
||||
.await
|
||||
.with_context(|| format!("Failed to apply {kind}/{name}"))?;
|
||||
.with_ctx(|| format!("Failed to apply {kind}/{name}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -194,7 +194,7 @@ async fn resolve_api_resource(
|
||||
let disc = discovery::Discovery::new(client.clone())
|
||||
.run()
|
||||
.await
|
||||
.context("API discovery failed")?;
|
||||
.ctx("API discovery failed")?;
|
||||
|
||||
for api_group in disc.groups() {
|
||||
if api_group.name() == group {
|
||||
@@ -216,7 +216,7 @@ pub async fn kube_get_secret(ns: &str, name: &str) -> Result<Option<Secret>> {
|
||||
let api: Api<Secret> = Api::namespaced(client.clone(), ns);
|
||||
match api.get_opt(name).await {
|
||||
Ok(secret) => Ok(secret),
|
||||
Err(e) => Err(e).context(format!("Failed to get secret {ns}/{name}")),
|
||||
Err(e) => Err(e).with_ctx(|| format!("Failed to get secret {ns}/{name}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,16 +225,16 @@ pub async fn kube_get_secret(ns: &str, name: &str) -> Result<Option<Secret>> {
|
||||
pub async fn kube_get_secret_field(ns: &str, name: &str, key: &str) -> Result<String> {
|
||||
let secret = kube_get_secret(ns, name)
|
||||
.await?
|
||||
.with_context(|| format!("Secret {ns}/{name} not found"))?;
|
||||
.with_ctx(|| format!("Secret {ns}/{name} not found"))?;
|
||||
|
||||
let data = secret.data.as_ref().context("Secret has no data")?;
|
||||
let data = secret.data.as_ref().ctx("Secret has no data")?;
|
||||
|
||||
let bytes = data
|
||||
.get(key)
|
||||
.with_context(|| format!("Key {key:?} not found in secret {ns}/{name}"))?;
|
||||
.with_ctx(|| format!("Key {key:?} not found in secret {ns}/{name}"))?;
|
||||
|
||||
String::from_utf8(bytes.0.clone())
|
||||
.with_context(|| format!("Key {key:?} in secret {ns}/{name} is not valid UTF-8"))
|
||||
.with_ctx(|| format!("Key {key:?} in secret {ns}/{name} is not valid UTF-8"))
|
||||
}
|
||||
|
||||
/// Check if a namespace exists.
|
||||
@@ -245,7 +245,7 @@ pub async fn ns_exists(ns: &str) -> Result<bool> {
|
||||
match api.get_opt(ns).await {
|
||||
Ok(Some(_)) => Ok(true),
|
||||
Ok(None) => Ok(false),
|
||||
Err(e) => Err(e).context(format!("Failed to check namespace {ns}")),
|
||||
Err(e) => Err(e).with_ctx(|| format!("Failed to check namespace {ns}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ pub async fn ensure_ns(ns: &str) -> Result<()> {
|
||||
let pp = PatchParams::apply("sunbeam").force();
|
||||
api.patch(ns, &pp, &Patch::Apply(ns_obj))
|
||||
.await
|
||||
.with_context(|| format!("Failed to create namespace {ns}"))?;
|
||||
.with_ctx(|| format!("Failed to create namespace {ns}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ pub async fn create_secret(ns: &str, name: &str, data: HashMap<String, String>)
|
||||
let pp = PatchParams::apply("sunbeam").force();
|
||||
api.patch(name, &pp, &Patch::Apply(secret_obj))
|
||||
.await
|
||||
.with_context(|| format!("Failed to create/update secret {ns}/{name}"))?;
|
||||
.with_ctx(|| format!("Failed to create/update secret {ns}/{name}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -323,12 +323,12 @@ pub async fn kube_exec(
|
||||
let mut attached = pods
|
||||
.exec(pod, cmd_strings, &ep)
|
||||
.await
|
||||
.with_context(|| format!("Failed to exec in pod {ns}/{pod}"))?;
|
||||
.with_ctx(|| format!("Failed to exec in pod {ns}/{pod}"))?;
|
||||
|
||||
let stdout = {
|
||||
let mut stdout_reader = attached
|
||||
.stdout()
|
||||
.context("No stdout stream from exec")?;
|
||||
.ctx("No stdout stream from exec")?;
|
||||
let mut buf = Vec::new();
|
||||
tokio::io::AsyncReadExt::read_to_end(&mut stdout_reader, &mut buf).await?;
|
||||
String::from_utf8_lossy(&buf).to_string()
|
||||
@@ -336,7 +336,7 @@ pub async fn kube_exec(
|
||||
|
||||
let status = attached
|
||||
.take_status()
|
||||
.context("No status channel from exec")?;
|
||||
.ctx("No status channel from exec")?;
|
||||
|
||||
// Wait for the status
|
||||
let exit_code = if let Some(status) = status.await {
|
||||
@@ -372,7 +372,7 @@ pub async fn kube_rollout_restart(ns: &str, deployment: &str) -> Result<()> {
|
||||
|
||||
api.patch(deployment, &PatchParams::default(), &Patch::Strategic(patch))
|
||||
.await
|
||||
.with_context(|| format!("Failed to restart deployment {ns}/{deployment}"))?;
|
||||
.with_ctx(|| format!("Failed to restart deployment {ns}/{deployment}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -485,14 +485,14 @@ pub async fn kustomize_build(overlay: &Path, domain: &str, email: &str) -> Resul
|
||||
.env("PATH", &env_path)
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run kustomize")?;
|
||||
.ctx("Failed to run kustomize")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("kustomize build failed: {stderr}");
|
||||
}
|
||||
|
||||
let mut text = String::from_utf8(output.stdout).context("kustomize output not UTF-8")?;
|
||||
let mut text = String::from_utf8(output.stdout).ctx("kustomize output not UTF-8")?;
|
||||
|
||||
// Domain substitution
|
||||
text = domain_replace(&text, domain);
|
||||
@@ -565,7 +565,7 @@ pub async fn cmd_k8s(kubectl_args: &[String]) -> Result<()> {
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run kubectl")?;
|
||||
.ctx("Failed to run kubectl")?;
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
@@ -580,18 +580,18 @@ pub async fn cmd_bao(bao_args: &[String]) -> Result<()> {
|
||||
let pods: Api<k8s_openapi::api::core::v1::Pod> = Api::namespaced(client.clone(), "data");
|
||||
|
||||
let lp = ListParams::default().labels("app.kubernetes.io/name=openbao");
|
||||
let pod_list = pods.list(&lp).await.context("Failed to list OpenBao pods")?;
|
||||
let pod_list = pods.list(&lp).await.ctx("Failed to list OpenBao pods")?;
|
||||
let ob_pod = pod_list
|
||||
.items
|
||||
.first()
|
||||
.and_then(|p| p.metadata.name.as_deref())
|
||||
.context("OpenBao pod not found -- is the cluster running?")?
|
||||
.ctx("OpenBao pod not found -- is the cluster running?")?
|
||||
.to_string();
|
||||
|
||||
// Get root token
|
||||
let root_token = kube_get_secret_field("data", "openbao-keys", "root-token")
|
||||
.await
|
||||
.context("root-token not found in openbao-keys secret")?;
|
||||
.ctx("root-token not found in openbao-keys secret")?;
|
||||
|
||||
// Build the command string for sh -c
|
||||
let bao_arg_str = bao_args.join(" ");
|
||||
@@ -606,7 +606,7 @@ pub async fn cmd_bao(bao_args: &[String]) -> Result<()> {
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run bao in OpenBao pod")?;
|
||||
.ctx("Failed to run bao in OpenBao pod")?;
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Replaces all `kubectl exec openbao-0 -- sh -c "bao ..."` calls from the
|
||||
//! Python version with direct HTTP API calls via port-forward to openbao:8200.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::error::{Result, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -96,13 +96,13 @@ impl BaoClient {
|
||||
.get(format!("{}/v1/sys/seal-status", self.base_url))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to connect to OpenBao")?;
|
||||
.ctx("Failed to connect to OpenBao")?;
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("OpenBao seal-status returned {status}: {body}");
|
||||
}
|
||||
resp.json().await.context("Failed to parse seal status")
|
||||
resp.json().await.ctx("Failed to parse seal status")
|
||||
}
|
||||
|
||||
/// Initialize OpenBao with the given number of key shares and threshold.
|
||||
@@ -122,14 +122,14 @@ impl BaoClient {
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to initialize OpenBao")?;
|
||||
.ctx("Failed to initialize OpenBao")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("OpenBao init returned {status}: {body}");
|
||||
}
|
||||
resp.json().await.context("Failed to parse init response")
|
||||
resp.json().await.ctx("Failed to parse init response")
|
||||
}
|
||||
|
||||
/// Unseal OpenBao with one key share.
|
||||
@@ -145,14 +145,14 @@ impl BaoClient {
|
||||
.json(&UnsealRequest { key })
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to unseal OpenBao")?;
|
||||
.ctx("Failed to unseal OpenBao")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
bail!("OpenBao unseal returned {status}: {body}");
|
||||
}
|
||||
resp.json().await.context("Failed to parse unseal response")
|
||||
resp.json().await.ctx("Failed to parse unseal response")
|
||||
}
|
||||
|
||||
// ── Secrets engine management ───────────────────────────────────────
|
||||
@@ -172,7 +172,7 @@ impl BaoClient {
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to enable secrets engine")?;
|
||||
.ctx("Failed to enable secrets engine")?;
|
||||
|
||||
let status = resp.status();
|
||||
if status.is_success() || status.as_u16() == 400 {
|
||||
@@ -193,7 +193,7 @@ impl BaoClient {
|
||||
.request(reqwest::Method::GET, &format!("{mount}/data/{path}"))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to read KV secret")?;
|
||||
.ctx("Failed to read KV secret")?;
|
||||
|
||||
if resp.status().as_u16() == 404 {
|
||||
return Ok(None);
|
||||
@@ -204,7 +204,7 @@ impl BaoClient {
|
||||
bail!("KV get {mount}/{path} returned {status}: {body}");
|
||||
}
|
||||
|
||||
let kv_resp: KvReadResponse = resp.json().await.context("Failed to parse KV response")?;
|
||||
let kv_resp: KvReadResponse = resp.json().await.ctx("Failed to parse KV response")?;
|
||||
let data = kv_resp
|
||||
.data
|
||||
.and_then(|d| d.data)
|
||||
@@ -251,7 +251,7 @@ impl BaoClient {
|
||||
.json(&KvWriteRequest { data })
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to write KV secret")?;
|
||||
.ctx("Failed to write KV secret")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
@@ -279,7 +279,7 @@ impl BaoClient {
|
||||
.json(&KvWriteRequest { data })
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to patch KV secret")?;
|
||||
.ctx("Failed to patch KV secret")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
@@ -295,7 +295,7 @@ impl BaoClient {
|
||||
.request(reqwest::Method::DELETE, &format!("{mount}/data/{path}"))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to delete KV secret")?;
|
||||
.ctx("Failed to delete KV secret")?;
|
||||
|
||||
// 404 is fine (already deleted)
|
||||
if !resp.status().is_success() && resp.status().as_u16() != 404 {
|
||||
@@ -323,7 +323,7 @@ impl BaoClient {
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to enable auth method")?;
|
||||
.ctx("Failed to enable auth method")?;
|
||||
|
||||
let status = resp.status();
|
||||
if status.is_success() || status.as_u16() == 400 {
|
||||
@@ -349,7 +349,7 @@ impl BaoClient {
|
||||
.json(&PolicyRequest { policy: policy_hcl })
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to write policy")?;
|
||||
.ctx("Failed to write policy")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
@@ -370,7 +370,7 @@ impl BaoClient {
|
||||
.json(data)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to write to {path}"))?;
|
||||
.with_ctx(|| format!("Failed to write to {path}"))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
@@ -382,7 +382,7 @@ impl BaoClient {
|
||||
if body.is_empty() {
|
||||
Ok(serde_json::Value::Null)
|
||||
} else {
|
||||
serde_json::from_str(&body).context("Failed to parse write response")
|
||||
serde_json::from_str(&body).ctx("Failed to parse write response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ impl BaoClient {
|
||||
.request(reqwest::Method::GET, path)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to read {path}"))?;
|
||||
.with_ctx(|| format!("Failed to read {path}"))?;
|
||||
|
||||
if resp.status().as_u16() == 404 {
|
||||
return Ok(None);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Service management — status, logs, restart.
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use crate::error::{Result, SunbeamError};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::api::{Api, DynamicObject, ListParams, LogParams};
|
||||
use kube::ResourceExt;
|
||||
@@ -397,7 +397,7 @@ pub async fn cmd_get(target: &str, output: &str) -> Result<()> {
|
||||
let pod = api
|
||||
.get_opt(name)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Pod {ns}/{name} not found."))?;
|
||||
.ok_or_else(|| SunbeamError::kube(format!("Pod {ns}/{name} not found.")))?;
|
||||
|
||||
let text = match output {
|
||||
"json" => serde_json::to_string_pretty(&pod)?,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, Result};
|
||||
use crate::error::{Result, ResultExt};
|
||||
use std::path::PathBuf;
|
||||
|
||||
static KUSTOMIZE_BIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/kustomize"));
|
||||
@@ -15,7 +15,7 @@ fn cache_dir() -> PathBuf {
|
||||
fn extract_embedded(data: &[u8], name: &str) -> Result<PathBuf> {
|
||||
let dir = cache_dir();
|
||||
std::fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("Failed to create cache dir: {}", dir.display()))?;
|
||||
.with_ctx(|| format!("Failed to create cache dir: {}", dir.display()))?;
|
||||
|
||||
let dest = dir.join(name);
|
||||
|
||||
@@ -29,7 +29,7 @@ fn extract_embedded(data: &[u8], name: &str) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
std::fs::write(&dest, data)
|
||||
.with_context(|| format!("Failed to write {}", dest.display()))?;
|
||||
.with_ctx(|| format!("Failed to write {}", dest.display()))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crate::error::{Result, ResultExt};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -141,7 +141,7 @@ pub async fn cmd_update() -> Result<()> {
|
||||
let binary_artifact = artifacts
|
||||
.iter()
|
||||
.find(|a| a.name == wanted)
|
||||
.with_context(|| format!("No artifact found for platform '{wanted}'"))?;
|
||||
.with_ctx(|| format!("No artifact found for platform '{wanted}'"))?;
|
||||
|
||||
let checksums_artifact = artifacts
|
||||
.iter()
|
||||
@@ -157,7 +157,7 @@ pub async fn cmd_update() -> Result<()> {
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
.context("Failed to download binary artifact")?
|
||||
.ctx("Failed to download binary artifact")?
|
||||
.bytes()
|
||||
.await?;
|
||||
|
||||
@@ -174,7 +174,7 @@ pub async fn cmd_update() -> Result<()> {
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
.context("Failed to download checksums")?
|
||||
.ctx("Failed to download checksums")?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
@@ -186,7 +186,7 @@ pub async fn cmd_update() -> Result<()> {
|
||||
|
||||
// 5. Atomic self-replace
|
||||
crate::output::step("Installing update...");
|
||||
let current_exe = std::env::current_exe().context("Failed to determine current executable path")?;
|
||||
let current_exe = std::env::current_exe().ctx("Failed to determine current executable path")?;
|
||||
atomic_replace(¤t_exe, &binary_bytes)?;
|
||||
|
||||
crate::output::ok(&format!(
|
||||
@@ -273,7 +273,7 @@ async fn fetch_latest_commit(client: &reqwest::Client, forge_url: &str) -> Resul
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
.context("Failed to query mainline branch")?
|
||||
.ctx("Failed to query mainline branch")?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(resp.commit.id)
|
||||
@@ -287,7 +287,7 @@ async fn fetch_artifacts(client: &reqwest::Client, forge_url: &str) -> Result<Ve
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
.context("Failed to query CI artifacts")?
|
||||
.ctx("Failed to query CI artifacts")?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(resp.artifacts)
|
||||
@@ -329,23 +329,23 @@ fn verify_checksum(binary: &[u8], artifact_name: &str, checksums_text: &str) ->
|
||||
fn atomic_replace(target: &std::path::Path, new_bytes: &[u8]) -> Result<()> {
|
||||
let parent = target
|
||||
.parent()
|
||||
.context("Cannot determine parent directory of current executable")?;
|
||||
.ctx("Cannot determine parent directory of current executable")?;
|
||||
|
||||
let tmp_path = parent.join(".sunbeam-update.tmp");
|
||||
|
||||
// Write new binary
|
||||
fs::write(&tmp_path, new_bytes).context("Failed to write temporary update file")?;
|
||||
fs::write(&tmp_path, new_bytes).ctx("Failed to write temporary update file")?;
|
||||
|
||||
// Set executable permissions (unix)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o755))
|
||||
.context("Failed to set executable permissions")?;
|
||||
.ctx("Failed to set executable permissions")?;
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
fs::rename(&tmp_path, target).context("Failed to replace current executable")?;
|
||||
fs::rename(&tmp_path, target).ctx("Failed to replace current executable")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
40
src/users.rs
40
src/users.rs
@@ -1,9 +1,9 @@
|
||||
//! User management -- Kratos identity operations via port-forwarded admin API.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde_json::Value;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::error::{Result, ResultExt, SunbeamError};
|
||||
use crate::output::{ok, step, table, warn};
|
||||
|
||||
const SMTP_LOCAL_PORT: u16 = 10025;
|
||||
@@ -33,7 +33,7 @@ fn spawn_port_forward(
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to spawn port-forward to {ns}/svc/{svc}"))?;
|
||||
.with_ctx(|| format!("Failed to spawn port-forward to {ns}/svc/{svc}"))?;
|
||||
|
||||
// Give the port-forward time to bind
|
||||
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||
@@ -99,7 +99,7 @@ fn api(
|
||||
req = req.json(b);
|
||||
}
|
||||
|
||||
let resp = req.send().with_context(|| format!("HTTP {method} {url} failed"))?;
|
||||
let resp = req.send().with_ctx(|| format!("HTTP {method} {url} failed"))?;
|
||||
let status = resp.status().as_u16();
|
||||
|
||||
if !resp.status().is_success() {
|
||||
@@ -115,7 +115,7 @@ fn api(
|
||||
return Ok(None);
|
||||
}
|
||||
let val: Value = serde_json::from_str(&text)
|
||||
.with_context(|| format!("Failed to parse API response as JSON: {text}"))?;
|
||||
.with_ctx(|| format!("Failed to parse API response as JSON: {text}"))?;
|
||||
Ok(Some(val))
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ fn find_identity(base_url: &str, target: &str, required: bool) -> Result<Option<
|
||||
}
|
||||
|
||||
if required {
|
||||
bail!("Identity not found: {target}");
|
||||
return Err(SunbeamError::identity(format!("Identity not found: {target}")));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -290,7 +290,7 @@ fn identity_id(identity: &Value) -> Result<String> {
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.context("Identity missing 'id' field")
|
||||
.ok_or_else(|| SunbeamError::identity("Identity missing 'id' field"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -345,7 +345,7 @@ pub async fn cmd_user_get(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
drop(pf);
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&identity)?);
|
||||
@@ -372,7 +372,7 @@ pub async fn cmd_user_create(email: &str, name: &str, schema_id: &str) -> Result
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = kratos_api(&pf.base_url, "/identities", "POST", Some(&body), &[])?
|
||||
.context("Failed to create identity")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Failed to create identity"))?;
|
||||
|
||||
let iid = identity_id(&identity)?;
|
||||
ok(&format!("Created identity: {iid}"));
|
||||
@@ -401,7 +401,7 @@ pub async fn cmd_user_delete(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
kratos_api(
|
||||
&pf.base_url,
|
||||
@@ -421,7 +421,7 @@ pub async fn cmd_user_recover(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
let (link, code) = generate_recovery(&pf.base_url, &iid)?;
|
||||
drop(pf);
|
||||
@@ -438,7 +438,7 @@ pub async fn cmd_user_disable(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
|
||||
let put_body = identity_put_body(&identity, Some("inactive"), None);
|
||||
@@ -471,7 +471,7 @@ pub async fn cmd_user_enable(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
|
||||
let put_body = identity_put_body(&identity, Some("active"), None);
|
||||
@@ -493,7 +493,7 @@ pub async fn cmd_user_set_password(target: &str, password: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
|
||||
let extra = serde_json::json!({
|
||||
@@ -577,15 +577,17 @@ Messages (Matrix):
|
||||
|
||||
let from: Mailbox = format!("Sunbeam Studios <noreply@{domain}>")
|
||||
.parse()
|
||||
.context("Invalid from address")?;
|
||||
let to: Mailbox = email.parse().context("Invalid recipient address")?;
|
||||
.map_err(|e| SunbeamError::Other(format!("Invalid from address: {e}")))?;
|
||||
let to: Mailbox = email
|
||||
.parse()
|
||||
.map_err(|e| SunbeamError::Other(format!("Invalid recipient address: {e}")))?;
|
||||
|
||||
let message = Message::builder()
|
||||
.from(from)
|
||||
.to(to)
|
||||
.subject("Welcome to Sunbeam Studios -- Set Your Password")
|
||||
.body(body_text)
|
||||
.context("Failed to build email message")?;
|
||||
.ctx("Failed to build email message")?;
|
||||
|
||||
let _pf = PortForward::new("lasuite", "postfix", SMTP_LOCAL_PORT, 25)?;
|
||||
|
||||
@@ -595,7 +597,7 @@ Messages (Matrix):
|
||||
|
||||
mailer
|
||||
.send(&message)
|
||||
.context("Failed to send welcome email via SMTP")?;
|
||||
.ctx("Failed to send welcome email via SMTP")?;
|
||||
|
||||
ok(&format!("Welcome email sent to {email}"));
|
||||
Ok(())
|
||||
@@ -669,7 +671,7 @@ pub async fn cmd_user_onboard(
|
||||
});
|
||||
|
||||
let identity = kratos_api(&pf.base_url, "/identities", "POST", Some(&body), &[])?
|
||||
.context("Failed to create identity")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Failed to create identity"))?;
|
||||
|
||||
let iid = identity_id(&identity)?;
|
||||
ok(&format!("Created identity: {iid}"));
|
||||
@@ -729,7 +731,7 @@ pub async fn cmd_user_offboard(target: &str) -> Result<()> {
|
||||
|
||||
let pf = PortForward::kratos()?;
|
||||
let identity = find_identity(&pf.base_url, target, true)?
|
||||
.context("Identity not found")?;
|
||||
.ok_or_else(|| SunbeamError::identity("Identity not found"))?;
|
||||
let iid = identity_id(&identity)?;
|
||||
|
||||
step("Disabling identity...");
|
||||
|
||||
Reference in New Issue
Block a user