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