refactor(notstrap): replace EnvInjector with SecretResolver
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled

Remove the EnvInjector closure and replace with a secrets_config path
that loads notsecrets.toml and uses SecretResolver for env injection.
Existing parse_env_line validation (blocked keys, null bytes) is
preserved in the injection loop.
This commit is contained in:
2026-06-06 09:57:20 -04:00
parent 1359def673
commit 50ff598684
3 changed files with 43 additions and 33 deletions

View File

@@ -2,19 +2,18 @@ use anyhow::{Context, Result};
use notcore::{HookPhase, Report, StepStatus}; use notcore::{HookPhase, Report, StepStatus};
use notfiles::{LinkOptions, link}; use notfiles::{LinkOptions, link};
use nothooks::{HookRunner, run_phase}; use nothooks::{HookRunner, run_phase};
use notsecrets::identities::Identity;
use notsecrets::sources::IdentitySource; use notsecrets::sources::IdentitySource;
use notsecrets::{BitwardenSource, FileSource, PromptSource, YubikeySource, resolve_identities}; use notsecrets::{
BitwardenSource, FileSource, PromptSource, SecretResolver, YubikeySource, resolve_identities,
};
use serde::Deserialize; use serde::Deserialize;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
pub mod prereqs; pub mod prereqs;
pub mod repo; pub mod repo;
pub use notnet::TailscaleOptions; pub use notnet::TailscaleOptions;
type EnvInjector = Box<dyn Fn(&Path, Vec<Box<dyn Identity>>) -> Result<String>>;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NotstrapConfig { pub struct NotstrapConfig {
pub bootstrap: BootstrapSection, pub bootstrap: BootstrapSection,
@@ -56,8 +55,8 @@ pub struct BootstrapOptions {
pub tailscale: Option<Option<TailscaleOptions>>, pub tailscale: Option<Option<TailscaleOptions>>,
/// None = skip prereq check (tests). Some(f) = run f(). /// None = skip prereq check (tests). Some(f) = run f().
pub check_prereqs: Option<Box<dyn Fn() -> Result<()>>>, pub check_prereqs: Option<Box<dyn Fn() -> Result<()>>>,
/// None = skip env injection (tests). Some(f) = decrypt sops at path and inject. /// Path to notsecrets.toml. None = skip secret resolution.
pub env_injector: Option<EnvInjector>, pub secrets_config: Option<PathBuf>,
} }
pub fn run(opts: BootstrapOptions) -> Result<Report> { pub fn run(opts: BootstrapOptions) -> Result<Report> {
@@ -142,7 +141,7 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
] ]
}; };
let identities = match resolve_identities(sources) { let _identities = match resolve_identities(sources) {
Ok(ids) => { Ok(ids) => {
report.add("age key", StepStatus::Ok); report.add("age key", StepStatus::Ok);
ids ids
@@ -153,33 +152,44 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
} }
}; };
// 5. Decrypt sops secrets (optional) // 5b. Resolve secrets via notsecrets.toml (optional)
if let Some(injector) = opts.env_injector { if let Some(secrets_path) = opts.secrets_config {
let sops_path = dotfiles_dir.join(&cfg.bootstrap.sops_file); match notsecrets::load_config(&secrets_path) {
match injector(&sops_path, identities) { Ok(secrets_cfg) => match SecretResolver::from_config(secrets_cfg) {
Ok(env_content) => { Ok(resolver) => match resolver.resolve_all() {
for line in env_content.lines() { Ok(env_map) => {
match parse_env_line(line) { for (k, v) in &env_map {
match parse_env_line(&format!("{k}={v}")) {
Ok(Some((k, v))) => { Ok(Some((k, v))) => {
// SAFETY: `notstrap` is a single-threaded bootstrap binary; no other // SAFETY: `notstrap` is a single-threaded bootstrap
// threads exist. Subsequent Command::spawn calls (git, hooks) will // binary; no other threads exist. Keys are validated
// inherit these vars — keys are validated by parse_env_line before // by parse_env_line (null bytes and dangerous keys
// reaching this point (null bytes and dangerous keys are rejected). // are rejected).
unsafe { unsafe {
std::env::set_var(&k, &v); std::env::set_var(&k, &v);
} }
} }
Ok(None) => {} Ok(None) => {}
Err(e) => { Err(e) => {
report.add("decrypt secrets", StepStatus::Failed(e.to_string())); report.add("secrets", StepStatus::Failed(e.to_string()));
return Ok(report); return Ok(report);
} }
} }
} }
report.add("decrypt secrets", StepStatus::Ok); report.add("secrets", StepStatus::Ok);
} }
Err(e) => { Err(e) => {
report.add("decrypt secrets", StepStatus::Failed(e.to_string())); report.add("secrets", StepStatus::Failed(e.to_string()));
return Ok(report);
}
},
Err(e) => {
report.add("secrets", StepStatus::Failed(e.to_string()));
return Ok(report);
}
},
Err(e) => {
report.add("secrets", StepStatus::Failed(e.to_string()));
return Ok(report); return Ok(report);
} }
} }

View File

@@ -46,7 +46,7 @@ fn main() -> Result<()> {
dotfiles, dotfiles,
tailscale: None, // defer to [tailscale] section in config (if present) tailscale: None, // defer to [tailscale] section in config (if present)
check_prereqs: Some(Box::new(prereqs::check_prerequisites)), check_prereqs: Some(Box::new(prereqs::check_prerequisites)),
env_injector: None, secrets_config: None,
}; };
let report = run(opts)?; let report = run(opts)?;
report.print(); report.print();

View File

@@ -71,7 +71,7 @@ fn make_opts(env: &TestEnv, force: bool) -> BootstrapOptions {
dotfiles: Some(env.dotfiles.path().to_path_buf()), dotfiles: Some(env.dotfiles.path().to_path_buf()),
tailscale: Some(None), // skip Tailscale in tests tailscale: Some(None), // skip Tailscale in tests
check_prereqs: None, check_prereqs: None,
env_injector: None, secrets_config: None,
} }
} }
@@ -219,7 +219,7 @@ fn test_bootstrap_fails_fast_on_bad_key() {
dotfiles: Some(d.to_path_buf()), dotfiles: Some(d.to_path_buf()),
tailscale: Some(None), // skip Tailscale in tests tailscale: Some(None), // skip Tailscale in tests
check_prereqs: None, check_prereqs: None,
env_injector: None, secrets_config: None,
}; };
let report = run(opts).expect("run() should return Ok even when a step fails"); let report = run(opts).expect("run() should return Ok even when a step fails");