use std::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
io::Write,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process::Command,
sync::Mutex,
};
use crate::{
secret::SecretString,
workflow::{NowJob, NowJobContainer, NowStepEnvVar, NowWorkflow},
};
#[derive(Debug)]
pub(crate) struct NowEnvironment {
pub(crate) secrets: HashMap<String, SecretString>,
pub(crate) vars: HashMap<String, String>,
pub(crate) local_env: HashMap<OsString, OsString>,
pub(crate) uploads: Mutex<HashMap<String, PathBuf>>,
}
struct ParsedWorkflow {
vars: HashSet<String>,
secrets: HashSet<String>,
}
impl NowEnvironment {
pub(crate) fn get(
workflow: &Path,
env_file: Option<&PathBuf>,
nixpkgs_expr: &str,
) -> color_eyre::Result<NowEnvironment> {
let mut env_vars: HashMap<OsString, OsString> = HashMap::new();
if let Some(env_file) = env_file {
env_vars.extend(
dotenvy::from_path_iter(env_file)?.filter_map(|result| {
result.ok().map(|(key, value)| (key.into(), value.into()))
}),
);
};
env_vars.extend(std::env::vars_os());
let parsed_workflow = Self::parse_workflow(workflow, &env_vars, nixpkgs_expr)?;
let secrets: color_eyre::Result<HashMap<String, SecretString>> = parsed_workflow
.secrets
.into_iter()
.map(
|secret| match env_vars.remove(OsStr::from_bytes(secret.as_bytes())) {
Some(value) => {
let value = SecretString::new(value.into_string().map_err(|_| {
color_eyre::eyre::eyre!("Invalid value for {secret} envvar")
})?);
Ok((secret, value))
}
None => Err(color_eyre::eyre::eyre!("Missing {secret} envvar")),
},
)
.collect();
let vars: color_eyre::Result<HashMap<String, String>> = parsed_workflow
.vars
.into_iter()
.map(
|var| match env_vars.remove(OsStr::from_bytes(var.as_bytes())) {
Some(value) => {
let value = value.into_string().map_err(|_| {
color_eyre::eyre::eyre!("Invalid value for {var} envvar")
})?;
Ok((var, value))
}
None => Err(color_eyre::eyre::eyre!("Missing {var} envvar")),
},
)
.collect();
Ok(Self {
secrets: secrets?,
vars: vars?,
local_env: env_vars,
uploads: Default::default(),
})
}
fn parse_workflow(
workflow: &Path,
env_vars: &HashMap<OsString, OsString>,
nixpkgs_expr: &str,
) -> color_eyre::Result<ParsedWorkflow> {
let workflow_canonical = std::fs::canonicalize(workflow)?;
let workflow_str = workflow_canonical
.to_str()
.ok_or_else(|| color_eyre::eyre::eyre!("non-UTF8 path"))?;
let workflow_path = format!("(/. + {})", serde_json::to_string(&workflow_str)?);
let env_var_json = serde_json::to_string(&serde_json::to_string(
&env_vars
.keys()
.filter_map(|key| key.to_str())
.collect::<Vec<_>>(),
)?)?;
let workflow_args = format!("{{ nixpkgs = ({}); }}", nixpkgs_expr);
let nix_command = format!(
"import ./nix/env.nix {workflow_args} {workflow_path} (builtins.fromJSON {env_var_json})"
);
let mut command = Command::new("nix");
command.args([
"--extra-experimental-features",
"nix-command",
"eval",
"--impure",
"--json",
]);
let output = command.arg("--expr").arg(nix_command).output()?;
if !output.status.success() {
let mut stderr = std::io::stderr();
stderr.write_all(&output.stderr)?;
stderr.flush()?;
return Err(color_eyre::eyre::eyre!(
"Failed to parse workflow for variables"
));
}
let workflow: NowWorkflow = serde_json::from_slice(&output.stdout)?;
let mut vars: HashSet<String> = HashSet::new();
let mut secrets: HashSet<String> = HashSet::new();
let vars_regex = regex::Regex::new(r#"@@__nowVar_([^@]+)@@"#).expect("valid regex");
let mut job_fn = |job: &NowJob| {
for step in &job.steps {
for env_value in step.env.values() {
match env_value {
NowStepEnvVar::Plain(var) => {
vars.extend(vars_regex.captures_iter(var).map(|needle| {
needle.get(1).expect("is match").as_str().to_string()
}));
}
NowStepEnvVar::Secret(secret) => {
secrets.insert(secret.secret_name.clone());
}
NowStepEnvVar::Download(_) => {}
}
}
}
};
for job in workflow.jobs.values() {
match job {
NowJobContainer::Single(job) => (job_fn)(job),
NowJobContainer::Multiple(job_vec) => {
for job in job_vec {
(job_fn)(job)
}
}
}
}
for secret in &secrets {
if vars.contains(secret) {
return Err(color_eyre::eyre::eyre!(
"Secret '{secret}' cannot also be used as a regular variable"
));
}
}
Ok(ParsedWorkflow { vars, secrets })
}
pub(crate) fn generate_env_vars_for_step(
&self,
step_env: &HashMap<String, NowStepEnvVar>,
) -> color_eyre::Result<HashMap<OsString, OsString>> {
let mut map: HashMap<OsString, OsString> = HashMap::with_capacity(step_env.len());
{
let uploads = self.uploads.lock().expect("not poisoned");
for (key, value) in step_env {
match value {
NowStepEnvVar::Plain(value) => {
map.insert(key.into(), value.into());
}
NowStepEnvVar::Secret(secret) => {
map.insert(
key.into(),
self.secrets
.get(&secret.secret_name)
.ok_or_else(|| {
color_eyre::eyre::eyre!(
"Missing secret {}",
&secret.secret_name
)
})?
.get_secret_value()
.into(),
);
}
NowStepEnvVar::Download(download) => {
let download_path =
uploads.get(&download.download_name).ok_or_else(|| {
color_eyre::eyre::eyre!(
"Missing download {}",
&download.download_name
)
})?;
map.insert(key.into(), download_path.into());
}
}
}
}
Ok(map)
}
}