use std::collections::BTreeMap;
use std::sync::Arc;
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
use orca_core::api_types::{SecretRef, SecretUsage, SecretsUsageResponse};
use orca_core::secrets::{SecretStore, default_path, extract_refs};
use crate::state::AppState;
#[derive(Debug, Deserialize)]
pub struct SetSecretRequest {
pub value: String,
}
pub async fn list_secrets() -> impl IntoResponse {
match SecretStore::open(default_path()) {
Ok(store) => {
let keys: Vec<String> = store.list().into_iter().collect();
(StatusCode::OK, Json(serde_json::json!({ "keys": keys }))).into_response()
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}
pub async fn set_secret(
Path(key): Path<String>,
Json(body): Json<SetSecretRequest>,
) -> impl IntoResponse {
let mut store = match SecretStore::open(default_path()) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response();
}
};
match store.set(&key, &body.value) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({ "status": "set", "key": key })),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}
pub async fn secrets_usage(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let store = match SecretStore::open(default_path()) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response();
}
};
let stored_keys: Vec<String> = store.list();
let mut refs_by_key: BTreeMap<(Option<String>, String), Vec<SecretRef>> = BTreeMap::new();
{
let services = state.services.read().await;
for svc in services.values() {
let project = svc.config.project.clone();
let mut seen_for_service: std::collections::HashSet<(Option<String>, String)> =
std::collections::HashSet::new();
for value in svc.config.env.values() {
for r in extract_refs(value) {
let map_key = (r.scope.clone(), r.key.clone());
if seen_for_service.insert(map_key.clone()) {
refs_by_key.entry(map_key).or_default().push(SecretRef {
service_name: svc.config.name.clone(),
project: project.clone(),
});
}
}
}
}
}
let mut out: Vec<SecretUsage> = stored_keys
.iter()
.map(|k| {
let map_key = (None, k.clone());
let refs = refs_by_key.remove(&map_key).unwrap_or_default();
SecretUsage {
key: k.clone(),
scope: None,
refs,
in_store: true,
}
})
.collect();
for ((scope, key), refs) in refs_by_key {
out.push(SecretUsage {
key,
scope,
refs,
in_store: false,
});
}
Json(SecretsUsageResponse { secrets: out }).into_response()
}
pub async fn remove_secret(Path(key): Path<String>) -> impl IntoResponse {
let mut store = match SecretStore::open(default_path()) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response();
}
};
match store.remove(&key) {
Ok(true) => (
StatusCode::OK,
Json(serde_json::json!({ "status": "removed", "key": key })),
)
.into_response(),
Ok(false) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "not found", "key": key })),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}