difftreelog
refactor drop unnecessary async/await
in: trunk
15 files changed
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -30,9 +30,9 @@
async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {
info!("building");
- let host = config.host(&hostname).await?;
+ let host = config.host(&hostname)?;
// let action = Action::from(self.subcommand.clone());
- let nixos = host.nixos_config().await?;
+ let nixos = host.nixos_config()?;
let drv = nix_go!(nixos.system.build[{ build_attr }]);
let out_output = spawn_blocking(move || drv.build("out"))
.await
@@ -59,7 +59,7 @@
impl BuildSystems {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
- let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+ let hosts = opts.filter_skipped(config.list_hosts()?)?;
let set = LocalSet::new();
let build_attr = self.build_attr.clone();
for host in hosts {
@@ -95,20 +95,20 @@
impl Deploy {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
- let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+ let hosts = opts.filter_skipped(config.list_hosts()?)?;
let set = LocalSet::new();
for host in hosts.into_iter() {
let config = config.clone();
let span = info_span!("deploy", host = field::display(&host.name));
let hostname = host.name.clone();
let opts = opts.clone();
- if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
+ if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind")? {
host.set_deploy_kind(deploy_kind);
};
- if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
+ if let Some(destination) = opts.action_attr::<String>(&host, "dest")? {
host.set_session_destination(destination);
};
- if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+ if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store")? {
host.set_legacy_ssh_store(legacy);
};
@@ -153,7 +153,7 @@
self.action,
&host,
remote_path,
- match opts.action_attr(&host, "specialisation").await {
+ match opts.action_attr(&host, "specialisation") {
Ok(v) => v,
_ => {
error!("unreachable? failed to get specialization");
cmds/fleet/src/cmds/info.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -35,7 +35,7 @@
let mut data = Vec::new();
match self.cmd {
InfoCmd::ListHosts { ref tagged } => {
- 'host: for host in config.list_hosts().await? {
+ 'host: for host in config.list_hosts()? {
if !tagged.is_empty() {
let config = &config.config_field;
let host_name = &host.name;
@@ -59,7 +59,7 @@
"at leas one of --external or --internal must be set"
);
let mut out = <BTreeSet<String>>::new();
- let host = config.system_config(&host).await?;
+ let host = config.system_config(&host)?;
if external {
let data: Vec<String> = nix_go_json!(host.network.externalIps);
out.extend(data);
cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -75,7 +75,7 @@
impl RollbackSingle {
pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
- let host = config.host(&self.machine).await?;
+ let host = config.host(&self.machine)?;
match &self.action {
RollbackAction::ListTargets => {
let generations = list_all_generations(&host, config).await;
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,24 +1,16 @@
use std::{
- collections::{BTreeMap, BTreeSet, HashSet},
- io::{self, Read, Write, stdin, stdout},
+ collections::{BTreeSet, HashSet},
+ io::{Read, Write, stdin, stdout},
path::PathBuf,
};
-use anyhow::{Context, Result, anyhow, bail, ensure};
-use chrono::{DateTime, Utc};
+use anyhow::{Context, Result, bail, ensure};
use clap::Parser;
-use fleet_base::{
- fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},
- host::Config,
- opts::FleetOpts,
- secret::{Expectations, RegenerationReason, secret_needs_regeneration},
-};
+use fleet_base::{host::Config, opts::FleetOpts};
use fleet_shared::SecretData;
-use nix_eval::{NixType, Value, nix_go, nix_go_json};
-use serde::Deserialize;
-use tabled::{Table, Tabled};
-use tokio::{fs::read, task::spawn_blocking};
-use tracing::{Instrument, error, info, info_span, warn};
+use tabled::Tabled;
+use tokio::fs::read;
+use tracing::{info, info_span, warn};
#[derive(Parser)]
pub enum Secret {
@@ -145,13 +137,7 @@
}
*/
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum GeneratorKind {
- Impure,
- Pure,
-}
-
+/*
async fn generate_pure(
_config: &Config,
_display_name: &str,
@@ -315,6 +301,7 @@
}
}
}
+*/
/*
async fn generate_shared(
config: &Config,
@@ -421,8 +408,8 @@
todo!("part of fleet-pusher")
}
Secret::ForceKeys => {
- for host in config.list_hosts().await? {
- if opts.should_skip(&host).await? {
+ for host in config.list_hosts()? {
+ if opts.should_skip(&host)? {
continue;
}
config.key(&host.name).await?;
@@ -467,7 +454,7 @@
let Some(identity_holder) = identity_holder else {
bail!("no available holder found");
};
- let host = config.host(identity_holder).await?;
+ let host = config.host(identity_holder)?;
host.decrypt(part.raw.clone()).await?
} else {
part.raw.data.clone()
@@ -619,7 +606,7 @@
}
Secret::List {} => {
let _span = info_span!("loading secrets").entered();
- let configured = config.list_configured_shared().await?;
+ let configured = config.list_configured_shared()?;
#[derive(Tabled)]
struct SecretDisplay {
#[tabled(rename = "Name")]
@@ -662,7 +649,7 @@
.host_secret(&machine, &name)
.context("secret not found")?;
if let Some(data) = secret.secret.parts.get(&part) {
- let host = config.host(&machine).await?;
+ let host = config.host(&machine)?;
let secret = host.decrypt(data.raw.clone()).await?;
String::from_utf8(secret).context("secret is not utf8")?
} else if add {
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -216,13 +216,10 @@
.map(|a| extra_args::parse_os(&a))
.transpose()?
.unwrap_or_default();
- let config = opts
- .fleet_opts
- .build(
- nix_args,
- matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
- )
- .await?;
+ let config = opts.fleet_opts.build(
+ nix_args,
+ matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
+ )?;
match run_command(&config, opts.fleet_opts, opts.command).await {
Ok(()) => {
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth1use std::{2 collections::{3 BTreeMap, BTreeSet,4 btree_map::{self, Entry},5 },6 io::{self, Cursor},7 ops::Deref,8};910use age::Recipient;11use chrono::{DateTime, Utc};12use fleet_shared::SecretData;13use rand::{14 distr::{Alphanumeric, SampleString as _},15 rng,16};17use serde::{18 Deserialize, Serialize,19 de::{self, Error},20};21use serde_json::Value;22use tracing::info;2324#[derive(Serialize, Deserialize, Default)]25#[serde(rename_all = "camelCase")]26pub struct HostData {27 #[serde(default)]28 #[serde(skip_serializing_if = "String::is_empty")]29 pub encryption_key: String,30}3132const VERSION: &str = "0.1.0";33pub struct FleetDataVersion;34impl Serialize for FleetDataVersion {35 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>36 where37 S: serde::Serializer,38 {39 VERSION.serialize(serializer)40 }41}42impl<'de> Deserialize<'de> for FleetDataVersion {43 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>44 where45 D: serde::Deserializer<'de>,46 {47 let version = String::deserialize(deserializer)?;48 if version != VERSION {49 return Err(D::Error::custom(format!(50 "fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"51 )));52 }53 Ok(Self)54 }55}5657fn generate_gc_prefix() -> String {58 let id = Alphanumeric.sample_string(&mut rng(), 8);59 format!("fleet-gc-{id}")60}6162#[derive(Serialize, Deserialize)]63#[serde(rename_all = "camelCase")]64pub struct ManagerKey {65 pub name: String,66 pub key: String,67}6869#[derive(Serialize, Deserialize)]70#[serde(rename_all = "camelCase")]71pub struct FleetData {72 pub version: FleetDataVersion,73 #[serde(default = "generate_gc_prefix")]74 pub gc_root_prefix: String,7576 #[serde(default, skip_serializing_if = "Vec::is_empty")]77 pub manager_keys: Vec<ManagerKey>,7879 #[serde(default)]80 pub hosts: BTreeMap<String, HostData>,8182 #[serde(default, alias = "shared_secrets")]83 pub secrets: FleetSecrets,8485 // extra_name => anything86 #[serde(default)]87 #[serde(skip_serializing_if = "BTreeMap::is_empty")]88 pub extra: BTreeMap<String, Value>,8990 #[serde(default)]91 #[serde(skip_serializing_if = "BTreeMap::is_empty")]92 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,93}94impl FleetData {95 pub fn from_str(s: &str) -> anyhow::Result<Self> {96 let mut data: Self = nixlike::parse_str(s)?;97 if !data.host_secrets.is_empty() {98 info!("migrating host secrets into shared secrets structure");99 data.secrets100 .merge_from_hosts(std::mem::take(&mut data.host_secrets));101 }102 Ok(data)103 }104}105106/// Returns None if recipients.is_empty()107pub fn encrypt_secret_data<'r>(108 recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,109 data: Vec<u8>,110) -> Option<SecretData> {111 let mut encrypted = vec![];112 let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))113 .ok()?114 .wrap_output(&mut encrypted)115 .expect("in memory write");116 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");117 encryptor.finish().expect("in memory flush");118 Some(SecretData {119 data: encrypted,120 encrypted: true,121 })122}123124#[derive(Serialize, Deserialize, Clone, Debug)]125pub struct FleetSecretPart {126 pub raw: SecretData,127}128129#[derive(Serialize, Deserialize, Clone, Debug)]130#[serde(rename_all = "camelCase")]131#[must_use]132pub struct FleetSecretData {133 #[serde(default = "Utc::now")]134 pub created_at: DateTime<Utc>,135 #[serde(default)]136 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]137 pub expires_at: Option<DateTime<Utc>>,138139 #[serde(flatten)]140 pub parts: BTreeMap<String, FleetSecretPart>,141142 #[serde(default)]143 #[serde(skip_serializing_if = "Value::is_null")]144 pub generation_data: Value,145}146147#[derive(Serialize, Deserialize, Clone, Debug)]148#[serde(rename_all = "camelCase")]149#[must_use]150pub struct FleetSecretDistribution {151 #[serde(default)]152 pub owners: BTreeSet<String>,153 #[serde(flatten)]154 pub secret: FleetSecretData,155156 #[serde(default, skip_serializing, alias = "managed")]157 pub _deprecated_managed: bool,158}159160#[derive(Clone)]161#[must_use]162pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);163164impl Deref for FleetSecretDistributions {165 type Target = [FleetSecretDistribution];166167 fn deref(&self) -> &Self::Target {168 self.0.as_slice()169 }170}171172impl FleetSecretDistributions {173 pub fn owners(&self) -> impl Iterator<Item = &String> {174 self.0.iter().flat_map(|v| v.owners.iter())175 }176 #[allow(177 clippy::len_without_is_empty,178 reason = "should not be empty for a long time"179 )]180 pub fn len(&self) -> usize {181 self.0.len()182 }183184 pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {185 self.0.iter().find(|d| d.owners.contains(owner))186 }187 fn entry(&mut self, owner: String) -> DistEntry<'_> {188 let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {189 return DistEntry::Vacant(VacantDistEntry {190 distributions: self,191 owner,192 });193 };194 DistEntry::Occupied(OccupiedDistEntry {195 distributions: self,196 idx,197 owner,198 })199 }200 fn extend(&mut self, dist: FleetSecretDistribution) {201 for owner in &dist.owners {202 self.entry(owner.to_owned()).remove();203 }204 self.0.push(dist);205 }206 pub fn contains(&self, owner: &str) -> bool {207 self.0.iter().any(|d| d.owners.contains(owner))208 }209}210211struct OccupiedDistEntry<'d> {212 distributions: &'d mut FleetSecretDistributions,213 idx: usize,214 owner: String,215}216impl<'d> OccupiedDistEntry<'d> {217 fn remove(self) -> VacantDistEntry<'d> {218 let dist = &mut self.distributions.0[self.idx];219 assert!(220 dist.owners.remove(&self.owner),221 "entry exists, as we have its reference"222 );223 if dist.owners.is_empty() {224 self.distributions.0.remove(self.idx);225 }226 VacantDistEntry {227 distributions: self.distributions,228 owner: self.owner,229 }230 }231 fn set(self, secret: FleetSecretData) -> Self {232 self.remove().set(secret)233 }234}235struct VacantDistEntry<'d> {236 distributions: &'d mut FleetSecretDistributions,237 owner: String,238}239impl<'d> VacantDistEntry<'d> {240 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {241 let Self {242 distributions,243 owner,244 } = self;245 let idx = distributions.0.len();246 distributions.0.push(FleetSecretDistribution {247 owners: BTreeSet::from_iter([owner.clone()]),248 secret,249250 _deprecated_managed: true,251 });252 OccupiedDistEntry {253 distributions,254 owner,255 idx,256 }257 }258}259260enum DistEntry<'d> {261 Vacant(VacantDistEntry<'d>),262 Occupied(OccupiedDistEntry<'d>),263}264impl DistEntry<'_> {265 fn remove(self) -> Self {266 match self {267 DistEntry::Vacant(_) => self,268 DistEntry::Occupied(o) => Self::Vacant(o.remove()),269 }270 }271 fn set(self, secret: FleetSecretData) -> Self {272 Self::Occupied(match self {273 DistEntry::Vacant(e) => e.set(secret),274 DistEntry::Occupied(e) => e.set(secret),275 })276 }277}278279impl Serialize for FleetSecretDistributions {280 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>281 where282 S: serde::Serializer,283 {284 let mut found_hosts = BTreeSet::new();285 for ele in self.0.iter() {286 if ele.owners.is_empty() {287 panic!("consistency: secret distribution has no defined owners");288 }289 for ele in ele.owners.iter() {290 if !found_hosts.insert(ele) {291 panic!(292 "consistency: secret distribution contains duplicate entry for the same host",293 );294 }295 }296 }297 match self.0.len() {298 0 => panic!("consistency: empty distributions"),299 1 => self.0[0].serialize(serializer),300 _ => self.0.serialize(serializer),301 }302 }303}304impl<'de> Deserialize<'de> for FleetSecretDistributions {305 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>306 where307 D: serde::Deserializer<'de>,308 {309 #[derive(Deserialize)]310 #[serde(untagged)]311 enum Distributions {312 One(FleetSecretDistribution),313 Many(Vec<FleetSecretDistribution>),314 }315 let d = Distributions::deserialize(deserializer)?;316 let ds = match d {317 Distributions::One(d) => vec![d],318 Distributions::Many(ds) => ds,319 };320 if ds.is_empty() {321 return Err(de::Error::custom("consistency: empty distributions"));322 }323 let mut found_hosts = BTreeSet::new();324 for ele in ds.iter() {325 if ele.owners.is_empty() {326 return Err(de::Error::custom(327 "consistency: secret distribution has no defined owners",328 ));329 }330 for ele in ele.owners.iter() {331 if !found_hosts.insert(ele) {332 return Err(de::Error::custom(333 "consistency: secret distribution contains duplicate entry for the same host",334 ));335 }336 }337 }338 Ok(Self(ds))339 }340}341342#[derive(Serialize, Deserialize, Default)]343pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);344345impl FleetSecrets {346 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {347 self.0.keys()348 }349350 pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {351 self.0352 .iter()353 .filter(|(_, d)| d.contains(owner))354 .map(|(n, _)| n)355 }356357 pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {358 let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {359 return false;360 };361 let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {362 return false;363 };364365 dist.remove();366367 if dists.get().0.is_empty() {368 dists.remove();369 };370371 true372 }373 pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {374 let e = self375 .0376 .entry(secret.to_owned())377 .or_insert_with(|| FleetSecretDistributions(Default::default()));378 e.entry(owner.to_owned()).set(data);379 }380 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {381 match self.0.entry(secret) {382 Entry::Vacant(e) => {383 e.insert(FleetSecretDistributions(vec![data]));384 }385 Entry::Occupied(mut e) => {386 let dists = e.get_mut();387 dists.extend(data)388 }389 }390 }391 pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {392 let secret = self.0.get(secret)?;393 secret.get(owner)394 }395 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {396 self.0.get(secret)397 }398399 pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {400 let Some(secret) = self.0.get(secret) else {401 return false;402 };403 secret.contains(owner)404 }405 pub fn contains(&self, secret: &str) -> bool {406 self.0.contains_key(secret)407 }408 pub fn remove(&mut self, secret: &str) {409 self.0.remove(secret);410 }411412 fn merge_from_hosts(413 &mut self,414 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,415 ) {416 for (host, host_secrets) in host_secrets {417 for (secret_name, mut secret_data) in host_secrets {418 secret_data.owners.insert(host.clone());419 self.set_data(secret_name, secret_data);420 }421 }422 }423}1use std::{2 collections::{3 BTreeMap, BTreeSet,4 btree_map::{self, Entry},5 },6 io::{self, Cursor},7 ops::Deref,8};910use age::Recipient;11use chrono::{DateTime, Utc};12use fleet_shared::SecretData;13use rand::{14 distr::{Alphanumeric, SampleString as _},15 rng,16};17use serde::{18 Deserialize, Serialize,19 de::{self, Error},20};21use serde_json::Value;22use tracing::info;2324#[derive(Serialize, Deserialize, Default)]25#[serde(rename_all = "camelCase")]26pub struct HostData {27 #[serde(default)]28 #[serde(skip_serializing_if = "String::is_empty")]29 pub encryption_key: String,30}3132const VERSION: &str = "0.1.0";33pub struct FleetDataVersion;34impl Serialize for FleetDataVersion {35 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>36 where37 S: serde::Serializer,38 {39 VERSION.serialize(serializer)40 }41}42impl<'de> Deserialize<'de> for FleetDataVersion {43 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>44 where45 D: serde::Deserializer<'de>,46 {47 let version = String::deserialize(deserializer)?;48 if version != VERSION {49 return Err(D::Error::custom(format!(50 "fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"51 )));52 }53 Ok(Self)54 }55}5657fn generate_gc_prefix() -> String {58 let id = Alphanumeric.sample_string(&mut rng(), 8);59 format!("fleet-gc-{id}")60}6162#[derive(Serialize, Deserialize)]63#[serde(rename_all = "camelCase")]64pub struct ManagerKey {65 pub name: String,66 pub key: String,67}6869#[derive(Serialize, Deserialize)]70#[serde(rename_all = "camelCase")]71pub struct FleetData {72 pub version: FleetDataVersion,73 #[serde(default = "generate_gc_prefix")]74 pub gc_root_prefix: String,7576 #[serde(default, skip_serializing_if = "Vec::is_empty")]77 pub manager_keys: Vec<ManagerKey>,7879 #[serde(default)]80 pub hosts: BTreeMap<String, HostData>,8182 #[serde(default, alias = "shared_secrets")]83 pub secrets: FleetSecrets,8485 // extra_name => anything86 #[serde(default)]87 #[serde(skip_serializing_if = "BTreeMap::is_empty")]88 pub extra: BTreeMap<String, Value>,8990 #[serde(default)]91 #[serde(skip_serializing_if = "BTreeMap::is_empty")]92 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,93}94impl FleetData {95 pub fn from_str(s: &str) -> anyhow::Result<Self> {96 let mut data: Self = nixlike::parse_str(s)?;97 if !data.host_secrets.is_empty() {98 info!("migrating host secrets into shared secrets structure");99 data.secrets100 .merge_from_hosts(std::mem::take(&mut data.host_secrets));101 }102 Ok(data)103 }104}105106/// Returns None if recipients.is_empty()107pub fn encrypt_secret_data<'r>(108 recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,109 data: Vec<u8>,110) -> Option<SecretData> {111 let mut encrypted = vec![];112 let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))113 .ok()?114 .wrap_output(&mut encrypted)115 .expect("in memory write");116 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");117 encryptor.finish().expect("in memory flush");118 Some(SecretData {119 data: encrypted,120 encrypted: true,121 })122}123124#[derive(Serialize, Deserialize, Clone, Debug)]125pub struct FleetSecretPart {126 pub raw: SecretData,127}128129#[derive(Serialize, Deserialize, Clone, Debug)]130#[serde(rename_all = "camelCase")]131#[must_use]132pub struct FleetSecretData {133 #[serde(default = "Utc::now")]134 pub created_at: DateTime<Utc>,135 #[serde(default)]136 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]137 pub expires_at: Option<DateTime<Utc>>,138139 #[serde(flatten)]140 pub parts: BTreeMap<String, FleetSecretPart>,141142 #[serde(default)]143 #[serde(skip_serializing_if = "Value::is_null")]144 pub generation_data: Value,145}146147#[derive(Serialize, Deserialize, Clone, Debug)]148#[serde(rename_all = "camelCase")]149#[must_use]150pub struct FleetSecretDistribution {151 #[serde(default)]152 pub owners: BTreeSet<String>,153 #[serde(flatten)]154 pub secret: FleetSecretData,155156 #[serde(default, skip_serializing, alias = "managed")]157 pub _deprecated_managed: bool,158}159160#[derive(Clone)]161#[must_use]162pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);163164impl Deref for FleetSecretDistributions {165 type Target = [FleetSecretDistribution];166167 fn deref(&self) -> &Self::Target {168 self.0.as_slice()169 }170}171172impl FleetSecretDistributions {173 pub fn owners(&self) -> impl Iterator<Item = &String> {174 self.0.iter().flat_map(|v| v.owners.iter())175 }176 #[allow(177 clippy::len_without_is_empty,178 reason = "should not be empty for a long time"179 )]180 pub fn len(&self) -> usize {181 self.0.len()182 }183184 pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {185 self.0.iter().find(|d| d.owners.contains(owner))186 }187 fn entry(&mut self, owner: String) -> DistEntry<'_> {188 let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {189 return DistEntry::Vacant(VacantDistEntry {190 distributions: self,191 owner,192 });193 };194 DistEntry::Occupied(OccupiedDistEntry {195 distributions: self,196 idx,197 owner,198 })199 }200 fn extend(&mut self, dist: FleetSecretDistribution) {201 for owner in &dist.owners {202 self.entry(owner.to_owned()).remove();203 }204 self.0.push(dist);205 }206 pub fn contains(&self, owner: &str) -> bool {207 self.0.iter().any(|d| d.owners.contains(owner))208 }209}210211struct OccupiedDistEntry<'d> {212 distributions: &'d mut FleetSecretDistributions,213 idx: usize,214 owner: String,215}216impl<'d> OccupiedDistEntry<'d> {217 fn remove(self) -> VacantDistEntry<'d> {218 let dist = &mut self.distributions.0[self.idx];219 assert!(220 dist.owners.remove(&self.owner),221 "entry exists, as we have its reference"222 );223 if dist.owners.is_empty() {224 self.distributions.0.remove(self.idx);225 }226 VacantDistEntry {227 distributions: self.distributions,228 owner: self.owner,229 }230 }231 fn set(self, secret: FleetSecretData) -> Self {232 self.remove().set(secret)233 }234}235struct VacantDistEntry<'d> {236 distributions: &'d mut FleetSecretDistributions,237 owner: String,238}239impl<'d> VacantDistEntry<'d> {240 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {241 let Self {242 distributions,243 owner,244 } = self;245 let idx = distributions.0.len();246 distributions.0.push(FleetSecretDistribution {247 owners: BTreeSet::from_iter([owner.clone()]),248 secret,249250 _deprecated_managed: true,251 });252 OccupiedDistEntry {253 distributions,254 owner,255 idx,256 }257 }258}259260enum DistEntry<'d> {261 Vacant(VacantDistEntry<'d>),262 Occupied(OccupiedDistEntry<'d>),263}264impl DistEntry<'_> {265 fn remove(self) -> Self {266 match self {267 DistEntry::Vacant(_) => self,268 DistEntry::Occupied(o) => Self::Vacant(o.remove()),269 }270 }271 fn set(self, secret: FleetSecretData) -> Self {272 Self::Occupied(match self {273 DistEntry::Vacant(e) => e.set(secret),274 DistEntry::Occupied(e) => e.set(secret),275 })276 }277}278279impl Serialize for FleetSecretDistributions {280 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>281 where282 S: serde::Serializer,283 {284 let mut found_hosts = BTreeSet::new();285 for ele in self.0.iter() {286 if ele.owners.is_empty() {287 panic!("consistency: secret distribution has no defined owners");288 }289 for ele in ele.owners.iter() {290 if !found_hosts.insert(ele) {291 panic!(292 "consistency: secret distribution contains duplicate entry for the same host",293 );294 }295 }296 }297 match self.0.len() {298 0 => panic!("consistency: empty distributions"),299 1 => self.0[0].serialize(serializer),300 _ => self.0.serialize(serializer),301 }302 }303}304impl<'de> Deserialize<'de> for FleetSecretDistributions {305 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>306 where307 D: serde::Deserializer<'de>,308 {309 #[derive(Deserialize)]310 #[serde(untagged)]311 enum Distributions {312 One(FleetSecretDistribution),313 Many(Vec<FleetSecretDistribution>),314 }315 let d = Distributions::deserialize(deserializer)?;316 let ds = match d {317 Distributions::One(d) => vec![d],318 Distributions::Many(ds) => ds,319 };320 if ds.is_empty() {321 return Err(de::Error::custom("consistency: empty distributions"));322 }323 let mut found_hosts = BTreeSet::new();324 for ele in ds.iter() {325 if ele.owners.is_empty() {326 return Err(de::Error::custom(327 "consistency: secret distribution has no defined owners",328 ));329 }330 for ele in ele.owners.iter() {331 if !found_hosts.insert(ele) {332 return Err(de::Error::custom(333 "consistency: secret distribution contains duplicate entry for the same host",334 ));335 }336 }337 }338 Ok(Self(ds))339 }340}341342#[derive(Serialize, Deserialize, Default)]343pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);344345impl FleetSecrets {346 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {347 self.0.keys()348 }349350 pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {351 self.0352 .iter()353 .filter(|(_, d)| d.contains(owner))354 .map(|(n, _)| n)355 }356357 pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {358 let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {359 return false;360 };361 let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {362 return false;363 };364365 dist.remove();366367 if dists.get().0.is_empty() {368 dists.remove();369 };370371 true372 }373 pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {374 let e = self375 .0376 .entry(secret.to_owned())377 .or_insert_with(|| FleetSecretDistributions(Default::default()));378 e.entry(owner.to_owned()).set(data);379 }380 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {381 match self.0.entry(secret) {382 Entry::Vacant(e) => {383 e.insert(FleetSecretDistributions(vec![data]));384 }385 Entry::Occupied(mut e) => {386 let dists = e.get_mut();387 dists.extend(data)388 }389 }390 }391 pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {392 let secret = self.0.get(secret)?;393 secret.get(owner)394 }395 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {396 self.0.get(secret)397 }398399 pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {400 let Some(secret) = self.0.get(secret) else {401 return false;402 };403 secret.contains(owner)404 }405 pub fn contains(&self, secret: &str) -> bool {406 self.0.contains_key(secret)407 }408 pub fn remove(&mut self, secret: &str) {409 self.0.remove(secret);410 }411412 fn merge_from_hosts(413 &mut self,414 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,415 ) {416 for (host, host_secrets) in host_secrets {417 for (secret_name, mut secret_data) in host_secrets {418 secret_data.owners.insert(host.clone());419 self.set_data(secret_name, secret_data);420 }421 }422 }423}424425#[derive(Debug)]426pub struct Expectations {427 pub owners: BTreeSet<String>,428 pub generation_data: serde_json::Value,429 pub parts: BTreeMap<String, GeneratorPart>,430}431#[derive(Deserialize, Debug, Clone)]432pub struct GeneratorPart {433 pub encrypted: bool,434}crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -471,10 +471,13 @@
cmd.run().await
}
}
+
+struct HostSecretDefinition(Value);
+
impl ConfigHost {
// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,
// assuming getting tags always returns the same value.
- pub async fn tags(&self) -> Result<Vec<String>> {
+ pub fn tags(&self) -> Result<Vec<String>> {
if let Some(v) = self.groups.get() {
return Ok(v.clone());
}
@@ -487,7 +490,7 @@
Ok(tags)
}
- pub async fn nixos_config(&self) -> Result<Value> {
+ pub fn nixos_config(&self) -> Result<Value> {
if let Some(v) = self.nixos_config.get() {
return Ok(v.clone());
}
@@ -495,7 +498,7 @@
bail!("local host has no nixos_config");
};
let nixos_config = nix_go!(host_config.nixos.config);
- assert_warn("nixos config evaluation", &nixos_config).await?;
+ assert_warn("nixos config evaluation", &nixos_config)?;
let _ = self.nixos_config.set(nixos_config.clone());
@@ -522,7 +525,7 @@
}
/// Packages for this host, resolved with nixpkgs overlays
- pub async fn pkgs(&self) -> Result<Value> {
+ pub fn pkgs(&self) -> Result<Value> {
if let Some(value) = &self.pkgs_override {
return Ok(value.clone());
}
@@ -534,17 +537,29 @@
}
}
+pub struct SharedSecretDefinition(Value);
+impl SharedSecretDefinition {
+ pub fn expected_owners(&self) -> Result<BTreeSet<String>> {
+ let secret = &self.0;
+ Ok(nix_go_json!(secret.expectedOwners))
+ }
+ pub fn generator(&self) -> Result<Value> {
+ let secret = &self.0;
+ Ok(nix_go!(secret.generator))
+ }
+}
+
impl Config {
- pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {
+ pub fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {
let config = &self.config_field;
let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);
Ok(tagged)
}
- pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {
+ pub fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {
let mut out = BTreeSet::new();
for owner in owners {
if let Some(tag) = owner.strip_prefix('@') {
- let hosts = self.tagged_hostnames(tag).await?;
+ let hosts = self.tagged_hostnames(tag)?;
out.extend(hosts);
} else {
out.insert(owner);
@@ -574,7 +589,7 @@
}
}
- pub async fn host(&self, name: &str) -> Result<ConfigHost> {
+ pub fn host(&self, name: &str) -> Result<ConfigHost> {
let config = &self.config_field;
let host_config = nix_go!(config.hosts[{ name }]);
@@ -595,23 +610,23 @@
legacy_ssh_store: OnceCell::new(),
})
}
- pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
+ pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
let config = &self.config_field;
let names = nix_go!(config.hosts).list_fields()?;
let mut out = vec![];
for name in names {
- out.push(self.host(&name).await?);
+ out.push(self.host(&name)?);
}
Ok(out)
}
// TODO: Replace usages with .host().nixos_config
- pub async fn system_config(&self, host: &str) -> Result<Value> {
+ pub fn system_config(&self, host: &str) -> Result<Value> {
let fleet_field = &self.config_field;
Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))
}
/// Shared secrets configured in fleet.nix or in flake
- pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
+ pub fn list_configured_shared(&self) -> Result<Vec<String>> {
let config_field = &self.config_field;
nix_go!(config_field.sharedSecrets).list_fields()
}
@@ -659,6 +674,17 @@
data.secrets.get(secret).cloned()
}
+ pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {
+ let config = &self.config_field;
+ let shared_secrets = nix_go!(config.secrets);
+ if !shared_secrets.has_field(secret)? {
+ return Ok(None);
+ }
+ Ok(Some(SharedSecretDefinition(nix_go!(
+ shared_secrets[secret]
+ ))))
+ }
+
// TODO: Should this be something modifiable from other processes?
// E.g terraform provider might want to update FleetData (e.g secrets),
// and current implementation assumes only one process holds current fleet.nix
crates/fleet-base/src/keys.rsdiffbeforeafterboth--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -12,10 +12,10 @@
pub fn cached_key(&self, host: &str) -> Option<String> {
let data = self.data();
let key = data.hosts.get(host).map(|h| &h.encryption_key);
- if let Some(key) = key {
- if key.is_empty() {
- return None;
- }
+ if let Some(key) = key
+ && key.is_empty()
+ {
+ return None;
}
key.cloned()
}
@@ -30,7 +30,7 @@
Ok(key)
} else {
warn!("Loading key for {}", host);
- let host = self.host(host).await?;
+ let host = self.host(host)?;
let mut cmd = host.cmd("cat").await?;
cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub");
let key = cmd.run_string().await?;
@@ -47,7 +47,7 @@
}
pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {
- let hosts = self.expand_owner_set(hosts).await?;
+ let hosts = self.expand_owner_set(hosts)?;
futures::stream::iter(hosts.iter())
.then(|m| self.recipient(m.as_ref()))
.try_collect::<Vec<_>>()
@@ -57,12 +57,7 @@
#[allow(dead_code)]
pub async fn orphaned_data(&self) -> Result<Vec<String>> {
let mut out = Vec::new();
- let host_names = self
- .list_hosts()
- .await?
- .into_iter()
- .map(|h| h.name)
- .collect_vec();
+ let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
for hostname in self
.data()
.hosts
crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -104,20 +104,20 @@
}
impl FleetOpts {
- pub async fn filter_skipped(
+ pub fn filter_skipped(
&self,
hosts: impl IntoIterator<Item = ConfigHost>,
) -> Result<Vec<ConfigHost>> {
let mut out = Vec::new();
for host in hosts {
- if self.should_skip(&host).await? {
+ if self.should_skip(&host)? {
continue;
}
out.push(host);
}
Ok(out)
}
- pub async fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
+ pub fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
if self.skip.iter().any(|h| h as &str == host.name) {
return Ok(true);
}
@@ -137,7 +137,7 @@
}
}
if have_group_matches {
- let host_tags = host.tags().await?;
+ let host_tags = host.tags()?;
for item in self.only.iter() {
match item {
HostItem::Tag { name, .. } if host_tags.contains(name) => {
@@ -149,15 +149,15 @@
}
Ok(true)
}
- pub async fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
+ pub fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
where
T::Err: Sync,
anyhow::Error: From<T::Err>,
{
- let str = self.action_attr_str(host, attr).await?;
+ let str = self.action_attr_str(host, attr)?;
Ok(str.map(|v| T::from_str(&v)).transpose()?)
}
- pub async fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
+ pub fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
if self.only.is_empty() {
return Ok(None);
}
@@ -176,7 +176,7 @@
}
}
if have_group_matches {
- let host_tags = host.tags().await?;
+ let host_tags = host.tags()?;
for item in self.only.iter() {
match item {
HostItem::Tag { name, attrs }
@@ -195,7 +195,7 @@
}
// TODO: Config should be detached from opts.
- pub async fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
+ pub fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
let cwd = current_dir()?;
let mut directory = cwd.clone();
let mut fleet_data_path = directory.join("fleet.nix");
@@ -248,7 +248,6 @@
if assert {
assert_warn("fleet config evaluation", &config_field)
- .await
.context("failed to verify assertions")?;
}
crates/fleet-base/src/primops.rsdiffbeforeafterboth--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,38 +1,168 @@
-use std::cell::OnceCell;
-use std::collections::{BTreeMap, HashMap};
-use std::sync::{Arc, Mutex, OnceLock};
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::sync::OnceLock;
-use anyhow::{Context, bail};
+use anyhow::{Context, bail, ensure};
+use fleet_shared::SecretData;
use itertools::Itertools;
use nix_eval::{NativeFn, Value, nix_go, nix_go_json};
use serde::Deserialize;
use tracing::{info, warn};
-use crate::fleetdata::{FleetData, FleetSecrets};
-use crate::host::Config;
+use crate::fleetdata::{
+ Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
+};
+use crate::host::{Config, ConfigHost};
+use crate::secret::{RegenerationReason, secret_needs_regeneration};
+use anyhow::{Result, anyhow};
#[derive(thiserror::Error, Debug)]
enum Error {}
-struct Parts {
- encrypted: Vec<String>,
- public: Vec<String>,
+pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum GeneratorKind {
+ Impure,
+ Pure,
}
-trait SecretsBackend {
- fn has_shared(&self, name: &str);
- fn has_host(&self, host: &str, name: &str);
- fn shared_parts(&self, name: &str) -> Parts;
- fn host_parts(&self, host: &str, name: &str) -> Parts;
+pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {
+ info!("get pkgs");
+ let pkgs = host_on.pkgs()?;
+ let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
+ let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
+ Ok(pkgs.clone().attrs_update(generators)?)
+}
+pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {
+ let host_on = config.local_host();
+ get_pkgs_and_generators(&host_on, vec![])
}
+pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result<Value> {
+ ensure!(
+ package.is_function(),
+ "package should be a function to be called with callPackage"
+ );
+ // No need to use nixpkgs.buildUsing, as only nixpkgs-lib is used.
+ let nixpkgs = &config.nixpkgs;
+ let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs));
+ Ok(nix_go!(call_package(package)(Obj {})))
+}
+
+pub fn get_default_generator_drv(config: &Config, generator: &Value) -> Result<Value> {
+ let default_pkgs_and_generators = get_default_pkgs_and_generators(config)?;
+ let default_generator_drv = call_package(config, &default_pkgs_and_generators, generator)
+ .context("failed to initialize generator to get metadata")?;
+
+ Ok(default_generator_drv)
+}
+
+pub async fn generate(
+ config: &Config,
+ expectations: Expectations,
+ generator: &Value,
+ default_generator_drv: &Value,
+) -> Result<FleetSecretDistribution> {
+ let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind);
+
+ match kind {
+ GeneratorKind::Impure => {
+ let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);
-struct FsSecretsBackend {}
+ let host_on = if let Some(on) = &impure_on {
+ config
+ .host(on)
+ .context("failed to get secret generation target host")?
+ } else {
+ config.local_host()
+ };
+ let pkgs_and_generators =
+ get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())
+ .context("failed to get pkgs for target host")?;
+ let generator = call_package(config, &pkgs_and_generators, generator)
+ .context("failed to evaluate generator for target host")?;
-pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+ let generator = generator
+ .build("out")
+ .context("failed to build generator for target host")?;
-#[derive(Deserialize, Debug)]
-struct GeneratorPart {
- encrypted: bool,
+ let generator = host_on
+ .remote_derivation(&generator)
+ .await
+ .context("failed to copy generator to target host")?;
+
+ // TODO: Remove destdir after everything is done
+ let out_parent = host_on
+ .mktemp_dir()
+ .await
+ .context("failed to prepare generator output dir on target host")?;
+ let out = format!("{out_parent}/out");
+ let mut generator_cmd = host_on.cmd(generator).await?;
+ generator_cmd.env("out", &out);
+ if impure_on.is_none() {
+ let project_path: String = config
+ .directory
+ .clone()
+ .into_os_string()
+ .into_string()
+ .map_err(|e| anyhow!("fleet project path is not utf-8: {e:?}"))?;
+ generator_cmd.env("FLEET_PROJECT", project_path);
+ };
+ generator_cmd
+ .run()
+ .await
+ .context("failed to run impure generator")?;
+
+ {
+ let marker = host_on.read_file_text(format!("{out}/marker")).await?;
+ ensure!(
+ marker == "SUCCESS",
+ "impure generator ended prematurely, secret generation failed"
+ );
+ }
+
+ let mut parts = BTreeMap::new();
+ for part in host_on.read_dir(&out).await? {
+ if part == "created_at" || part == "expires_at" || part == "marker" {
+ continue;
+ }
+ let contents: SecretData = host_on
+ .read_file_text(format!("{out}/{part}"))
+ .await?
+ .parse()
+ .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;
+ parts.insert(part.to_owned(), FleetSecretPart { raw: contents });
+ }
+
+ let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;
+ let expires_at = host_on
+ .read_file_value(format!("{out}/expires_at"))
+ .await
+ .ok();
+
+ let new_data = FleetSecretData {
+ created_at,
+ expires_at,
+ parts,
+ generation_data: expectations.generation_data.clone(),
+ };
+
+ let new_data = FleetSecretDistribution {
+ secret: new_data,
+ owners: expectations.owners.clone(),
+ _deprecated_managed: true,
+ };
+
+ if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {
+ bail!("newly generated secret needs to be regenerated: {reason}")
+ }
+
+ Ok(new_data)
+ }
+ GeneratorKind::Pure => {
+ bail!("pure generators are disabled for now")
+ }
+ }
}
pub fn init_primops() {
@@ -52,52 +182,61 @@
.get()
.expect("primops data should be set on init");
- info!("get pkgs");
- let nixpkgs = &config.nixpkgs;
- let default_pkgs = &config.default_pkgs;
- let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
- let generators = nix_go!(default_mk_secret_generators(Obj {
- recipients: <Vec<String>>::new(),
- }));
- let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;
+ let shared_def = config.secret_definition(&secret).context("failed to get shared secret definition")?;
- info!("call package");
- let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
- let default_generator = call_package
- .call(generator.clone())
- .context("calling callPackage with generator")?
- .call(Value::new_attrs(HashMap::new()))
- .context("providing extra callPackage args")?;
+ let (shared, generator, expected_owners) = if generator.is_string() {
+ assert_eq!(generator.to_string()?, "shared", "asserted by nixos type system");
+ let Some(shared_def) = shared_def else {
+ 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")
+ };
+ let expected_owners = shared_def.expected_owners()?;
+
+ ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
- info!("get parts");
- let mut parts: BTreeMap<String, GeneratorPart> = nix_go_json!(default_generator.parts);
- info!("got parts: {parts:?}");
+ (true, shared_def.generator()?, expected_owners)
+ } else {
+ if shared_def.is_some() {
+ 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")
+ }
- let Some(existing) = config
- .host_secret(&host, &secret) else {
- bail!("missing secret {secret} for host {host}; secret needs regeneration")
+ (false, generator.clone(), BTreeSet::from_iter([host.clone()]))
};
- info!("got existing: {existing:?}");
+ let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;
+ let expectations = Expectations {
+ parts: nix_go_json!(default_generator_drv.parts),
+ generation_data: nix_go_json!(default_generator_drv.generationData),
+ owners: expected_owners,
+ };
+
+ let reason: RegenerationReason = 'regenerate: {
+ let Some(existing) = config
+ .host_secret(&host, &secret) else {
+ break 'regenerate RegenerationReason::Missing;
+ };
+ if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {
+ break 'regenerate reason;
+ }
- let mut out = HashMap::new();
+ let mut parts = expectations.parts.clone();
- for (part_name, part) in &existing.secret.parts {
- let Some(definition) = parts.remove(part_name) else {
- warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
- continue;
- };
- if definition.encrypted != part.raw.encrypted {
- bail!("secret {secret} part {part_name} is supposed to be {}, but it is {}; secret needs regeneration", if definition.encrypted {"encrypted"} else {"unencrypted"}, if part.raw.encrypted {"encrypted"} else {"unencrypted"});
+ let mut out = HashMap::new();
+ for (part_name, part) in &existing.secret.parts {
+ let Some(definition) = parts.remove(part_name) else {
+ warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
+ continue;
+ };
+ assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");
+ out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
}
- out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
- }
- if !parts.is_empty(){
- let defs = parts.keys().collect_vec();
- bail!("secret parts are defined, but not stored: {defs:?}, secret needs regeneration")
- }
+ assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");
- Ok(Value::new_attrs(out))
+ return Ok(Value::new_attrs(out))
+ };
+
+ todo!()
+
+
},
)
.register();
crates/fleet-base/src/secret.rsdiffbeforeafterboth--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,16 +1,8 @@
-use std::collections::BTreeSet;
+use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
-use crate::fleetdata::FleetSecretData;
-
-#[derive(Debug)]
-pub struct Expectations {
- pub owners: BTreeSet<String>,
- pub generation_data: serde_json::Value,
- pub public_parts: BTreeSet<String>,
- pub private_parts: BTreeSet<String>,
-}
+use crate::fleetdata::{Expectations, FleetSecretData, FleetSecretDistribution, GeneratorPart};
#[derive(thiserror::Error, Debug)]
pub enum RegenerationReason {
@@ -34,56 +26,62 @@
ExpectedPublic(String),
#[error("secret is expired at {0}")]
Expired(DateTime<Utc>),
+
+ #[error("secret is not generated for this host")]
+ Missing,
}
pub fn secret_needs_regeneration(
- secret: &FleetSecretData,
- owners: &BTreeSet<String>,
+ secret: &FleetSecretDistribution,
expectations: &Expectations,
) -> Option<RegenerationReason> {
- if !owners.is_empty() {
- let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();
- if !added.is_empty() {
- return Some(RegenerationReason::OwnersAdded(added));
- }
+ let added: BTreeSet<String> = expectations
+ .owners
+ .difference(&secret.owners)
+ .cloned()
+ .collect();
+ if !added.is_empty() {
+ return Some(RegenerationReason::OwnersAdded(added));
+ }
- let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();
- if !removed.is_empty() {
- return Some(RegenerationReason::OwnersRemoved(removed));
- }
+ let removed: BTreeSet<String> = secret
+ .owners
+ .difference(&expectations.owners)
+ .cloned()
+ .collect();
+ if !removed.is_empty() {
+ return Some(RegenerationReason::OwnersRemoved(removed));
}
- if secret.generation_data != expectations.generation_data {
+ if secret.secret.generation_data != expectations.generation_data {
return Some(RegenerationReason::GenerationData {
expected: expectations.generation_data.clone(),
- found: secret.generation_data.clone(),
+ found: secret.secret.generation_data.clone(),
});
}
- if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {
- let expected: BTreeSet<String> = expectations
- .public_parts
- .union(&expectations.private_parts)
- .cloned()
- .collect();
- let found: BTreeSet<String> = secret.parts.keys().cloned().collect();
+ let expected: BTreeSet<String> = expectations.parts.keys().cloned().collect();
+ let found: BTreeSet<String> = secret.secret.parts.keys().cloned().collect();
- if found != expected {
- return Some(RegenerationReason::PartList { expected, found });
- }
+ if found != expected {
+ return Some(RegenerationReason::PartList { expected, found });
+ }
- for (name, value) in secret.parts.iter() {
- if value.raw.encrypted {
- if !expectations.private_parts.contains(name) {
- return Some(RegenerationReason::ExpectedPrivate(name.clone()));
- }
- } else if !expectations.public_parts.contains(name) {
- return Some(RegenerationReason::ExpectedPublic(name.clone()));
+ for (name, value) in secret.secret.parts.iter() {
+ let expectation = expectations
+ .parts
+ .get(name)
+ .expect("found == expected checked");
+ if value.raw.encrypted {
+ if !expectation.encrypted {
+ return Some(RegenerationReason::ExpectedPrivate(name.clone()));
}
+ } else if expectation.encrypted {
+ return Some(RegenerationReason::ExpectedPublic(name.clone()));
}
}
- if let Some(expiration) = secret.expires_at {
+ if let Some(expiration) = secret.secret.expires_at {
// TODO: Leeway?
if expiration < Utc::now() {
return Some(RegenerationReason::Expired(expiration));
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -731,6 +731,10 @@
}
pub fn has_field(&self, field: &str) -> Result<bool> {
+ if !matches!(self.type_of(), NixType::Attrs) {
+ bail!("invalid type: expected attrs");
+ }
+
let f = init_field_name(field);
with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })
}
@@ -881,6 +885,12 @@
pub fn is_null(&self) -> bool {
matches!(self.type_of(), NixType::Null)
}
+ pub fn is_string(&self) -> bool {
+ matches!(self.type_of(), NixType::String)
+ }
+ pub fn is_attrs(&self) -> bool {
+ matches!(self.type_of(), NixType::Attrs)
+ }
}
impl From<String> for Value {
crates/nix-eval/src/util.rsdiffbeforeafterboth--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -1,23 +1,15 @@
use std::time::Instant;
use anyhow::bail;
-use serde::Deserialize;
use tracing::{debug, warn};
use crate::{Value, nix_go_json};
-
-#[derive(Deserialize, Debug)]
-struct Assertion {
- assertion: bool,
- message: String,
-}
#[tracing::instrument(level = "info", skip(val))]
-pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
+pub fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
let before_errors = Instant::now();
let errors: Vec<String> = nix_go_json!(val.errors);
- // let assertions: Vec<Assertion> = nix_go_json!(val.assertions);
- debug!("errors evaluation took {:?} {errors:?} ", before_errors.elapsed());
+ debug!("errors evaluation took {:?}", before_errors.elapsed());
if !errors.is_empty() {
bail!(
"failed with error{}{}",
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -160,7 +160,7 @@
mkImpureSecretGenerator,
}:
mkImpureSecretGenerator {
- # TODO: Escape prompt?
+ # TODO: Escape prompt/part (preferrably just use env) to prevent shell injection
script = ''
${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}
'';
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -89,6 +89,7 @@
# If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
# (Some secrets-encryption-in-git/managed PKI solution is expected)
impureOn ? null,
+ generationData ? null,
parts,
}:
(prev.writeShellScript "impureGenerator.sh" ''
@@ -117,7 +118,7 @@
'').overrideAttrs
(old: {
passthru = {
- inherit impureOn parts;
+ inherit impureOn parts generationData;
generatorKind = "impure";
};
});