// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use super::exec_snippet::FloweyPipelineStaticDb; use super::exec_snippet::VarDbBackendKind; use anyhow::Context; use clap::ValueEnum; use flowey_core::node::RuntimeVarDb; use std::io::Read; use std::io::Write; use std::path::Path; use std::path::PathBuf; pub struct VarDbRequest<'a> { flowey_bin: &'a str, job_idx: usize, var_name: &'a str, action: RequestAction<'a>, is_raw_string: bool, condvar: Option<&'a str>, } enum RequestAction<'a> { WriteToEnv { backend: EnvBackend, env: &'a str, }, Update { file: Option<&'a Path>, is_secret: bool, env_source: Option<&'a str>, }, } pub struct VarDbRequestBuilder<'a> { flowey_bin: &'a str, job_idx: usize, } impl<'a> VarDbRequestBuilder<'a> { pub fn new(flowey_bin: &'a str, job_idx: usize) -> Self { Self { flowey_bin, job_idx, } } fn req<'b>(&'b self, var_name: &'b str, action: RequestAction<'b>) -> VarDbRequest<'b> { VarDbRequest::new(self.flowey_bin, self.job_idx, var_name, action) } pub fn write_to_ado_env<'b>(&'b self, var_name: &'b str, env: &'b str) -> VarDbRequest<'b> { self.req( var_name, RequestAction::WriteToEnv { backend: EnvBackend::Ado, env, }, ) } pub fn write_to_gh_env<'b>(&'b self, var_name: &'b str, env: &'b str) -> VarDbRequest<'b> { self.req( var_name, RequestAction::WriteToEnv { backend: EnvBackend::Github, env, }, ) } pub fn update_from_stdin<'b>(&'b self, var_name: &'b str, is_secret: bool) -> VarDbRequest<'b> { self.req( var_name, RequestAction::Update { file: None, is_secret, env_source: None, }, ) } #[expect(dead_code)] pub fn update_from_file<'b>( &'b self, var_name: &'b str, file: &'b Path, is_secret: bool, ) -> VarDbRequest<'b> { self.req( var_name, RequestAction::Update { file: Some(file), is_secret, env_source: None, }, ) } } impl<'a> VarDbRequest<'a> { fn new( flowey_bin: &'a str, job_idx: usize, var_name: &'a str, action: RequestAction<'a>, ) -> Self { Self { flowey_bin, job_idx, var_name, action, is_raw_string: false, condvar: None, } } pub fn raw_string(self, is_raw_string: bool) -> Self { Self { is_raw_string, ..self } } pub fn condvar(self, condvar: Option<&'a str>) -> Self { Self { condvar, ..self } } #[track_caller] pub fn env_source(mut self, source: Option<&'a str>) -> Self { let RequestAction::Update { env_source, .. } = &mut self.action else { panic!("env_source can only be set on Update actions"); }; *env_source = source; self } } impl std::fmt::Display for VarDbRequest<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { flowey_bin, job_idx, var_name, ref action, is_raw_string, condvar, } = *self; write!(f, r#"{flowey_bin} v {job_idx} '{var_name}'"#)?; if is_raw_string { f.write_str(" --is-raw-string")?; } if let Some(condvar) = condvar { write!(f, " --condvar {condvar}")?; } match *action { RequestAction::WriteToEnv { backend, env } => { write!( f, " write-to-env {backend} {env}", backend = backend.to_possible_value().unwrap().get_name() )?; } RequestAction::Update { file, is_secret, env_source, } => { write!(f, " update")?; if is_secret { f.write_str(" --is-secret")?; } if let Some(env_source) = env_source { write!(f, " --env-source {env_source}")?; } if let Some(file) = file { write!(f, " {}", file.to_str().unwrap())?; } } } Ok(()) } } /// (internal) interact with the runtime variable database #[derive(clap::Args)] pub struct VarDb { /// job idx corresponding to the var db to access pub(crate) job_idx: usize, /// Runtime variable to access var_name: String, /// Variable is a raw string, and should be read/written as a plain string. #[clap(long)] is_raw_string: bool, /// Only run if the given variable is true. #[clap(long)] condvar: Option, #[clap(subcommand)] action: Option, } #[derive(clap::Subcommand)] enum VarDbAction { WriteToEnv { backend: EnvBackend, env: String, }, Update { #[clap(long)] env_source: Option, #[clap(long)] is_secret: bool, file: Option, }, } #[derive(clap::ValueEnum, Copy, Clone)] enum EnvBackend { Ado, Github, } impl VarDb { pub fn run(self) -> anyhow::Result<()> { let Self { job_idx, var_name, is_raw_string, condvar, action, } = self; let mut runtime_var_db = open_var_db(job_idx)?; if let Some(condvar) = condvar { let (condvar_data, _) = runtime_var_db.get_var(&condvar); let set: bool = serde_json::from_slice(&condvar_data).unwrap(); if !set { return Ok(()); } } let get = |runtime_var_db: &mut Box, var_name: &str| { let (mut data, data_is_secret) = runtime_var_db.get_var(var_name); // HACK: only one kind of db, so we know what routine to use if is_raw_string { let s: String = serde_json::from_slice(&data).unwrap(); data = s.into(); } (data, data_is_secret) }; let env_source_name = |env_source| format!(".env.is_secret.{env_source}"); match action { None => { // Raw get. let (data, _) = get(&mut runtime_var_db, &var_name); std::io::stdout().write_all(&data).unwrap(); } Some(VarDbAction::WriteToEnv { backend, env }) => { let (data, is_secret) = get(&mut runtime_var_db, &var_name); if is_secret { // Remember that this environment variable is secret so that // it cannot be easily laundered into a non-secret variable. runtime_var_db.set_var(&env_source_name(&env), false, "null".into()); } match backend { EnvBackend::Ado => { print!("##vso[task.setvariable variable={env};issecret={is_secret}]"); std::io::stdout().write_all(&data).unwrap(); println!(); } EnvBackend::Github => { let data_string = String::from_utf8(data)?; if is_secret { data_string.lines().for_each(|line| { println!("::add-mask::{}", line); }); } let gh_env_file_path = std::env::var("GITHUB_ENV")?; let mut gh_env_file = fs_err::OpenOptions::new() .append(true) .open(gh_env_file_path)?; let gh_env_var_assignment = format!("{}< { if !is_secret { // If the source environment variable for this was known to // be a secret, then mark it secret. if let Some(env_source) = env_source { is_secret |= runtime_var_db .try_get_var(&env_source_name(&env_source)) .is_some(); } } let data = if let Some(file) = file { let mut data = fs_err::read(file)?; // HACK: only one kind of db, so we know what routine to use if is_raw_string { let s: String = String::from_utf8(data).unwrap(); data = serde_json::to_vec(&s).unwrap(); } data } else { let mut data = Vec::new(); std::io::stdin().read_to_end(&mut data).unwrap(); // HACK: only one kind of db, so we know what routine to use if is_raw_string { // account for bash HEREDOCs including a trailing newline // TODO: probably want this to be configurable. if matches!(data.last(), Some(b'\n')) { data.pop(); } let s = String::from_utf8(data).unwrap(); data = serde_json::to_vec(&s).unwrap(); } data }; runtime_var_db.set_var(&var_name, is_secret, data); } } Ok(()) } } /// Obtain a handle to a runtime var db /// /// CONTRACT: Requires a pipeline-specific `pipeline.json` file to be in the /// same dir as the flowey exe /// /// CONTRACT: Requires a var-backend specific var db file called /// `job{job_idx}.` to be in the same dir as the flowey exe pub(crate) fn open_var_db(job_idx: usize) -> anyhow::Result> { let current_exe = std::env::current_exe().context("failed to get path to current flowey executable")?; let FloweyPipelineStaticDb { var_db_backend_kind, .. } = { let pipeline_static_db = fs_err::File::open(current_exe.with_file_name("pipeline.json"))?; serde_json::from_reader(pipeline_static_db)? }; Ok(match var_db_backend_kind { VarDbBackendKind::Json => { Box::new(crate::var_db::single_json_file::SingleJsonFileVarDb::new( current_exe.with_file_name(format!("job{job_idx}.json")), )?) } }) }