use std::{collections::BTreeMap, fs, path::Path};
use serde::Serialize;
use crate::{
CliResult,
cloudflare::{Cloudflare, Echo},
output::{CliError, Report},
project::Project,
secret::SECRET_KEY_BASE,
};
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct SecretStatus {
pub name: String,
pub local: bool,
pub deployed: bool,
}
pub fn parse_vars(text: &str) -> BTreeMap<String, String> {
text.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split_once('='))
.map(|(name, value)| {
let value = value.trim();
let unquoted = value
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.or_else(|| value.strip_prefix('\'').and_then(|value| value.strip_suffix('\'')))
.unwrap_or(value);
(name.trim().to_owned(), unquoted.to_owned())
})
.collect()
}
pub(crate) fn read_vars(root: &Path, file: &str) -> Result<BTreeMap<String, String>, CliError> {
match fs::read_to_string(root.join(file)) {
Ok(text) => Ok(parse_vars(&text)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(BTreeMap::new()),
Err(err) => Err(CliError::new(format!("cannot read {file}: {err}"))),
}
}
pub fn list(project: &Project, json: bool) -> CliResult {
let local = read_vars(&project.root, ".dev.vars")?;
let worker = project.config()?.worker_name()?.to_owned();
let deployed = Cloudflare::new(&project.root, Echo::for_json(json)).secret_names(&worker)?.unwrap_or_default();
let mut names: Vec<&String> = local.keys().chain(&deployed).collect();
names.sort();
names.dedup();
let secrets = names
.into_iter()
.map(|name| SecretStatus {
name: name.clone(),
local: local.contains_key(name),
deployed: deployed.contains(name),
})
.collect::<Vec<_>>();
let mut report = Report::new("secrets list");
let missing: Vec<&str> = secrets
.iter()
.filter(|secret| secret.local && !secret.deployed && !DEV_ONLY.contains(&secret.name.as_str()))
.map(|secret| secret.name.as_str())
.collect();
if !missing.is_empty() {
report.next.push(format!("ocre secrets push {} --file <production values>", missing.join(" ")));
}
report.secrets = Some(secrets);
Ok(report)
}
const DEV_ONLY: [&str; 3] = [SECRET_KEY_BASE, "MAIL_ADAPTER", CACHE_STORE];
pub const CACHE_STORE: &str = ocre::cache::STORE_VAR;
pub fn set_dev_cache(root: &Path, on: bool) -> Result<String, CliError> {
let path = root.join(".dev.vars");
let text = fs::read_to_string(&path).unwrap_or_default();
let prefix = format!("{CACHE_STORE}=");
let mut lines: Vec<&str> = text.lines().filter(|line| !line.trim_start().starts_with(&prefix)).collect();
let off = format!("{CACHE_STORE}=null");
if !on {
lines.push(&off);
}
let mut out = lines.join("\n");
out.push('\n');
fs::write(&path, out)?;
Ok(if on {
"caching on: CACHE_STORE removed from .dev.vars".to_owned()
} else {
"caching off: CACHE_STORE=null in .dev.vars".to_owned()
})
}
pub fn fetch(project: &Project, name: &str, file: &str) -> CliResult {
let vars = read_vars(&project.root, file)?;
let value = vars.get(name).ok_or_else(|| {
CliError::new(format!("{name} is not set in {file}")).hint(format!(
"add `{name}=<value>` to {file}; deployed values cannot be read back (Cloudflare only stores them)"
))
})?;
Ok(Report { secret: Some(value.clone()), ..Report::new("secrets fetch") })
}
pub fn push(project: &Project, names: &[String], file: &str, json: bool) -> CliResult {
if names.is_empty() {
return Err(CliError::new("name the secrets to upload")
.hint("e.g. `ocre secrets push GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET`; `ocre secrets list` shows them"));
}
let vars = read_vars(&project.root, file)?;
let mut values = BTreeMap::new();
for name in names {
if file == ".dev.vars" && DEV_ONLY.contains(&name.as_str()) {
return Err(CliError::new(format!("{name} in .dev.vars is a development value")).hint(
"production needs its own: `ocre deploy` creates SECRET_KEY_BASE; for others put the production \
value in another git-ignored file (e.g. .prod.vars) and pass `--file <it>`",
));
}
let value = vars.get(name).ok_or_else(|| {
CliError::new(format!("{name} is not set in {file}"))
.hint(format!("add `{name}=<value>` to {file} (a git-ignored file), then run this again"))
})?;
values.insert(name.clone(), value.clone());
}
let worker = project.config()?.worker_name()?.to_owned();
Cloudflare::new(&project.root, Echo::for_json(json)).put_secrets(&worker, &values)?;
let mut report = Report::new("secrets push");
report.ran = names.iter().map(|name| format!("uploaded {name}")).collect();
Ok(report)
}