1use age::Recipient;2use anyhow::Result;3use chrono::{DateTime, Utc};4use itertools::Itertools;5use nixlike::format_nix;6use serde::{Deserialize, Deserializer, Serialize, Serializer};7use std::{8 collections::BTreeMap,9 io::{self, Cursor},10};11use tempfile::TempDir;12use tokio::{13 fs::{self, File},14 io::AsyncWriteExt,15 process::Command,16};1718#[derive(Serialize, Deserialize, Default)]19#[serde(rename_all = "camelCase")]20pub struct HostData {21 #[serde(default)]22 #[serde(skip_serializing_if = "String::is_empty")]23 pub encryption_key: String,24}2526#[derive(Serialize, Deserialize)]27#[serde(rename_all = "camelCase")]28pub struct FleetData {29 #[serde(default)]30 pub hosts: BTreeMap<String, HostData>,31 #[serde(default)]32 #[serde(skip_serializing_if = "BTreeMap::is_empty")]33 pub shared_secrets: BTreeMap<String, FleetSharedSecret>,34 #[serde(default)]35 #[serde(skip_serializing_if = "BTreeMap::is_empty")]36 pub host_secrets: BTreeMap<String, BTreeMap<String, FleetSecret>>,37}3839#[derive(Serialize, Deserialize, Clone)]40#[serde(rename_all = "camelCase")]41#[must_use]42pub struct FleetSharedSecret {43 pub owners: Vec<String>,44 #[serde(flatten)]45 pub secret: FleetSecret,46}4748#[derive(Serialize, Deserialize, Clone)]49pub struct SecretData(50 #[serde(51 default,52 skip_serializing_if = "Vec::is_empty",53 serialize_with = "as_z85",54 deserialize_with = "from_z85"55 )]56 pub Vec<u8>,57);58impl SecretData {59 60 pub fn encrypt(61 recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,62 data: Vec<u8>,63 ) -> Option<Self> {64 let mut encrypted = vec![];65 let recipients = recipients66 .into_iter()67 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)68 .collect_vec();69 let mut encryptor = age::Encryptor::with_recipients(recipients)?70 .wrap_output(&mut encrypted)71 .expect("in memory write");72 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");73 encryptor.finish().expect("in memory flush");74 Some(Self(encrypted))75 }76 pub fn encode_z85(&self) -> String {77 z85::encode(&self.0)78 }79 pub fn decode_z85(v: &str) -> Result<Self> {80 let v = z85::decode(v)?;81 Ok(Self(v))82 }83}8485#[derive(Serialize, Deserialize, Clone)]86#[serde(rename_all = "camelCase")]87#[must_use]88pub struct FleetSecret {89 #[serde(default = "Utc::now")]90 pub created_at: DateTime<Utc>,91 #[serde(default)]92 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]93 pub expires_at: Option<DateTime<Utc>>,94 #[serde(skip_serializing_if = "Option::is_none")]95 pub public: Option<String>,96 #[serde(skip_serializing_if = "Option::is_none")]97 pub secret: Option<SecretData>,98}99100fn as_z85<S>(key: &[u8], serializer: S) -> Result<S::Ok, S::Error>101where102 S: Serializer,103{104 serializer.serialize_str(&z85::encode(key))105}106107fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>108where109 D: Deserializer<'de>,110{111 use serde::de::Error;112 String::deserialize(deserializer)113 .and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))114}115116117#[allow(dead_code)]118pub async fn dummy_flake() -> Result<TempDir> {119 let data_str = fs::read_to_string("fleet.nix").await?;120121 let mut cmd = Command::new("nix");122 cmd.arg("flake").arg("metadata").arg("--json");123124 let flake_dir = tempfile::tempdir()?;125 let mut flake_nix = flake_dir.path().to_path_buf();126 flake_nix.push("flake.nix");127 128129 File::create(&flake_nix)130 .await?131 .write_all(132 format_nix(&format!(133 "134 {{135 outputs = {{self, ...}}: {{136 data = {data_str};137 }};138 }}139 "140 ))141 .as_bytes(),142 )143 .await?;144145 146 147 148 dbg!(&flake_nix);149 Ok(flake_dir)150}