difftreelog
refactor split build-systems and deploy commands
in: trunk
10 files changed
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,3 +5,5 @@
[workspace.dependencies]
nixlike = { path = "./crates/nixlike" }
better-command = { path = "./crates/better-command" }
+uuid = { version = "1.3.3", features = ["v4"] }
+tokio = { version = "1.33.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -8,6 +8,7 @@
[dependencies]
nixlike.workspace = true
better-command.workspace = true
+tokio.workspace = true
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@@ -27,7 +28,6 @@
"wrap_help",
"unicode",
] }
-tokio = { version = "1.33.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
tokio-util = { version = "0.7.10", features = ["codec"] }
cmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -428,6 +428,7 @@
self.used_fields.extend(e.used_fields);
}
+ #[allow(dead_code)]
pub fn session(&self) -> NixSession {
let mut session = None;
for ele in &self.used_fields {
@@ -444,6 +445,7 @@
}
session.expect("expr without fields used")
}
+ #[allow(dead_code)]
pub fn index_attr(&mut self, s: &str) {
let escaped = nixlike::serialize(s).expect("string");
self.out.push('.');
@@ -559,7 +561,9 @@
pub enum Index {
Var(String),
String(String),
+ #[allow(dead_code)]
Apply(String),
+ #[allow(dead_code)]
Expr(NixExprBuilder),
ExprApply(NixExprBuilder),
Pipe(NixExprBuilder),
@@ -576,6 +580,7 @@
pub fn attr(v: impl AsRef<str>) -> Self {
Self::String(v.as_ref().to_owned())
}
+ #[allow(dead_code)]
pub fn apply(v: impl Serialize) -> Self {
let serialized = nixlike::serialize(v).expect("invalid value for apply");
Self::Apply(serialized.trim_end().to_owned())
@@ -749,6 +754,7 @@
.await
.with_context(|| context("as_json", self.0.full_path.as_deref(), &query))
}
+ #[allow(dead_code)]
pub async fn has_field(&self, name: &str) -> Result<bool> {
let id = self.0.value.expect("can't list root fields");
let key = nixlike::escape_string(name);
@@ -786,6 +792,7 @@
.await
.with_context(|| context("type_of", self.0.full_path.as_deref(), &query))
}
+ #[allow(dead_code)]
pub async fn import(&self) -> Result<Self> {
let import = Self::new(self.0.session.clone(), "import").await?;
Ok(nix_go!(self | import))
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -6,34 +6,40 @@
use crate::host::{Config, ConfigHost};
use crate::nix_go;
use anyhow::{anyhow, Result};
-use clap::Parser;
+use clap::{Parser, ValueEnum};
use itertools::Itertools as _;
use tokio::{task::LocalSet, time::sleep};
use tracing::{error, field, info, info_span, warn, Instrument};
-#[derive(Parser, Clone)]
-pub struct BuildSystems {
+#[derive(Parser)]
+pub struct Deploy {
/// Disable automatic rollback
#[clap(long)]
disable_rollback: bool,
- #[clap(subcommand)]
- subcommand: Subcommand,
+ action: DeployAction,
}
-enum UploadAction {
+#[derive(ValueEnum, Clone, Copy)]
+enum DeployAction {
+ /// Upload derivation, but do not execute the update.
+ Upload,
+ /// Upload and execute the activation script, old version will be used after reboot.
Test,
+ /// Upload and set as current system profile, but do not execute activation script.
Boot,
+ /// Upload, set current profile, and execute activation script.
Switch,
}
-impl UploadAction {
- fn name(&self) -> &'static str {
+
+impl DeployAction {
+ pub(crate) fn name(&self) -> Option<&'static str> {
match self {
- UploadAction::Test => "test",
- UploadAction::Boot => "boot",
- UploadAction::Switch => "switch",
+ DeployAction::Upload => None,
+ DeployAction::Test => Some("test"),
+ DeployAction::Boot => Some("boot"),
+ DeployAction::Switch => Some("switch"),
}
}
-
pub(crate) fn should_switch_profile(&self) -> bool {
matches!(self, Self::Switch | Self::Boot)
}
@@ -42,69 +48,15 @@
}
pub(crate) fn should_schedule_rollback_run(&self) -> bool {
matches!(self, Self::Switch | Self::Test)
- }
-}
-
-enum PackageAction {
- SdImage,
- InstallationCd,
-}
-impl PackageAction {
- fn build_attr(&self) -> String {
- match self {
- PackageAction::SdImage => "sdImage".to_owned(),
- PackageAction::InstallationCd => "isoImage".to_owned(),
- }
- }
-}
-
-enum Action {
- Upload { action: Option<UploadAction> },
- Package(PackageAction),
-}
-impl Action {
- fn build_attr(&self) -> String {
- match self {
- Action::Upload { .. } => "toplevel".to_owned(),
- Action::Package(p) => p.build_attr(),
- }
}
}
-impl From<Subcommand> for Action {
- fn from(s: Subcommand) -> Self {
- match s {
- Subcommand::Upload => Self::Upload { action: None },
- Subcommand::Test => Self::Upload {
- action: Some(UploadAction::Test),
- },
- Subcommand::Boot => Self::Upload {
- action: Some(UploadAction::Boot),
- },
- Subcommand::Switch => Self::Upload {
- action: Some(UploadAction::Switch),
- },
- Subcommand::SdImage => Self::Package(PackageAction::SdImage),
- Subcommand::InstallationCd => Self::Package(PackageAction::InstallationCd),
- }
- }
-}
-
#[derive(Parser, Clone)]
-enum Subcommand {
- /// Upload, but do not switch
- Upload,
- /// Upload + switch to built system until reboot
- Test,
- /// Upload + switch to built system after reboot
- Boot,
- /// Upload + test + boot
- Switch,
-
- /// Build SD .img image
- SdImage,
- /// Build an installation cd ISO image
- InstallationCd,
+pub struct BuildSystems {
+ /// Attribute to build. Systems are deployed from "toplevel" attr, well-known used attributes
+ /// are "sdImage"/"isoImage", and your configuration may include any other build attributes.
+ #[clap(long, default_value = "toplevel")]
+ build_attr: String,
}
struct Generation {
@@ -163,11 +115,11 @@
Ok(current)
}
-async fn execute_upload(
- build: &BuildSystems,
- action: UploadAction,
+async fn deploy_task(
+ action: DeployAction,
host: &ConfigHost,
built: PathBuf,
+ disable_rollback: bool,
) -> Result<()> {
let mut failed = false;
// TODO: Lockfile, to prevent concurrent system switch?
@@ -175,7 +127,7 @@
// is scheduler on next boot (default behavior). On current boot - rollback activator will fail due to
// unit name conflict in systemd-run
// This code is tied to rollback.nix
- if !build.disable_rollback {
+ if !disable_rollback {
let _span = info_span!("preparing").entered();
info!("preparing for rollback");
let generation = get_current_generation(host).await?;
@@ -235,13 +187,13 @@
switch_script.push("bin");
switch_script.push("switch-to-configuration");
let mut cmd = host.cmd(switch_script).in_current_span().await?;
- cmd.arg(action.name());
+ cmd.arg(action.name().expect("upload.should_activate == false"));
if let Err(e) = cmd.sudo().run().in_current_span().await {
error!("failed to activate: {e}");
failed = true;
}
}
- if !build.disable_rollback {
+ if !disable_rollback {
if failed {
info!("executing rollback");
if let Err(e) = host
@@ -280,97 +232,45 @@
Ok(())
}
-impl BuildSystems {
- async fn build_task(self, config: Config, host: String) -> Result<()> {
- info!("building");
- let host = config.host(&host).await?;
- let action = Action::from(self.subcommand.clone());
- let fleet_config = &config.config_field;
- let drv = nix_go!(
- fleet_config.hosts[{ &host.name }].nixosSystem.config.system.build[{ action.build_attr() }]
- );
- let outputs = drv.build().await.map_err(|e| {
- if action.build_attr() == "sdImage" {
+async fn build_task(config: Config, host: String, build_attr: &str) -> Result<PathBuf> {
+ info!("building");
+ let host = config.host(&host).await?;
+ // let action = Action::from(self.subcommand.clone());
+ let fleet_config = &config.config_field;
+ let drv = nix_go!(
+ fleet_config.hosts[{ &host.name }]
+ .nixosSystem
+ .config
+ .system
+ .build[{ build_attr }]
+ );
+ let outputs = drv.build().await.map_err(|e| {
+ if build_attr == "sdImage" {
info!("sd-image build failed");
info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");
}
e
})?;
- let out_output = outputs
- .get("out")
- .ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
-
- match action {
- Action::Upload { action } => {
- if !config.is_local(&host.name) {
- info!("uploading system closure");
- {
- // TODO: Move to remote_derivation method.
- // Alternatively, nix store make-content-addressed can be used,
- // at least for the first deployment, to provide trusted store key.
- //
- // It is much slower, yet doesn't require root on the deployer machine.
- let mut sign = MyCommand::new("nix");
- // Private key for host machine is registered in nix-sign.nix
- sign.arg("store")
- .arg("sign")
- .comparg("--key-file", "/etc/nix/private-key")
- .arg("-r")
- .arg(out_output);
- if let Err(e) = sign.sudo().run_nix().await {
- warn!("Failed to sign store paths: {e}");
- };
- }
- let mut tries = 0;
- loop {
- match host.remote_derivation(out_output).await {
- Ok(remote) => {
- assert!(&remote == out_output, "CA derivations aren't implemented");
- break;
- }
- Err(e) if tries < 3 => {
- tries += 1;
- warn!("Copy failure ({}/3): {}", tries, e);
- sleep(Duration::from_millis(5000)).await;
- }
- Err(e) => return Err(e),
- }
- }
- }
- if let Some(action) = action {
- execute_upload(&self, action, &host, out_output.clone()).await?
- }
- }
- Action::Package(PackageAction::SdImage) => {
- let mut out = current_dir()?;
- out.push(format!("sd-image-{}", host.name));
-
- info!("linking sd image to {:?}", out);
- symlink(out_output, out)?;
- }
- Action::Package(PackageAction::InstallationCd) => {
- let mut out = current_dir()?;
- out.push(format!("installation-cd-{}", host.name));
+ let out_output = outputs
+ .get("out")
+ .ok_or_else(|| anyhow!("system build should produce \"out\" output"))?;
- info!("linking iso image to {:?}", out);
- symlink(out_output, out)?;
- }
- };
- Ok(())
- }
+ Ok(out_output.clone())
+}
+impl BuildSystems {
pub async fn run(self, config: &Config) -> Result<()> {
let hosts = config.list_hosts().await?;
let set = LocalSet::new();
- let this = &self;
+ let build_attr = self.build_attr.clone();
for host in hosts.into_iter() {
if config.should_skip(&host.name) {
continue;
}
let config = config.clone();
- let this = this.clone();
- let span = info_span!("deployment", host = field::display(&host.name));
+ let span = info_span!("build", host = field::display(&host.name));
let hostname = host.name;
+ let build_attr = build_attr.clone();
// FIXME: Since the introduction of better-nix-eval,
// due to single repl used for builds, hosts are waiting for each other to build,
// instead of building concurrently.
@@ -384,11 +284,94 @@
// multiple hosts.
set.spawn_local(
(async move {
- match this.build_task(config, hostname).await {
- Ok(_) => {}
+ let built = match build_task(config, hostname.clone(), &build_attr).await {
+ Ok(path) => path,
+ Err(e) => {
+ error!("failed to deploy host: {}", e);
+ return;
+ }
+ };
+ // TODO: Handle error
+ let mut out = current_dir().expect("cwd exists");
+ out.push(format!("built-{}", hostname));
+
+ info!("linking iso image to {:?}", out);
+ if let Err(e) = symlink(built, out) {
+ error!("failed to symlink: {e}")
+ }
+ })
+ .instrument(span),
+ );
+ }
+ set.await;
+ Ok(())
+ }
+}
+
+impl Deploy {
+ pub async fn run(self, config: &Config) -> Result<()> {
+ let hosts = config.list_hosts().await?;
+ let set = LocalSet::new();
+ for host in hosts.into_iter() {
+ if config.should_skip(&host.name) {
+ continue;
+ }
+ let config = config.clone();
+ let span = info_span!("deploy", host = field::display(&host.name));
+ let hostname = host.name.clone();
+ // FIXME: Fix repl concurrency (see build-systems)
+ set.spawn_local(
+ (async move {
+ let built = match build_task(config.clone(), hostname.clone(), "toplevel").await
+ {
+ Ok(path) => path,
Err(e) => {
- error!("failed to deploy host: {}", e)
+ error!("failed to deploy host: {}", e);
+ return;
}
+ };
+ if !config.is_local(&hostname) {
+ info!("uploading system closure");
+ {
+ // TODO: Move to remote_derivation method.
+ // Alternatively, nix store make-content-addressed can be used,
+ // at least for the first deployment, to provide trusted store key.
+ //
+ // It is much slower, yet doesn't require root on the deployer machine.
+ let mut sign = MyCommand::new("nix");
+ // Private key for host machine is registered in nix-sign.nix
+ sign.arg("store")
+ .arg("sign")
+ .comparg("--key-file", "/etc/nix/private-key")
+ .arg("-r")
+ .arg(&built);
+ if let Err(e) = sign.sudo().run_nix().await {
+ warn!("Failed to sign store paths: {e}");
+ };
+ }
+ let mut tries = 0;
+ loop {
+ match host.remote_derivation(&built).await {
+ Ok(remote) => {
+ assert!(remote == built, "CA derivations aren't implemented");
+ break;
+ }
+ Err(e) if tries < 3 => {
+ tries += 1;
+ warn!("copy failure ({}/3): {}", tries, e);
+ sleep(Duration::from_millis(5000)).await;
+ }
+ Err(e) => {
+ error!("upload failed: {e}");
+ return;
+ }
+ }
+ }
+ }
+ if let Err(e) =
+ deploy_task(self.action, &host, built, self.disable_rollback).await
+ {
+ error!("activation failed: {e}");
}
})
.instrument(span),
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use crate::{2 better_nix_eval::Field,3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4 host::Config,5 nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use futures::StreamExt;11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15 collections::{BTreeSet, HashSet},16 io::{self, Cursor, Read},17 path::PathBuf,18};19use tabled::{Table, Tabled};20use tokio::fs::read_to_string;21use tracing::{error, info, info_span, warn, Instrument};2223#[derive(Parser)]24pub enum Secret {25 /// Force load host keys for all defined hosts26 ForceKeys,27 /// Add secret, data should be provided in stdin28 AddShared {29 /// Secret name30 name: String,31 /// Secret owners32 machines: Vec<String>,33 /// Override secret if already present34 #[clap(long)]35 force: bool,36 /// Secret public part37 #[clap(long)]38 public: Option<String>,39 /// Load public part from specified file40 #[clap(long)]41 public_file: Option<PathBuf>,4243 /// Create a notification on secret expiration44 #[clap(long)]45 expires_at: Option<DateTime<Utc>>,4647 /// Secret with this name already exists, override its value while keeping the same owners.48 #[clap(long)]49 re_add: bool,50 },51 /// Add secret, data should be provided in stdin52 Add {53 /// Secret name54 name: String,55 /// Secret owners56 machine: String,57 /// Override secret if already present58 #[clap(long)]59 force: bool,60 #[clap(long)]61 public: Option<String>,62 #[clap(long)]63 public_file: Option<PathBuf>,64 },65 /// Read secret from remote host, requires sudo on said host66 Read {67 name: String,68 machine: String,69 #[clap(long)]70 plaintext: bool,71 },72 UpdateShared {73 name: String,7475 #[clap(long)]76 machines: Option<Vec<String>>,7778 #[clap(long)]79 add_machines: Vec<String>,80 #[clap(long)]81 remove_machines: Vec<String>,8283 /// Which host should we use to decrypt84 #[clap(long)]85 prefer_identities: Vec<String>,86 },87 Regenerate {88 /// Which host should we use to decrypt, in case if reencryption is required, without89 /// regeneration90 #[clap(long)]91 prefer_identities: Vec<String>,92 },93 List {},94}9596#[tracing::instrument(skip(config, secret, field, prefer_identities))]97async fn update_owner_set(98 secret_name: &str,99 config: &Config,100 mut secret: FleetSharedSecret,101 field: Field,102 updated_set: &[String],103 prefer_identities: &[String],104) -> Result<FleetSharedSecret> {105 let original_set = secret.owners.clone();106107 let set = original_set.iter().collect::<BTreeSet<_>>();108 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();109110 if set == expected_set {111 info!("no need to update owner list, it is already correct");112 return Ok(secret);113 }114115 let should_regenerate = if set.difference(&expected_set).next().is_some() {116 // TODO: Remove this warning for revokable secrets.117 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");118 nix_go_json!(field.regenerateOnOwnerRemoved)119 } else if expected_set.difference(&set).next().is_some() {120 nix_go_json!(field.regenerateOnOwnerAdded)121 } else {122 false123 };124125 if should_regenerate {126 info!("secret is owner-dependent, will regenerate");127 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;128 Ok(generated)129 } else {130 let identity_holder = if !prefer_identities.is_empty() {131 prefer_identities132 .iter()133 .find(|i| original_set.iter().any(|s| s == *i))134 } else {135 secret.owners.first()136 };137 let Some(identity_holder) = identity_holder else {138 bail!("no available holder found");139 };140141 if let Some(data) = secret.secret.secret {142 let host = config.host(identity_holder).await?;143 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;144 secret.secret.secret = Some(encrypted);145 }146147 secret.owners = updated_set.to_vec();148 Ok(secret)149 }150}151152#[derive(Deserialize)]153#[serde(rename_all = "camelCase")]154enum GeneratorKind {155 Impure,156}157158async fn generate_impure(159 config: &Config,160 _display_name: &str,161 secret: Field,162 default_generator: Field,163 owners: &[String],164) -> Result<FleetSecret> {165 let config_field = &config.config_unchecked_field;166 let generator = nix_go!(secret.generator);167168 let on: String = nix_go_json!(default_generator.impureOn);169 let call_package = nix_go!(170 config_field.hosts[{ on }]171 .nixosSystem172 .config173 .nixpkgs174 .resolvedPkgs175 .callPackage176 );177178 let host = config.host(&on).await?;179180 let generator = nix_go!(call_package(generator)(Obj {}));181 let generator = generator.build().await?;182 let generator = generator183 .get("out")184 .ok_or_else(|| anyhow!("missing generateImpure out"))?;185 let generator = host.remote_derivation(generator).await?;186187 let mut recipients = String::new();188 for owner in owners {189 let key = config.key(owner).await?;190 recipients.push_str(&format!("-r \"{key}\" "));191 }192 recipients.push_str("-e");193194 let out = host.mktemp_dir().await?;195196 let mut gen = host.cmd(generator).await?;197 gen.env("rageArgs", recipients).env("out", &out);198 gen.run().await.context("impure generator")?;199200 {201 let marker = host.read_file_text(format!("{out}/marker")).await?;202 ensure!(marker == "SUCCESS", "generation not succeeded");203 }204205 let public = host.read_file_text(format!("{out}/public")).await.ok();206 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();207 if let Some(secret) = &secret {208 ensure!(209 age::Decryptor::new(Cursor::new(&secret)).is_ok(),210 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."211 );212 }213214 let created_at = host.read_file_value(format!("{out}/created_at")).await?;215 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();216217 Ok(FleetSecret {218 created_at,219 expires_at,220 public,221 secret: secret.map(SecretData),222 })223}224async fn generate(225 config: &Config,226 display_name: &str,227 secret: Field,228 owners: &[String],229) -> Result<FleetSecret> {230 let generator = nix_go!(secret.generator);231 // Can't properly check on nix module system level232 {233 let gen_ty = generator.type_of().await?;234 if gen_ty == "null" {235 bail!("secret has no generator defined, can't automatically generate it.");236 }237 if gen_ty != "lambda" {238 bail!("generator should be lambda, got {gen_ty}");239 }240 }241 let default_pkgs = &config.default_pkgs;242 let default_call_package = nix_go!(default_pkgs.callPackage);243 // Generators provide additional information in passthru, to access244 // passthru we should call generator, but information about where this generator is supposed to build245 // is located in passthru... Thus evaluating generator on host.246 //247 // Maybe it is also possible to do some magic with __functor?248 //249 // I don't want to make modules always responsible for additional secret data anyway,250 // so it should be in derivation, and not in the secret data itself.251 let default_generator = nix_go!(default_call_package(generator)(Obj {}));252253 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);254255 match kind {256 GeneratorKind::Impure => {257 generate_impure(config, display_name, secret, default_generator, owners).await258 }259 }260}261async fn generate_shared(262 config: &Config,263 display_name: &str,264 secret: Field,265 expected_owners: Vec<String>,266) -> Result<FleetSharedSecret> {267 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);268 Ok(FleetSharedSecret {269 secret: generate(config, display_name, secret, &expected_owners).await?,270 owners: expected_owners,271 })272}273274async fn parse_public(275 public: Option<String>,276 public_file: Option<PathBuf>,277) -> Result<Option<String>> {278 Ok(match (public, public_file) {279 (Some(v), None) => Some(v),280 (None, Some(v)) => Some(read_to_string(v).await?),281 (Some(_), Some(_)) => {282 bail!("only public or public_file should be set")283 }284 (None, None) => None,285 })286}287288fn parse_machines(289 initial: Vec<String>,290 machines: Option<Vec<String>>,291 mut add_machines: Vec<String>,292 mut remove_machines: Vec<String>,293) -> Result<Vec<String>> {294 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {295 bail!("no operation");296 }297298 let initial_machines = initial.clone();299 let mut target_machines = initial;300 info!("Currently encrypted for {initial_machines:?}");301302 // ensure!(machines.is_some() || !add_machines.is_empty() || )303 if let Some(machines) = machines {304 ensure!(305 add_machines.is_empty() && remove_machines.is_empty(),306 "can't combine --machines and --add-machines/--remove-machines"307 );308 let target = initial_machines.iter().collect::<HashSet<_>>();309 let source = machines.iter().collect::<HashSet<_>>();310 for removed in target.difference(&source) {311 remove_machines.push((*removed).clone());312 }313 for added in source.difference(&target) {314 add_machines.push((*added).clone());315 }316 }317318 for machine in &remove_machines {319 let mut removed = false;320 while let Some(pos) = target_machines.iter().position(|m| m == machine) {321 target_machines.swap_remove(pos);322 removed = true;323 }324 if !removed {325 warn!("secret is not enabled for {machine}");326 }327 }328 for machine in &add_machines {329 if target_machines.iter().any(|m| m == machine) {330 warn!("secret is already added to {machine}");331 } else {332 target_machines.push(machine.to_owned());333 }334 }335 if !remove_machines.is_empty() {336 // TODO: maybe force secret regeneration?337 // Not that useful without revokation.338 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");339 }340 Ok(target_machines)341}342impl Secret {343 pub async fn run(self, config: &Config) -> Result<()> {344 match self {345 Secret::ForceKeys => {346 for host in config.list_hosts().await? {347 if config.should_skip(&host.name) {348 continue;349 }350 config.key(&host.name).await?;351 }352 }353 Secret::AddShared {354 mut machines,355 name,356 force,357 public,358 public_file,359 expires_at,360 re_add,361 } => {362 let exists = config.has_shared(&name);363 if exists && !force && !re_add {364 bail!("secret already defined");365 }366 if re_add {367 // Fixme: use clap to limit this usage368 ensure!(!force, "--force and --readd are not compatible");369 ensure!(exists, "secret doesn't exists");370 ensure!(371 machines.is_empty(),372 "you can't use machines argument for --readd"373 );374 let shared = config.shared_secret(&name)?;375 machines = shared.owners;376 }377378 let recipients = config.recipients(machines.clone()).await?;379380 let secret = {381 let mut input = vec![];382 io::stdin().read_to_end(&mut input)?;383384 if input.is_empty() {385 None386 } else {387 Some(388 SecretData::encrypt(recipients, input)389 .ok_or_else(|| anyhow!("no recipients provided"))?,390 )391 }392 };393 let public = parse_public(public, public_file).await?;394 config.replace_shared(395 name,396 FleetSharedSecret {397 owners: machines,398 secret: FleetSecret {399 created_at: Utc::now(),400 expires_at,401 secret,402 public,403 },404 },405 );406 }407 Secret::Add {408 machine,409 name,410 force,411 public,412 public_file,413 } => {414 let recipient = config.recipient(&machine).await?;415416 let secret = {417 let mut input = vec![];418 io::stdin().read_to_end(&mut input)?;419 if input.is_empty() {420 bail!("no data provided")421 }422423 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))424 };425426 if config.has_secret(&machine, &name) && !force {427 bail!("secret already defined");428 }429 let public = parse_public(public, public_file).await?;430431 config.insert_secret(432 &machine,433 name,434 FleetSecret {435 created_at: Utc::now(),436 expires_at: None,437 secret,438 public,439 },440 );441 }442 #[allow(clippy::await_holding_refcell_ref)]443 Secret::Read {444 name,445 machine,446 plaintext,447 } => {448 let secret = config.host_secret(&machine, &name)?;449 let Some(secret) = secret.secret else {450 bail!("no secret {name}");451 };452 let host = config.host(&machine).await?;453 let data = host.decrypt(secret).await?;454 if plaintext {455 let s = String::from_utf8(data).context("output is not utf8")?;456 print!("{s}");457 } else {458 println!("{}", z85::encode(&data));459 }460 }461 Secret::UpdateShared {462 name,463 machines,464 add_machines,465 remove_machines,466 prefer_identities,467 } => {468 let secret = config.shared_secret(&name)?;469 if secret.secret.secret.is_none() {470 bail!("no secret");471 }472473 let initial_machines = secret.owners.clone();474 let target_machines = parse_machines(475 initial_machines.clone(),476 machines,477 add_machines,478 remove_machines,479 )?;480481 if target_machines.is_empty() {482 info!("no machines left for secret, removing it");483 config.remove_shared(&name);484 return Ok(());485 }486487 let config_field = &config.config_unchecked_field;488 let field = nix_go!(config_field.sharedSecrets[{ name }]);489490 let updated = update_owner_set(491 &name,492 config,493 secret,494 field,495 &target_machines,496 &prefer_identities,497 )498 .await?;499 config.replace_shared(name, updated);500 }501 Secret::Regenerate { prefer_identities } => {502 info!("checking for secrets to regenerate");503 {504 let _span = info_span!("shared").entered();505 let expected_shared_set = config506 .list_configured_shared()507 .await?508 .into_iter()509 .collect::<HashSet<_>>();510 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();511 for missing in expected_shared_set.difference(&shared_set) {512 let config_field = &config.config_unchecked_field;513 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);514 let expected_owners: Option<Vec<String>> =515 nix_go_json!(secret.expectedOwners);516 let Some(expected_owners) = expected_owners else {517 // TODO: Might still need to regenerate518 continue;519 };520 info!("generating secret: {missing}");521 let shared = generate_shared(config, missing, secret, expected_owners)522 .in_current_span()523 .await?;524 config.replace_shared(missing.to_string(), shared)525 }526 }527 for host in config.list_hosts().await? {528 let _span = info_span!("host", host = host.name).entered();529 let expected_set = host530 .list_configured_secrets()531 .in_current_span()532 .await?533 .into_iter()534 .collect::<HashSet<_>>();535 let stored_set = config536 .list_secrets(&host.name)537 .into_iter()538 .collect::<HashSet<_>>();539 for missing in expected_set.difference(&stored_set) {540 info!("generating secret: {missing}");541 let secret = host.secret_field(missing).in_current_span().await?;542 let generated =543 match generate(config, missing, secret, &[host.name.clone()])544 .in_current_span()545 .await546 {547 Ok(v) => v,548 Err(e) => {549 error!("{e}");550 continue;551 }552 };553 config.insert_secret(&host.name, missing.to_string(), generated)554 }555 }556 let mut to_remove = Vec::new();557 for name in &config.list_shared() {558 info!("updating secret: {name}");559 let data = config.shared_secret(name)?;560 let config_field = &config.config_unchecked_field;561 let expected_owners: Vec<String> =562 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);563 if expected_owners.is_empty() {564 warn!("secret was removed from fleet config: {name}, removing from data");565 to_remove.push(name.to_string());566 continue;567 }568569 let secret = nix_go!(config_field.sharedSecrets[{ name }]);570 config.replace_shared(571 name.to_owned(),572 update_owner_set(573 &name,574 config,575 data,576 secret,577 &expected_owners,578 &prefer_identities,579 )580 .await?,581 );582 }583 for k in to_remove {584 config.remove_shared(&k);585 }586 }587 Secret::List {} => {588 let _span = info_span!("loading secrets").entered();589 let configured = config.list_configured_shared().await?;590 #[derive(Tabled)]591 struct SecretDisplay {592 #[tabled(rename = "Name")]593 name: String,594 #[tabled(rename = "Owners")]595 owners: String,596 }597 let mut table = vec![];598 for name in configured.iter().cloned() {599 let config = config.clone();600 let expected_owners = config.shared_secret_expected_owners(&name).await?;601 let data = config.shared_secret(&name)?;602 let owners = data603 .owners604 .iter()605 .map(|o| {606 if expected_owners.contains(o) {607 o.green().to_string()608 } else {609 o.red().to_string()610 }611 })612 .collect::<Vec<_>>();613 table.push(SecretDisplay {614 owners: owners.join(", "),615 name,616 })617 }618 info!("loaded\n{}", Table::new(table).to_string())619 }620 }621 Ok(())622 }623}1use crate::{2 better_nix_eval::Field,3 fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4 host::Config,5 nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use owo_colors::OwoColorize;11use serde::Deserialize;12use std::{13 collections::{BTreeSet, HashSet},14 io::{self, Cursor, Read},15 path::PathBuf,16};17use tabled::{Table, Tabled};18use tokio::fs::read_to_string;19use tracing::{error, info, info_span, warn, Instrument};2021#[derive(Parser)]22pub enum Secret {23 /// Force load host keys for all defined hosts24 ForceKeys,25 /// Add secret, data should be provided in stdin26 AddShared {27 /// Secret name28 name: String,29 /// Secret owners30 machines: Vec<String>,31 /// Override secret if already present32 #[clap(long)]33 force: bool,34 /// Secret public part35 #[clap(long)]36 public: Option<String>,37 /// Load public part from specified file38 #[clap(long)]39 public_file: Option<PathBuf>,4041 /// Create a notification on secret expiration42 #[clap(long)]43 expires_at: Option<DateTime<Utc>>,4445 /// Secret with this name already exists, override its value while keeping the same owners.46 #[clap(long)]47 re_add: bool,48 },49 /// Add secret, data should be provided in stdin50 Add {51 /// Secret name52 name: String,53 /// Secret owners54 machine: String,55 /// Override secret if already present56 #[clap(long)]57 force: bool,58 #[clap(long)]59 public: Option<String>,60 #[clap(long)]61 public_file: Option<PathBuf>,62 },63 /// Read secret from remote host, requires sudo on said host64 Read {65 name: String,66 machine: String,67 #[clap(long)]68 plaintext: bool,69 },70 UpdateShared {71 name: String,7273 #[clap(long)]74 machines: Option<Vec<String>>,7576 #[clap(long)]77 add_machines: Vec<String>,78 #[clap(long)]79 remove_machines: Vec<String>,8081 /// Which host should we use to decrypt82 #[clap(long)]83 prefer_identities: Vec<String>,84 },85 Regenerate {86 /// Which host should we use to decrypt, in case if reencryption is required, without87 /// regeneration88 #[clap(long)]89 prefer_identities: Vec<String>,90 },91 List {},92}9394#[tracing::instrument(skip(config, secret, field, prefer_identities))]95async fn update_owner_set(96 secret_name: &str,97 config: &Config,98 mut secret: FleetSharedSecret,99 field: Field,100 updated_set: &[String],101 prefer_identities: &[String],102) -> Result<FleetSharedSecret> {103 let original_set = secret.owners.clone();104105 let set = original_set.iter().collect::<BTreeSet<_>>();106 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();107108 if set == expected_set {109 info!("no need to update owner list, it is already correct");110 return Ok(secret);111 }112113 let should_regenerate = if set.difference(&expected_set).next().is_some() {114 // TODO: Remove this warning for revokable secrets.115 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");116 nix_go_json!(field.regenerateOnOwnerRemoved)117 } else if expected_set.difference(&set).next().is_some() {118 nix_go_json!(field.regenerateOnOwnerAdded)119 } else {120 false121 };122123 if should_regenerate {124 info!("secret is owner-dependent, will regenerate");125 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;126 Ok(generated)127 } else {128 let identity_holder = if !prefer_identities.is_empty() {129 prefer_identities130 .iter()131 .find(|i| original_set.iter().any(|s| s == *i))132 } else {133 secret.owners.first()134 };135 let Some(identity_holder) = identity_holder else {136 bail!("no available holder found");137 };138139 if let Some(data) = secret.secret.secret {140 let host = config.host(identity_holder).await?;141 let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;142 secret.secret.secret = Some(encrypted);143 }144145 secret.owners = updated_set.to_vec();146 Ok(secret)147 }148}149150#[derive(Deserialize)]151#[serde(rename_all = "camelCase")]152enum GeneratorKind {153 Impure,154}155156async fn generate_impure(157 config: &Config,158 _display_name: &str,159 secret: Field,160 default_generator: Field,161 owners: &[String],162) -> Result<FleetSecret> {163 let config_field = &config.config_unchecked_field;164 let generator = nix_go!(secret.generator);165166 let on: String = nix_go_json!(default_generator.impureOn);167 let call_package = nix_go!(168 config_field.hosts[{ on }]169 .nixosSystem170 .config171 .nixpkgs172 .resolvedPkgs173 .callPackage174 );175176 let host = config.host(&on).await?;177178 let generator = nix_go!(call_package(generator)(Obj {}));179 let generator = generator.build().await?;180 let generator = generator181 .get("out")182 .ok_or_else(|| anyhow!("missing generateImpure out"))?;183 let generator = host.remote_derivation(generator).await?;184185 let mut recipients = String::new();186 for owner in owners {187 let key = config.key(owner).await?;188 recipients.push_str(&format!("-r \"{key}\" "));189 }190 recipients.push_str("-e");191192 let out = host.mktemp_dir().await?;193194 let mut gen = host.cmd(generator).await?;195 gen.env("rageArgs", recipients).env("out", &out);196 gen.run().await.context("impure generator")?;197198 {199 let marker = host.read_file_text(format!("{out}/marker")).await?;200 ensure!(marker == "SUCCESS", "generation not succeeded");201 }202203 let public = host.read_file_text(format!("{out}/public")).await.ok();204 let secret = host.read_file_bin(format!("{out}/secret")).await.ok();205 if let Some(secret) = &secret {206 ensure!(207 age::Decryptor::new(Cursor::new(&secret)).is_ok(),208 "builder produced non-encrypted value as secret, this is highly insecure, and not allowed."209 );210 }211212 let created_at = host.read_file_value(format!("{out}/created_at")).await?;213 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();214215 Ok(FleetSecret {216 created_at,217 expires_at,218 public,219 secret: secret.map(SecretData),220 })221}222async fn generate(223 config: &Config,224 display_name: &str,225 secret: Field,226 owners: &[String],227) -> Result<FleetSecret> {228 let generator = nix_go!(secret.generator);229 // Can't properly check on nix module system level230 {231 let gen_ty = generator.type_of().await?;232 if gen_ty == "null" {233 bail!("secret has no generator defined, can't automatically generate it.");234 }235 if gen_ty != "lambda" {236 bail!("generator should be lambda, got {gen_ty}");237 }238 }239 let default_pkgs = &config.default_pkgs;240 let default_call_package = nix_go!(default_pkgs.callPackage);241 // Generators provide additional information in passthru, to access242 // passthru we should call generator, but information about where this generator is supposed to build243 // is located in passthru... Thus evaluating generator on host.244 //245 // Maybe it is also possible to do some magic with __functor?246 //247 // I don't want to make modules always responsible for additional secret data anyway,248 // so it should be in derivation, and not in the secret data itself.249 let default_generator = nix_go!(default_call_package(generator)(Obj {}));250251 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);252253 match kind {254 GeneratorKind::Impure => {255 generate_impure(config, display_name, secret, default_generator, owners).await256 }257 }258}259async fn generate_shared(260 config: &Config,261 display_name: &str,262 secret: Field,263 expected_owners: Vec<String>,264) -> Result<FleetSharedSecret> {265 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);266 Ok(FleetSharedSecret {267 secret: generate(config, display_name, secret, &expected_owners).await?,268 owners: expected_owners,269 })270}271272async fn parse_public(273 public: Option<String>,274 public_file: Option<PathBuf>,275) -> Result<Option<String>> {276 Ok(match (public, public_file) {277 (Some(v), None) => Some(v),278 (None, Some(v)) => Some(read_to_string(v).await?),279 (Some(_), Some(_)) => {280 bail!("only public or public_file should be set")281 }282 (None, None) => None,283 })284}285286fn parse_machines(287 initial: Vec<String>,288 machines: Option<Vec<String>>,289 mut add_machines: Vec<String>,290 mut remove_machines: Vec<String>,291) -> Result<Vec<String>> {292 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {293 bail!("no operation");294 }295296 let initial_machines = initial.clone();297 let mut target_machines = initial;298 info!("Currently encrypted for {initial_machines:?}");299300 // ensure!(machines.is_some() || !add_machines.is_empty() || )301 if let Some(machines) = machines {302 ensure!(303 add_machines.is_empty() && remove_machines.is_empty(),304 "can't combine --machines and --add-machines/--remove-machines"305 );306 let target = initial_machines.iter().collect::<HashSet<_>>();307 let source = machines.iter().collect::<HashSet<_>>();308 for removed in target.difference(&source) {309 remove_machines.push((*removed).clone());310 }311 for added in source.difference(&target) {312 add_machines.push((*added).clone());313 }314 }315316 for machine in &remove_machines {317 let mut removed = false;318 while let Some(pos) = target_machines.iter().position(|m| m == machine) {319 target_machines.swap_remove(pos);320 removed = true;321 }322 if !removed {323 warn!("secret is not enabled for {machine}");324 }325 }326 for machine in &add_machines {327 if target_machines.iter().any(|m| m == machine) {328 warn!("secret is already added to {machine}");329 } else {330 target_machines.push(machine.to_owned());331 }332 }333 if !remove_machines.is_empty() {334 // TODO: maybe force secret regeneration?335 // Not that useful without revokation.336 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");337 }338 Ok(target_machines)339}340impl Secret {341 pub async fn run(self, config: &Config) -> Result<()> {342 match self {343 Secret::ForceKeys => {344 for host in config.list_hosts().await? {345 if config.should_skip(&host.name) {346 continue;347 }348 config.key(&host.name).await?;349 }350 }351 Secret::AddShared {352 mut machines,353 name,354 force,355 public,356 public_file,357 expires_at,358 re_add,359 } => {360 let exists = config.has_shared(&name);361 if exists && !force && !re_add {362 bail!("secret already defined");363 }364 if re_add {365 // Fixme: use clap to limit this usage366 ensure!(!force, "--force and --readd are not compatible");367 ensure!(exists, "secret doesn't exists");368 ensure!(369 machines.is_empty(),370 "you can't use machines argument for --readd"371 );372 let shared = config.shared_secret(&name)?;373 machines = shared.owners;374 }375376 let recipients = config.recipients(machines.clone()).await?;377378 let secret = {379 let mut input = vec![];380 io::stdin().read_to_end(&mut input)?;381382 if input.is_empty() {383 None384 } else {385 Some(386 SecretData::encrypt(recipients, input)387 .ok_or_else(|| anyhow!("no recipients provided"))?,388 )389 }390 };391 let public = parse_public(public, public_file).await?;392 config.replace_shared(393 name,394 FleetSharedSecret {395 owners: machines,396 secret: FleetSecret {397 created_at: Utc::now(),398 expires_at,399 secret,400 public,401 },402 },403 );404 }405 Secret::Add {406 machine,407 name,408 force,409 public,410 public_file,411 } => {412 let recipient = config.recipient(&machine).await?;413414 let secret = {415 let mut input = vec![];416 io::stdin().read_to_end(&mut input)?;417 if input.is_empty() {418 bail!("no data provided")419 }420421 Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))422 };423424 if config.has_secret(&machine, &name) && !force {425 bail!("secret already defined");426 }427 let public = parse_public(public, public_file).await?;428429 config.insert_secret(430 &machine,431 name,432 FleetSecret {433 created_at: Utc::now(),434 expires_at: None,435 secret,436 public,437 },438 );439 }440 #[allow(clippy::await_holding_refcell_ref)]441 Secret::Read {442 name,443 machine,444 plaintext,445 } => {446 let secret = config.host_secret(&machine, &name)?;447 let Some(secret) = secret.secret else {448 bail!("no secret {name}");449 };450 let host = config.host(&machine).await?;451 let data = host.decrypt(secret).await?;452 if plaintext {453 let s = String::from_utf8(data).context("output is not utf8")?;454 print!("{s}");455 } else {456 println!("{}", z85::encode(&data));457 }458 }459 Secret::UpdateShared {460 name,461 machines,462 add_machines,463 remove_machines,464 prefer_identities,465 } => {466 let secret = config.shared_secret(&name)?;467 if secret.secret.secret.is_none() {468 bail!("no secret");469 }470471 let initial_machines = secret.owners.clone();472 let target_machines = parse_machines(473 initial_machines.clone(),474 machines,475 add_machines,476 remove_machines,477 )?;478479 if target_machines.is_empty() {480 info!("no machines left for secret, removing it");481 config.remove_shared(&name);482 return Ok(());483 }484485 let config_field = &config.config_unchecked_field;486 let field = nix_go!(config_field.sharedSecrets[{ name }]);487488 let updated = update_owner_set(489 &name,490 config,491 secret,492 field,493 &target_machines,494 &prefer_identities,495 )496 .await?;497 config.replace_shared(name, updated);498 }499 Secret::Regenerate { prefer_identities } => {500 info!("checking for secrets to regenerate");501 {502 let _span = info_span!("shared").entered();503 let expected_shared_set = config504 .list_configured_shared()505 .await?506 .into_iter()507 .collect::<HashSet<_>>();508 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();509 for missing in expected_shared_set.difference(&shared_set) {510 let config_field = &config.config_unchecked_field;511 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);512 let expected_owners: Option<Vec<String>> =513 nix_go_json!(secret.expectedOwners);514 let Some(expected_owners) = expected_owners else {515 // TODO: Might still need to regenerate516 continue;517 };518 info!("generating secret: {missing}");519 let shared = generate_shared(config, missing, secret, expected_owners)520 .in_current_span()521 .await?;522 config.replace_shared(missing.to_string(), shared)523 }524 }525 for host in config.list_hosts().await? {526 let _span = info_span!("host", host = host.name).entered();527 let expected_set = host528 .list_configured_secrets()529 .in_current_span()530 .await?531 .into_iter()532 .collect::<HashSet<_>>();533 let stored_set = config534 .list_secrets(&host.name)535 .into_iter()536 .collect::<HashSet<_>>();537 for missing in expected_set.difference(&stored_set) {538 info!("generating secret: {missing}");539 let secret = host.secret_field(missing).in_current_span().await?;540 let generated =541 match generate(config, missing, secret, &[host.name.clone()])542 .in_current_span()543 .await544 {545 Ok(v) => v,546 Err(e) => {547 error!("{e}");548 continue;549 }550 };551 config.insert_secret(&host.name, missing.to_string(), generated)552 }553 }554 let mut to_remove = Vec::new();555 for name in &config.list_shared() {556 info!("updating secret: {name}");557 let data = config.shared_secret(name)?;558 let config_field = &config.config_unchecked_field;559 let expected_owners: Vec<String> =560 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);561 if expected_owners.is_empty() {562 warn!("secret was removed from fleet config: {name}, removing from data");563 to_remove.push(name.to_string());564 continue;565 }566567 let secret = nix_go!(config_field.sharedSecrets[{ name }]);568 config.replace_shared(569 name.to_owned(),570 update_owner_set(571 name,572 config,573 data,574 secret,575 &expected_owners,576 &prefer_identities,577 )578 .await?,579 );580 }581 for k in to_remove {582 config.remove_shared(&k);583 }584 }585 Secret::List {} => {586 let _span = info_span!("loading secrets").entered();587 let configured = config.list_configured_shared().await?;588 #[derive(Tabled)]589 struct SecretDisplay {590 #[tabled(rename = "Name")]591 name: String,592 #[tabled(rename = "Owners")]593 owners: String,594 }595 let mut table = vec![];596 for name in configured.iter().cloned() {597 let config = config.clone();598 let expected_owners = config.shared_secret_expected_owners(&name).await?;599 let data = config.shared_secret(&name)?;600 let owners = data601 .owners602 .iter()603 .map(|o| {604 if expected_owners.contains(o) {605 o.green().to_string()606 } else {607 o.red().to_string()608 }609 })610 .collect::<Vec<_>>();611 table.push(SecretDisplay {612 owners: owners.join(", "),613 name,614 })615 }616 info!("loaded\n{}", Table::new(table).to_string())617 }618 }619 Ok(())620 }621}cmds/fleet/src/host.rsdiffbeforeafterboth--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -14,7 +14,6 @@
use openssh::SessionBuilder;
use serde::de::DeserializeOwned;
use tempfile::NamedTempFile;
-use tracing::instrument;
use crate::{
better_nix_eval::{Field, NixSessionPool},
@@ -90,6 +89,7 @@
cmd.arg(path);
cmd.run_string().await
}
+ #[allow(dead_code)]
pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {
let text = self.read_file_text(path).await?;
Ok(serde_json::from_str(&text)?)
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -12,14 +12,17 @@
mod fleetdata;
use std::ffi::OsString;
-use std::io::{stderr, stdout, Write};
use std::process::exit;
use std::time::Duration;
use anyhow::{bail, Result};
use clap::Parser;
-use cmds::{build_systems::BuildSystems, info::Info, secrets::Secret};
+use cmds::{
+ build_systems::{BuildSystems, Deploy},
+ info::Info,
+ secrets::Secret,
+};
use futures::future::LocalBoxFuture;
use futures::stream::FuturesUnordered;
use futures::TryStreamExt;
@@ -73,6 +76,8 @@
enum Opts {
/// Prepare systems for deployments
BuildSystems(BuildSystems),
+
+ Deploy(Deploy),
/// Secret management
#[clap(subcommand)]
Secret(Secret),
@@ -94,6 +99,7 @@
async fn run_command(config: &Config, command: Opts) -> Result<()> {
match command {
Opts::BuildSystems(c) => c.run(config).await?,
+ Opts::Deploy(d) => d.run(config).await?,
Opts::Secret(s) => s.run(config).await?,
Opts::Info(i) => i.run(config).await?,
Opts::Prefetch(p) => p.run(config).await?,
crates/better-command/src/handler.rsdiffbeforeafterboth--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -165,7 +165,7 @@
drv = pkg;
}
}
- // info!(target: "nix","copying {} {} -> {}", drv, from, to);
+ info!(target: "nix","copying {} {} -> {}", drv, from, to);
let span = info_span!("copy", from, to, drv);
span.pb_start();
self.spans.insert(id, span);
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -38,11 +38,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1703974965,
- "narHash": "sha256-dvZjLuAcLnv25bqStTL2ZICC5YSs8aynF5amRM+I6UM=",
+ "lastModified": 1704409229,
+ "narHash": "sha256-Vc41cRJ3trOnocovLe0zZE35pK5Lfuo/zHk0xx3CNDY=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "9f434bd436e2bb5615827469ed651e30c26daada",
+ "rev": "786f788914f2a6e94cedf361541894e972b8fd23",
"type": "github"
},
"original": {
@@ -67,11 +67,11 @@
]
},
"locked": {
- "lastModified": 1703902408,
- "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
+ "lastModified": 1704075545,
+ "narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
+ "rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"type": "github"
},
"original": {
flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -29,7 +29,7 @@
llvmPkgs = pkgs.buildPackages.llvmPackages_11;
rust =
(pkgs.rustChannelOf {
- date = "2023-12-29";
+ date = "2024-01-01";
channel = "nightly";
})
.default