git.delta.rocks / jrsonnet / refs/commits / faec7071817b

difftreelog

source

crates/fleet-base/src/primops.rs7.9 KiBsourcehistory
1use std::collections::{BTreeMap, BTreeSet, HashMap};2use std::sync::OnceLock;34use anyhow::{Context, bail, ensure};5use fleet_shared::SecretData;6use itertools::Itertools;7use nix_eval::{NativeFn, Value, nix_go, nix_go_json};8use serde::Deserialize;9use tracing::{info, warn};1011use crate::fleetdata::{12	Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,13};14use crate::host::{Config, ConfigHost};15use crate::secret::{RegenerationReason, secret_needs_regeneration};16use anyhow::{Result, anyhow};1718#[derive(thiserror::Error, Debug)]19enum Error {}2021pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();2223#[derive(Deserialize)]24#[serde(rename_all = "camelCase")]25enum GeneratorKind {26	Impure,27	Pure,28}2930pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {31	info!("get pkgs");32	let pkgs = host_on.pkgs()?;33	let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);34	let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));35	Ok(pkgs.clone().attrs_update(generators)?)36}37pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {38	let host_on = config.local_host();39	get_pkgs_and_generators(&host_on, vec![])40}41pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result<Value> {42	ensure!(43		package.is_function(),44		"package should be a function to be called with callPackage"45	);46	// No need to use nixpkgs.buildUsing, as only nixpkgs-lib is used.47	let nixpkgs = &config.nixpkgs;48	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs));49	Ok(nix_go!(call_package(package)(Obj {})))50}5152pub fn get_default_generator_drv(config: &Config, generator: &Value) -> Result<Value> {53	let default_pkgs_and_generators = get_default_pkgs_and_generators(config)?;54	let default_generator_drv = call_package(config, &default_pkgs_and_generators, generator)55		.context("failed to initialize generator to get metadata")?;5657	Ok(default_generator_drv)58}5960pub async fn generate(61	config: &Config,62	expectations: Expectations,63	generator: &Value,64	default_generator_drv: &Value,65) -> Result<FleetSecretDistribution> {66	let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind);6768	match kind {69		GeneratorKind::Impure => {70			let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);7172			let host_on = if let Some(on) = &impure_on {73				config74					.host(on)75					.context("failed to get secret generation target host")?76			} else {77				config.local_host()78			};79			let pkgs_and_generators =80				get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())81					.context("failed to get pkgs for target host")?;82			let generator = call_package(config, &pkgs_and_generators, generator)83				.context("failed to evaluate generator for target host")?;8485			let generator = generator86				.build("out")87				.context("failed to build generator for target host")?;8889			let generator = host_on90				.remote_derivation(&generator)91				.await92				.context("failed to copy generator to target host")?;9394			// TODO: Remove destdir after everything is done95			let out_parent = host_on96				.mktemp_dir()97				.await98				.context("failed to prepare generator output dir on target host")?;99			let out = format!("{out_parent}/out");100			let mut generator_cmd = host_on.cmd(generator).await?;101			generator_cmd.env("out", &out);102			if impure_on.is_none() {103				let project_path: String = config104					.directory105					.clone()106					.into_os_string()107					.into_string()108					.map_err(|e| anyhow!("fleet project path is not utf-8: {e:?}"))?;109				generator_cmd.env("FLEET_PROJECT", project_path);110			};111			generator_cmd112				.run()113				.await114				.context("failed to run impure generator")?;115116			{117				let marker = host_on.read_file_text(format!("{out}/marker")).await?;118				ensure!(119					marker == "SUCCESS",120					"impure generator ended prematurely, secret generation failed"121				);122			}123124			let mut parts = BTreeMap::new();125			for part in host_on.read_dir(&out).await? {126				if part == "created_at" || part == "expires_at" || part == "marker" {127					continue;128				}129				let contents: SecretData = host_on130					.read_file_text(format!("{out}/{part}"))131					.await?132					.parse()133					.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;134				parts.insert(part.to_owned(), FleetSecretPart { raw: contents });135			}136137			let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;138			let expires_at = host_on139				.read_file_value(format!("{out}/expires_at"))140				.await141				.ok();142143			let new_data = FleetSecretData {144				created_at,145				expires_at,146				parts,147				generation_data: expectations.generation_data.clone(),148			};149150			let new_data = FleetSecretDistribution {151				secret: new_data,152				owners: expectations.owners.clone(),153				_deprecated_managed: true,154			};155156			if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {157				bail!("newly generated secret needs to be regenerated: {reason}")158			}159160			Ok(new_data)161		}162		GeneratorKind::Pure => {163			bail!("pure generators are disabled for now")164		}165	}166}167168pub fn init_primops() {169	info!("initializing primops");170	NativeFn::new(171		c"__fleetEnsureHostSecret",172		c"Ensure secret existence for a host, regenerating it in case of some mismatch",173		[c"host", c"secret", c"generator"],174		|es, [host, secret, generator]| {175			info!("get host");176			let host = host.to_string()?;177			info!("get secret");178			let secret = secret.to_string()?;179180			info!("get config");181			let config = PRIMOPS_DATA182				.get()183				.expect("primops data should be set on init");184185			let shared_def = config.secret_definition(&secret).context("failed to get shared secret definition")?;186187			let (shared, generator, expected_owners) = if generator.is_string() {188				assert_eq!(generator.to_string()?, "shared", "asserted by nixos type system");189				let Some(shared_def) = shared_def else {190					bail!("secret {secret} is defined on host {host} as shared, but there is no shared secret with same name defined at fleetConfiguration.secrets.{secret}.generator")191				};192				let expected_owners = shared_def.expected_owners()?;193194				ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");195196				(true, shared_def.generator()?, expected_owners)197			} else {198				if shared_def.is_some() {199					bail!("hosts can only have their own generators for non-shared secrets, either set host secret generator to \"shared\", or remove shared secret generator at fleetConfiguration.secrets.{secret}.generator")200				}201202				(false, generator.clone(), BTreeSet::from_iter([host.clone()]))203			};204205			let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;206			let expectations = Expectations {207				parts: nix_go_json!(default_generator_drv.parts),208				generation_data: nix_go_json!(default_generator_drv.generationData),209				owners: expected_owners,210			};211212			let reason: RegenerationReason = 'regenerate: {213				let Some(existing) = config214					.host_secret(&host, &secret) else {215					break 'regenerate RegenerationReason::Missing;216				};217				if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {218					break 'regenerate reason;219				}220221				let mut parts = expectations.parts.clone();222223				let mut out = HashMap::new();224				for (part_name, part) in &existing.secret.parts {225					let Some(definition) = parts.remove(part_name) else {226						warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");227						continue;228					};229					assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");230					out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));231				}232				assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");233234				return Ok(Value::new_attrs(out))235			};236237			todo!()238239240		},241	)242	.register();243}