use crate::config::CliConfig;
use crate::project_config::ProjectConfig;
use std::collections::HashMap;
use std::process::Command;
async fn get_service_id(config: &CliConfig) -> eyre::Result<String> {
let project = ProjectConfig::find_and_load()?;
let mode = project
.as_ref()
.and_then(|p| p.service.mode.as_deref())
.unwrap_or("rust");
let service_name = if mode == "web" {
project
.as_ref()
.and_then(|p| p.service.name.as_deref())
.ok_or_else(|| eyre::eyre!("Web mode requires [service].name in Cufflink.toml"))?
.to_string()
} else {
let output = Command::new("cargo")
.args(["run", "--", "--emit-manifest"])
.output()?;
if !output.status.success() {
eyre::bail!("Failed to build service. Run from a cufflink service directory.");
}
let stdout = String::from_utf8(output.stdout)?;
let manifest: serde_json::Value = serde_json::from_str(stdout.trim())?;
manifest["name"]
.as_str()
.ok_or_else(|| eyre::eyre!("No service name in manifest"))?
.to_string()
};
let service_name = &service_name;
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::GET,
&format!("{}/api/services", config.api_url),
)
.send()
.await?;
let services: serde_json::Value = resp.json().await?;
let service = services["services"]
.as_array()
.and_then(|arr| {
arr.iter()
.find(|s| s["name"].as_str() == Some(service_name))
})
.ok_or_else(|| eyre::eyre!("Service '{}' not found on platform", service_name))?;
Ok(service["id"]
.as_str()
.ok_or_else(|| eyre::eyre!("Service has no ID"))?
.to_string())
}
pub async fn list(env: Option<&str>) -> eyre::Result<()> {
let config = CliConfig::load_with_env(env)?;
if let Some(ref name) = config.env_name {
println!("Environment: {}", name);
}
let service_id = get_service_id(&config).await?;
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::GET,
&format!("{}/api/services/{}/config", config.api_url, service_id),
)
.send()
.await?;
if resp.status().is_success() {
let body: serde_json::Value = resp.json().await?;
if let Some(configs) = body["configs"].as_array() {
if configs.is_empty() {
println!("No configuration values set.");
} else {
println!("{:<20} {:<30} SECRET", "KEY", "VALUE");
println!("{}", "-".repeat(60));
for c in configs {
println!(
"{:<20} {:<30} {}",
c["key"].as_str().unwrap_or(""),
c["value"].as_str().unwrap_or(""),
if c["is_secret"].as_bool() == Some(true) {
"yes"
} else {
"no"
},
);
}
}
}
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!("Failed to list config ({}): {}", status, body);
}
Ok(())
}
pub async fn set(key: &str, value: &str, is_secret: bool, env: Option<&str>) -> eyre::Result<()> {
let config = CliConfig::load_with_env(env)?;
if let Some(ref name) = config.env_name {
println!("Environment: {}", name);
}
let service_id = get_service_id(&config).await?;
let payload = serde_json::json!({
"key": key,
"value": value,
"is_secret": is_secret,
});
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::PUT,
&format!("{}/api/services/{}/config", config.api_url, service_id),
)
.json(&payload)
.send()
.await?;
if resp.status().is_success() {
println!("Config '{}' set successfully", key);
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!("Failed to set config ({}): {}", status, body);
}
Ok(())
}
pub async fn delete(key: &str, env: Option<&str>) -> eyre::Result<()> {
let config = CliConfig::load_with_env(env)?;
if let Some(ref name) = config.env_name {
println!("Environment: {}", name);
}
let service_id = get_service_id(&config).await?;
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::DELETE,
&format!(
"{}/api/services/{}/config/{}",
config.api_url, service_id, key
),
)
.send()
.await?;
if resp.status().is_success() {
println!("Config '{}' deleted", key);
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!("Failed to delete config ({}): {}", status, body);
}
Ok(())
}
pub async fn sync(env: Option<&str>) -> eyre::Result<()> {
let config = CliConfig::load_with_env(env)?;
if let Some(ref name) = config.env_name {
println!("Environment: {}", name);
}
let service_id = get_service_id(&config).await?;
let project =
ProjectConfig::find_and_load()?.ok_or_else(|| eyre::eyre!("No Cufflink.toml found"))?;
let env_name = env
.map(|s| s.to_string())
.or_else(|| project.service.default_env.clone())
.ok_or_else(|| eyre::eyre!("No environment specified"))?;
let env_config = project.get_env(&env_name)?;
sync_to_platform(
&config,
&service_id,
&env_config.config,
&env_config.secrets,
&project,
&env_name,
)
.await
}
pub async fn sync_to_platform(
cli_config: &CliConfig,
service_id: &str,
configs: &HashMap<String, String>,
secrets: &HashMap<String, String>,
project: &ProjectConfig,
env_name: &str,
) -> eyre::Result<()> {
let client = cli_config.http_client();
let config_url = format!("{}/api/services/{}/config", cli_config.api_url, service_id);
let mut synced = 0;
for (key, value) in configs {
let payload = serde_json::json!({
"key": key,
"value": value,
"is_secret": false,
});
let resp = cli_config
.auth_request(&client, reqwest::Method::PUT, &config_url)
.json(&payload)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!("Failed to sync config '{}' ({}): {}", key, status, body);
}
synced += 1;
}
let mut secrets_synced = 0;
if !secrets.is_empty() {
let store = super::secrets_cmd::load_store(project, env_name)?;
for (config_key, store_name) in secrets {
let value: String = store.get(store_name).map_err(|e| {
eyre::eyre!(
"Secret '{}' not found in securestore for '{}': {}",
store_name,
env_name,
e
)
})?;
let payload = serde_json::json!({
"key": config_key,
"value": value,
"is_secret": true,
});
let resp = cli_config
.auth_request(&client, reqwest::Method::PUT, &config_url)
.json(&payload)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!(
"Failed to sync secret '{}' ({}): {}",
config_key,
status,
body
);
}
secrets_synced += 1;
}
}
if synced > 0 || secrets_synced > 0 {
println!(
" Synced {} config(s) and {} secret(s)",
synced, secrets_synced
);
}
Ok(())
}