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.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -7,8 +7,6 @@
use anyhow::{anyhow, bail, ensure, Context, Result};
use chrono::{DateTime, Utc};
use clap::Parser;
-use futures::StreamExt;
-use itertools::Itertools;
use owo_colors::OwoColorize;
use serde::Deserialize;
use std::{
@@ -570,7 +568,7 @@
config.replace_shared(
name.to_owned(),
update_owner_set(
- &name,
+ name,
config,
data,
secret,
cmds/fleet/src/host.rsdiffbeforeafterboth1use std::{2 env::current_dir,3 ffi::{OsStr, OsString},4 fmt::Display,5 io::Write,6 ops::Deref,7 path::PathBuf,8 str::FromStr,9 sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{anyhow, bail, Context, Result};13use clap::{ArgGroup, Parser};14use openssh::SessionBuilder;15use serde::de::DeserializeOwned;16use tempfile::NamedTempFile;17use tracing::instrument;1819use crate::{20 better_nix_eval::{Field, NixSessionPool},21 command::MyCommand,22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},23 nix_go, nix_go_json,24};2526pub struct FleetConfigInternals {27 pub local_system: String,28 pub directory: PathBuf,29 pub opts: FleetOpts,30 pub data: Mutex<FleetData>,31 pub nix_args: Vec<OsString>,32 /// fleet_config.config33 pub config_field: Field,34 /// fleet_config.unchecked.config35 pub config_unchecked_field: Field,3637 /// import nixpkgs {system = local};38 pub default_pkgs: Field,39}4041#[derive(Clone)]42pub struct Config(Arc<FleetConfigInternals>);4344impl Deref for Config {45 type Target = FleetConfigInternals;4647 fn deref(&self) -> &Self::Target {48 &self.049 }50}5152pub struct ConfigHost {53 config: Config,54 pub name: String,55 pub local: bool,56 pub session: OnceLock<Arc<openssh::Session>>,5758 pub nixos_config: Field,59}60impl ConfigHost {61 async fn open_session(&self) -> Result<Arc<openssh::Session>> {62 assert!(!self.local, "do not open ssh connection to local session");63 // FIXME: TOCTOU64 if let Some(session) = &self.session.get() {65 return Ok((*session).clone());66 };67 let session = SessionBuilder::default();6869 let session = session70 .connect(&self.name)71 .await72 .map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;73 let session = Arc::new(session);74 self.session.set(session.clone()).expect("TOCTOU happened");75 Ok(session)76 }77 pub async fn mktemp_dir(&self) -> Result<String> {78 let mut cmd = self.cmd("mktemp").await?;79 cmd.arg("-d");80 let path = cmd.run_string().await?;81 Ok(path.trim_end().to_owned())82 }83 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {84 let mut cmd = self.cmd("cat").await?;85 cmd.arg(path);86 cmd.run_bytes().await87 }88 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {89 let mut cmd = self.cmd("cat").await?;90 cmd.arg(path);91 cmd.run_string().await92 }93 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {94 let text = self.read_file_text(path).await?;95 Ok(serde_json::from_str(&text)?)96 }97 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>98 where99 <D as FromStr>::Err: Display,100 {101 let text = self.read_file_text(path).await?;102 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))103 }104 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {105 if self.local {106 Ok(MyCommand::new(cmd))107 } else {108 let session = self.open_session().await?;109 Ok(MyCommand::new_on(cmd, session))110 }111 }112113 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {114 let mut cmd = self.cmd("fleet-install-secrets").await?;115 cmd.arg("decrypt").eqarg("--secret", data.encode_z85());116 let encoded = cmd117 .sudo()118 .run_string()119 .await120 .context("failed to call remote host for decrypt")?;121 z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")122 }123 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {124 let mut cmd = self.cmd("fleet-install-secrets").await?;125 cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());126 for target in targets {127 let key = self.config.key(&target).await?;128 cmd.eqarg("--targets", key);129 }130 let encoded = cmd131 .sudo()132 .run_string()133 .await134 .context("failed to call remote host for decrypt")?;135 SecretData::decode_z85(encoded.trim_end()).context("bad encoded data? outdated host?")136 }137 /// Returns path for futureproofing, as path might change i.e on conversion to CA138 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {139 if self.local {140 // Path is located locally, thus already trusted.141 return Ok(path.to_owned());142 }143 let mut nix = MyCommand::new("nix");144 nix.arg("copy")145 .arg("--substitute-on-destination")146 .comparg("--to", format!("ssh-ng://{}", self.name))147 .arg(path);148 nix.run_nix().await.context("nix copy")?;149 Ok(path.to_owned())150 }151 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {152 let mut cmd = self.cmd("systemctl").await?;153 cmd.arg("stop").arg(name);154 cmd.sudo().run().await155 }156 pub async fn systemctl_start(&self, name: &str) -> Result<()> {157 let mut cmd = self.cmd("systemctl").await?;158 cmd.arg("start").arg(name);159 cmd.sudo().run().await160 }161162 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {163 let mut cmd = self.cmd("rm").await?;164 cmd.arg("-f").arg(path);165 if sudo {166 cmd = cmd.sudo()167 }168 cmd.run().await169 }170171 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {172 let nixos = &self.nixos_config;173 let secrets = nix_go!(nixos.secrets);174 let mut out = Vec::new();175 for name in secrets.list_fields().await? {176 let secret = nix_go!(secrets[{ name }]);177 let is_shared: bool = nix_go_json!(secret.shared);178 if is_shared {179 continue;180 }181 out.push(name);182 }183 Ok(out)184 }185 pub async fn secret_field(&self, name: &str) -> Result<Field> {186 let nixos = &self.nixos_config;187 Ok(nix_go!(nixos.secrets[{ name }]))188 }189}190191impl Config {192 pub fn should_skip(&self, host: &str) -> bool {193 if !self.opts.skip.is_empty() {194 self.opts.skip.iter().any(|h| h as &str == host)195 } else if !self.opts.only.is_empty() {196 !self.opts.only.iter().any(|h| h as &str == host)197 } else {198 false199 }200 }201 pub fn is_local(&self, host: &str) -> bool {202 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)203 }204205 pub async fn host(&self, name: &str) -> Result<ConfigHost> {206 let config = &self.config_unchecked_field;207 let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);208 Ok(ConfigHost {209 config: self.clone(),210 name: name.to_owned(),211 local: self.is_local(name),212 session: OnceLock::new(),213 nixos_config,214 })215 }216 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {217 let config = &self.config_unchecked_field;218 let names = nix_go!(config.hosts).list_fields().await?;219 let mut out = vec![];220 for name in names {221 out.push(self.host(&name).await?);222 }223 Ok(out)224 }225 pub async fn system_config(&self, host: &str) -> Result<Field> {226 let fleet_field = &self.config_unchecked_field;227 Ok(nix_go!(fleet_field.hosts[{ host }].nixosSystem.config))228 }229230 pub(super) fn data(&self) -> MutexGuard<FleetData> {231 self.data.lock().unwrap()232 }233 pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {234 self.data.lock().unwrap()235 }236 /// Shared secrets configured in fleet.nix or in flake237 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {238 let config_field = &self.config_unchecked_field;239 nix_go!(config_field.sharedSecrets).list_fields().await240 }241 /// Shared secrets configured in fleet.nix242 pub fn list_shared(&self) -> Vec<String> {243 let data = self.data();244 data.shared_secrets.keys().cloned().collect()245 }246 pub fn has_shared(&self, name: &str) -> bool {247 let data = self.data();248 data.shared_secrets.contains_key(name)249 }250 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {251 let mut data = self.data_mut();252 data.shared_secrets.insert(name.to_owned(), shared);253 }254 pub fn remove_shared(&self, secret: &str) {255 let mut data = self.data_mut();256 data.shared_secrets.remove(secret);257 }258259 pub fn list_secrets(&self, host: &str) -> Vec<String> {260 let data = self.data();261 let Some(secrets) = data.host_secrets.get(host) else {262 return Vec::new();263 };264 secrets.keys().cloned().collect()265 }266267 pub fn has_secret(&self, host: &str, secret: &str) -> bool {268 let data = self.data();269 let Some(host_secrets) = data.host_secrets.get(host) else {270 return false;271 };272 host_secrets.contains_key(secret)273 }274 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {275 let mut data = self.data_mut();276 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();277 host_secrets.insert(secret, value);278 }279280 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {281 let data = self.data();282 let Some(host_secrets) = data.host_secrets.get(host) else {283 bail!("no secrets for machine {host}");284 };285 let Some(secret) = host_secrets.get(secret) else {286 bail!("machine {host} has no secret {secret}");287 };288 Ok(secret.clone())289 }290 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {291 let data = self.data();292 let Some(secret) = data.shared_secrets.get(secret) else {293 bail!("no shared secret {secret}");294 };295 Ok(secret.clone())296 }297 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {298 let config_field = &self.config_unchecked_field;299 Ok(nix_go_json!(300 config_field.sharedSecrets[{ secret }].expectedOwners301 ))302 }303304 pub fn save(&self) -> Result<()> {305 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;306 let data = nixlike::serialize(&self.data() as &FleetData)?;307 tempfile.write_all(308 format!(309 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",310 data311 )312 .as_bytes(),313 )?;314 let mut fleet_data_path = self.directory.clone();315 fleet_data_path.push("fleet.nix");316 tempfile.persist(fleet_data_path)?;317 Ok(())318 }319}320321#[derive(Parser, Clone)]322#[clap(group = ArgGroup::new("target_hosts"))]323pub struct FleetOpts {324 /// All hosts except those would be skipped325 #[clap(long, number_of_values = 1, group = "target_hosts")]326 only: Vec<String>,327328 /// Hosts to skip329 #[clap(long, number_of_values = 1, group = "target_hosts")]330 skip: Vec<String>,331332 /// Host, which should be threaten as current machine333 #[clap(long)]334 pub localhost: Option<String>,335336 /// Override detected system for host, to perform builds via337 /// binfmt-declared qemu instead of trying to crosscompile338 #[clap(long, default_value = "detect")]339 pub local_system: String,340}341342impl FleetOpts {343 pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {344 if self.localhost.is_none() {345 self.localhost346 .replace(hostname::get().unwrap().to_str().unwrap().to_owned());347 }348 let directory = current_dir()?;349350 let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;351 let root_field = pool.get().await?;352353 let builtins_field = Field::field(root_field.clone(), "builtins").await?;354 if self.local_system == "detect" {355 self.local_system = nix_go_json!(builtins_field.currentSystem);356 }357 let local_system = self.local_system.clone();358359 let fleet_root = Field::field(root_field, "fleetConfigurations").await?;360 let fleet_field = nix_go!(fleet_root.default);361362 let config_field = nix_go!(fleet_field.config);363 let config_unchecked_field = nix_go!(fleet_field.unchecked.config);364365 let import = nix_go!(builtins_field.import);366 let overlays = nix_go!(fleet_field.overlays);367 let nixpkgs = nix_go!(fleet_field.nixpkgs | import);368369 let default_pkgs = nix_go!(nixpkgs(Obj {370 overlays,371 system: { self.local_system.clone() },372 }));373374 let mut fleet_data_path = directory.clone();375 fleet_data_path.push("fleet.nix");376 let bytes = std::fs::read_to_string(fleet_data_path)?;377 let data = nixlike::parse_str(&bytes)?;378379 Ok(Config(Arc::new(FleetConfigInternals {380 opts: self,381 directory,382 data,383 local_system,384 nix_args,385 config_field,386 config_unchecked_field,387 default_pkgs,388 })))389 }390}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