use anyhow::{Context, Result};
use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Table};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Deserialize)]
struct EnvVarResponse {
key: String,
value: String, is_secret: bool,
is_protected: bool,
}
#[derive(Debug, Deserialize)]
struct EnvVarsResponse {
env_vars: Vec<EnvVarResponse>,
}
#[derive(Debug, Serialize)]
struct SetEnvVarRequest {
value: String,
#[serde(default)]
is_secret: bool,
#[serde(default)]
is_protected: bool,
}
async fn fetch_env_vars_response(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
) -> Result<EnvVarsResponse> {
let url = format!("{}/api/v1/projects/{}/env", backend_url, project);
let response = http_client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.context("Failed to fetch environment variables")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to fetch environment variables (status {}): {}",
status,
error_text
);
}
let env_vars_response: EnvVarsResponse = response
.json()
.await
.context("Failed to parse environment variables response")?;
Ok(env_vars_response)
}
pub async fn fetch_preview_env_vars(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
deployment_group: &str,
) -> Result<(Vec<(String, String)>, Vec<String>)> {
let url = format!(
"{}/api/v1/projects/{}/env/preview?deployment_group={}",
backend_url, project, deployment_group
);
let response = http_client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.context("Failed to fetch preview environment variables")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to fetch preview environment variables (status {}): {}",
status,
error_text
);
}
let env_response: EnvVarsResponse = response
.json()
.await
.context("Failed to parse preview environment variables response")?;
let mut loadable_vars = Vec::new();
let mut protected_keys = Vec::new();
for var in env_response.env_vars {
if var.is_protected {
protected_keys.push(var.key);
} else {
loadable_vars.push((var.key, var.value));
}
}
Ok((loadable_vars, protected_keys))
}
#[allow(clippy::too_many_arguments)]
pub async fn set_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
key: &str,
value: &str,
is_secret: bool,
is_protected: bool,
) -> Result<()> {
let url = format!("{}/api/v1/projects/{}/env/{}", backend_url, project, key);
let payload = SetEnvVarRequest {
value: value.to_string(),
is_secret,
is_protected,
};
let response = http_client
.put(&url)
.header("Authorization", format!("Bearer {}", token))
.json(&payload)
.send()
.await
.context("Failed to set environment variable")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to set environment variable (status {}): {}",
status,
error_text
);
}
let var_type = if is_secret {
if is_protected {
"protected secret"
} else {
"unprotected secret"
}
} else {
"plain text"
};
println!(
"✓ Set {} variable '{}' for project '{}'",
var_type, key, project
);
Ok(())
}
pub async fn list_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
) -> Result<()> {
let env_vars_response =
fetch_env_vars_response(http_client, backend_url, token, project).await?;
if env_vars_response.env_vars.is_empty() {
println!(
"No environment variables configured for project '{}'",
project
);
return Ok(());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_header(vec![
Cell::new("KEY").add_attribute(Attribute::Bold),
Cell::new("VALUE").add_attribute(Attribute::Bold),
Cell::new("TYPE").add_attribute(Attribute::Bold),
Cell::new("PROTECTED").add_attribute(Attribute::Bold),
]);
for var in env_vars_response.env_vars {
let var_type = if var.is_secret { "secret" } else { "plain" };
let protected = if var.is_secret {
if var.is_protected {
"yes"
} else {
"no"
}
} else {
"-"
};
table.add_row(vec![
Cell::new(&var.key),
Cell::new(&var.value),
Cell::new(var_type),
Cell::new(protected),
]);
}
println!("{}", table);
println!("\nProject: {}", project);
println!("Note: Secret values are always masked for security");
Ok(())
}
pub async fn get_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
key: &str,
) -> Result<()> {
let env_vars_response =
fetch_env_vars_response(http_client, backend_url, token, project).await?;
let env_var = env_vars_response
.env_vars
.into_iter()
.find(|v| v.key == key)
.ok_or_else(|| anyhow::anyhow!("Environment variable '{}' not found", key))?;
if env_var.is_secret && env_var.is_protected {
anyhow::bail!(
"Cannot retrieve value: '{}' is a protected secret.\n\
To make it unprotected, update it with: rise env set {} <value> --secret --protected=false",
key, key
);
}
if env_var.is_secret && !env_var.is_protected {
let url = format!(
"{}/api/v1/projects/{}/env/{}/value",
backend_url, project, key
);
let response = http_client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.context("Failed to get environment variable value")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to get environment variable value (status {}): {}",
status,
error_text
);
}
#[derive(Debug, serde::Deserialize)]
struct EnvVarValueResponse {
value: String,
}
let value_response: EnvVarValueResponse = response
.json()
.await
.context("Failed to parse environment variable value response")?;
println!("{}", value_response.value);
} else {
println!("{}", env_var.value);
}
Ok(())
}
pub async fn unset_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
key: &str,
) -> Result<()> {
let url = format!("{}/api/v1/projects/{}/env/{}", backend_url, project, key);
let response = http_client
.delete(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.context("Failed to delete environment variable")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to delete environment variable (status {}): {}",
status,
error_text
);
}
println!("✓ Deleted variable '{}' from project '{}'", key, project);
Ok(())
}
pub async fn import_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
file_path: &PathBuf,
) -> Result<()> {
let contents = std::fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;
let mut success_count = 0;
let mut error_count = 0;
for (line_num, line) in contents.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() != 2 {
eprintln!(
"Warning: Line {} has invalid format (expected KEY=value): {}",
line_num + 1,
line
);
error_count += 1;
continue;
}
let key = parts[0].trim();
let value_part = parts[1];
let (value, is_secret) = if let Some(stripped) = value_part.strip_prefix("secret:") {
(stripped, true)
} else {
(value_part, false)
};
if !key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
eprintln!(
"Warning: Line {} has invalid key name '{}' (must be alphanumeric with underscores)",
line_num + 1,
key
);
error_count += 1;
continue;
}
let is_protected = is_secret;
match set_env(
http_client,
backend_url,
token,
project,
key,
value,
is_secret,
is_protected,
)
.await
{
Ok(_) => success_count += 1,
Err(e) => {
eprintln!(
"Warning: Failed to set variable '{}' from line {}: {}",
key,
line_num + 1,
e
);
error_count += 1;
}
}
}
println!(
"\n✓ Import complete: {} variables set, {} errors",
success_count, error_count
);
if error_count > 0 {
anyhow::bail!("Import completed with {} errors", error_count);
}
Ok(())
}
pub async fn list_deployment_env(
http_client: &Client,
backend_url: &str,
token: &str,
project: &str,
deployment_id: &str,
) -> Result<()> {
let url = format!(
"{}/api/v1/projects/{}/deployments/{}/env",
backend_url, project, deployment_id
);
let response = http_client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.context("Failed to list deployment environment variables")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
anyhow::bail!(
"Failed to list deployment environment variables (status {}): {}",
status,
error_text
);
}
let env_vars_response: EnvVarsResponse = response
.json()
.await
.context("Failed to parse environment variables response")?;
if env_vars_response.env_vars.is_empty() {
println!(
"No environment variables configured for deployment '{}' in project '{}'",
deployment_id, project
);
return Ok(());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_header(vec![
Cell::new("KEY").add_attribute(Attribute::Bold),
Cell::new("VALUE").add_attribute(Attribute::Bold),
Cell::new("TYPE").add_attribute(Attribute::Bold),
]);
for var in env_vars_response.env_vars {
let var_type = if var.is_secret { "secret" } else { "plain" };
table.add_row(vec![
Cell::new(&var.key),
Cell::new(&var.value),
Cell::new(var_type),
]);
}
println!("{}", table);
println!("\nProject: {}", project);
println!("Deployment: {}", deployment_id);
println!("Note: Secret values are always masked for security");
println!("Note: Deployment environment variables are read-only snapshots");
Ok(())
}