difftreelog
feat ability to select specialisation to activate
in: trunk
6 files changed
Cargo.lockdiffbeforeafterboth784 "itertools",784 "itertools",785 "nix-eval",785 "nix-eval",786 "nixlike",786 "nixlike",787 "once_cell",787 "nom",788 "openssh",788 "openssh",789 "owo-colors",789 "owo-colors",790 "peg",790 "peg",cmds/fleet/Cargo.tomldiffbeforeafterboth19serde_json.workspace = true19serde_json.workspace = true20tempfile.workspace = true20tempfile.workspace = true21time = { version = "0.3", features = ["serde"] }21time = { version = "0.3", features = ["serde"] }22once_cell = "1.19"23hostname = "0.4.0"22hostname = "0.4.0"24age-core = "0.10"23age-core = "0.10"25peg = "0.8"24peg = "0.8"45human-repr = { version = "1.1", optional = true }44human-repr = { version = "1.1", optional = true }46indicatif = { version = "0.17", optional = true }45indicatif = { version = "0.17", optional = true }47nix-eval.workspace = true46nix-eval.workspace = true47nom = "7.1.3"484849[features]49[features]50# Not quite stable50# Not quite stablecmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth126 action: DeployAction,126 action: DeployAction,127 host: &ConfigHost,127 host: &ConfigHost,128 built: PathBuf,128 built: PathBuf,129 specialisation: Option<String>,129 disable_rollback: bool,130 disable_rollback: bool,130) -> Result<()> {131) -> Result<()> {131 let mut failed = false;132 let mut failed = false;190 if action.should_activate() && !failed {191 if action.should_activate() && !failed {191 let _span = info_span!("activating").entered();192 let _span = info_span!("activating").entered();192 info!("executing activation script");193 info!("executing activation script");193 let mut switch_script = built.clone();194 let specialised = if let Some(specialisation) = specialisation {194 switch_script.push("bin");195 let mut specialised = built.join("specialisation");196 specialised.push(specialisation);197 specialised198 } else {199 built.clone()200 };195 switch_script.push("switch-to-configuration");201 let switch_script = specialised.join("bin/switch-to-configuration");196 let mut cmd = host.cmd(switch_script).in_current_span().await?;202 let mut cmd = host.cmd(switch_script).in_current_span().await?;197 cmd.arg(action.name().expect("upload.should_activate == false"));203 cmd.arg(action.name().expect("upload.should_activate == false"));198 if let Err(e) = cmd.sudo().run().in_current_span().await {204 if let Err(e) = cmd.sudo().run().in_current_span().await {255 .system261 .system256 .build[{ build_attr }]262 .build[{ build_attr }]257 );263 );258 let outputs = drv.build().await.map_err(|e| {264 let outputs = drv.build().await.inspect_err(|_| {259 if build_attr == "sdImage" {265 if build_attr == "sdImage" {260 info!("sd-image build failed");266 info!("sd-image build failed");261 info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");267 info!("Make sure you have imported modulesPath/installer/sd-card/sd-image-<arch>[-installer].nix (For installer, you may want to check config)");262 }268 }263 e264 })?;269 })?;265 let out_output = outputs270 let out_output = outputs266 .get("out")271 .get("out")275 let set = LocalSet::new();280 let set = LocalSet::new();276 let build_attr = self.build_attr.clone();281 let build_attr = self.build_attr.clone();277 for host in hosts.into_iter() {282 for host in hosts.into_iter() {278 if config.should_skip(&host.name) {283 if config.should_skip(&host).await? {279 continue;284 continue;280 }285 }281 let config = config.clone();286 let config = config.clone();324 let hosts = config.list_hosts().await?;329 let hosts = config.list_hosts().await?;325 let set = LocalSet::new();330 let set = LocalSet::new();326 for host in hosts.into_iter() {331 for host in hosts.into_iter() {327 if config.should_skip(&host.name) {332 if config.should_skip(&host).await? {328 continue;333 continue;329 }334 }330 let config = config.clone();335 let config = config.clone();383 deploy_task(self.action, &host, built, self.disable_rollback).await388 self.action,389 &host,390 built,391 if let Ok(v) = config.action_attr(&host, "specialisation").await {392 v393 } else {394 error!("unreachable? failed to get specialization");395 return;396 },397 self.disable_rollback,398 )399 .await384 {400 {cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth436 match self {436 match self {437 Secret::ForceKeys => {437 Secret::ForceKeys => {438 for host in config.list_hosts().await? {438 for host in config.list_hosts().await? {439 if config.should_skip(&host.name) {439 if config.should_skip(&host).await? {440 continue;440 continue;441 }441 }442 config.key(&host.name).await?;442 config.key(&host.name).await?;639 }639 }640 }640 }641 for host in config.list_hosts().await? {641 for host in config.list_hosts().await? {642 if config.should_skip(&host.name) {642 if config.should_skip(&host).await? {643 continue;643 continue;644 }644 }645645cmds/fleet/src/host.rsdiffbeforeafterboth1use std::{1use std::{2 cell::OnceCell,3 collections::BTreeMap,2 env::current_dir,4 env::current_dir,3 ffi::{OsStr, OsString},5 ffi::{OsStr, OsString},4 fmt::Display,6 fmt::Display,10};12};111312use anyhow::{anyhow, bail, ensure, Context, Result};14use anyhow::{anyhow, bail, ensure, Context, Result};13use clap::{ArgGroup, Parser};15use clap::Parser;14use fleet_shared::SecretData;16use fleet_shared::SecretData;15use nix_eval::{nix_go, nix_go_json, NixSessionPool, Value};17use nix_eval::{nix_go, nix_go_json, NixSessionPool, Value};18use nom::{19 bytes::complete::take_while1,20 character::complete::char,21 combinator::{map, opt},22 multi::separated_list1,23 sequence::{preceded, separated_pair},24};16use openssh::SessionBuilder;25use openssh::SessionBuilder;17use serde::de::DeserializeOwned;26use serde::de::DeserializeOwned;18use tempfile::NamedTempFile;27use tempfile::NamedTempFile;53 pub name: String,62 pub name: String,54 pub local: bool,63 pub local: bool,55 pub session: OnceLock<Arc<openssh::Session>>,64 pub session: OnceLock<Arc<openssh::Session>>,65 groups: OnceCell<Vec<String>>,566657 pub nixos_config: Option<Value>,67 pub nixos_config: Option<Value>,58}68}59impl ConfigHost {69impl ConfigHost {70 pub async fn tags(&self) -> Result<Vec<String>> {71 if let Some(v) = self.groups.get() {72 return Ok(v.clone());73 }74 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,75 // assuming getting tags always returns the same value.76 let Some(nixos_config) = &self.nixos_config else {77 return Ok(vec![]);78 };79 let tags: Vec<String> = nix_go_json!(nixos_config.tags);8081 let _ = self.groups.set(tags.clone());8283 Ok(tags)84 }60 async fn open_session(&self) -> Result<Arc<openssh::Session>> {85 async fn open_session(&self) -> Result<Arc<openssh::Session>> {61 assert!(!self.local, "do not open ssh connection to local session");86 assert!(!self.local, "do not open ssh connection to local session");62 // FIXME: TOCTOU87 // FIXME: TOCTOU217}242}218243219impl Config {244impl Config {220 pub fn should_skip(&self, host: &str) -> bool {245 pub async fn should_skip(&self, host: &ConfigHost) -> Result<bool> {221 if !self.opts.skip.is_empty() {246 if !self.opts.skip.is_empty() && self.opts.skip.iter().any(|h| h as &str == host.name) {222 self.opts.skip.iter().any(|h| h as &str == host)223 } else if !self.opts.only.is_empty() {224 !self.opts.only.iter().any(|h| h as &str == host)247 return Ok(true);225 } else {226 false227 }248 }249 if self.opts.only.is_empty() {250 return Ok(false);251 }252 let mut have_group_matches = false;253 for item in self.opts.only.iter() {254 match item {255 HostItem::Host { name, .. } if *name == host.name => {256 return Ok(false);257 }258 HostItem::Tag { .. } => {259 have_group_matches = true;260 }261 _ => {}262 }263 }264 if have_group_matches {265 let host_tags = host.tags().await?;266 for item in self.opts.only.iter() {267 match item {268 HostItem::Tag { name, .. } if host_tags.contains(name) => {269 return Ok(false);270 }271 _ => {}272 }273 }274 }275 Ok(true)228 }276 }277 pub async fn action_attr(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {278 if self.opts.only.is_empty() {279 return Ok(None);280 }281 let mut have_group_matches = false;282 for item in self.opts.only.iter() {283 match item {284 HostItem::Host { name, attrs }285 if *name == host.name && attrs.contains_key(attr) =>286 {287 return Ok(attrs.get(attr).cloned());288 }289 HostItem::Tag { attrs, .. } if attrs.contains_key(attr) => {290 have_group_matches = true;291 }292 _ => {}293 }294 }295 if have_group_matches {296 let host_tags = host.tags().await?;297 for item in self.opts.only.iter() {298 match item {299 HostItem::Tag { name, attrs }300 if host_tags.contains(name) && attrs.contains_key(attr) =>301 {302 return Ok(attrs.get(attr).cloned());303 }304 _ => {}305 }306 }307 }308 Ok(None)309 }229 pub fn is_local(&self, host: &str) -> bool {310 pub fn is_local(&self, host: &str) -> bool {230 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)311 self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)231 }312 }237 local: true,318 local: true,238 session: OnceLock::new(),319 session: OnceLock::new(),239 nixos_config: None,320 nixos_config: None,321 groups: {322 let cell = OnceCell::new();323 let _ = cell.set(vec![]);324 cell325 },240 }326 }241 }327 }242328249 local: self.is_local(name),335 local: self.is_local(name),250 session: OnceLock::new(),336 session: OnceLock::new(),251 nixos_config: Some(nixos_config),337 nixos_config: Some(nixos_config),338 groups: OnceCell::new(),252 })339 })253 }340 }254 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {341 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {353 fleet_data_path.push("fleet.nix");440 fleet_data_path.push("fleet.nix");354 tempfile.persist(fleet_data_path)?;441 tempfile.persist(fleet_data_path)?;355 Ok(())442 Ok(())443 }444}445446#[derive(Clone)]447enum HostItem {448 Host {449 name: String,450 attrs: BTreeMap<String, String>,451 },452 Tag {453 name: String,454 attrs: BTreeMap<String, String>,455 },456}457fn host_item_parser(input: &str) -> Result<HostItem, String> {458 fn err_to_string(err: nom::Err<nom::error::Error<&str>>) -> String {459 err.to_string()460 }461462 let (input, is_tag) = map(opt(char('@')), |c| c.is_some())(input).map_err(err_to_string)?;463 let (input, name) = map(464 take_while1(|v| v != ',' && v != '?' && v != '@'),465 str::to_owned,466 )(input)467 .map_err(err_to_string)?;468469 let kw_item = separated_pair(470 map(take_while1(|v| v != '&' && v != '='), str::to_owned),471 char('='),472 map(take_while1(|v| v != '&'), str::to_owned),473 );474 let kw = map(separated_list1(char('&'), kw_item), |vec| {475 vec.into_iter().collect::<BTreeMap<_, _>>()476 });477 let mut opt_kw = map(opt(preceded(char('?'), kw)), Option::unwrap_or_default);478479 let (input, attrs) = opt_kw(input).map_err(err_to_string)?;480481 if !input.is_empty() {482 return Err(format!("unexpected trailing input: {input:?}"));356 }483 }484 Ok(if is_tag {485 HostItem::Tag { name, attrs }486 } else {487 HostItem::Host { name, attrs }488 })357}489}358490359#[derive(Parser, Clone)]491#[derive(Parser, Clone)]360#[clap(group = ArgGroup::new("target_hosts"))]361pub struct FleetOpts {492pub struct FleetOpts {362 /// All hosts except those would be skipped493 /// All hosts except those would be skipped363 #[clap(long, number_of_values = 1, group = "target_hosts")]494 #[clap(long, number_of_values = 1, value_parser = host_item_parser)]364 only: Vec<String>,495 only: Vec<HostItem>,365496366 /// Hosts to skip497 /// Hosts to skip367 #[clap(long, number_of_values = 1, group = "target_hosts")]498 #[clap(long, number_of_values = 1)]368 skip: Vec<String>,499 skip: Vec<String>,369500370 /// Host, which should be threaten as current machine501 /// Host, which should be threaten as current machineflake.lockdiffbeforeafterboth7 ]7 ]8 },8 },9 "locked": {9 "locked": {10 "lastModified": 1720226507,10 "lastModified": 1721699339,11 "narHash": "sha256-yHVvNsgrpyNTXZBEokL8uyB2J6gB1wEx0KOJzoeZi1A=",11 "narHash": "sha256-UqtSwU13vpzzM6w8tGghEbA7ObM3NCDzSpz19QQo9XE=",12 "owner": "ipetkov",12 "owner": "ipetkov",13 "repo": "crane",13 "repo": "crane",14 "rev": "0aed560c5c0a61c9385bddff471a13036203e11c",14 "rev": "0081e9c447f3b70822c142908f08ceeb436982b8",15 "type": "github"15 "type": "github"16 },16 },17 "original": {17 "original": {40 },40 },41 "nixpkgs": {41 "nixpkgs": {42 "locked": {42 "locked": {43 "lastModified": 1720525988,43 "lastModified": 1721814637,44 "narHash": "sha256-6Vvrwl2rKrRt5gAYTFlM/pihCwHw8SY2o81TBm7KhIQ=",44 "narHash": "sha256-L3QkCvxeByJfW45wLkdZ9pL5h9PezOwwfx7G2sRfjiU=",45 "owner": "nixos",45 "owner": "nixos",46 "repo": "nixpkgs",46 "repo": "nixpkgs",47 "rev": "a630e7a8476e51b116f1ca7444dbad20701823d7",47 "rev": "e0c444a0b8413a31df199052f5714d409dc4c1d0",48 "type": "github"48 "type": "github"49 },49 },50 "original": {50 "original": {68 },68 },69 "nixpkgs-stable-for-tests": {69 "nixpkgs-stable-for-tests": {70 "locked": {70 "locked": {71 "lastModified": 1720386169,71 "lastModified": 1721548954,72 "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=",72 "narHash": "sha256-7cCC8+Tdq1+3OPyc3+gVo9dzUNkNIQfwSDJ2HSi2u3o=",73 "owner": "nixos",73 "owner": "nixos",74 "repo": "nixpkgs",74 "repo": "nixpkgs",75 "rev": "194846768975b7ad2c4988bdb82572c00222c0d7",75 "rev": "63d37ccd2d178d54e7fb691d7ec76000740ea24a",76 "type": "github"76 "type": "github"77 },77 },78 "original": {78 "original": {98 ]98 ]99 },99 },100 "locked": {100 "locked": {101 "lastModified": 1720491570,101 "lastModified": 1721810656,102 "narHash": "sha256-PHS2BcQ9kxBpu9GKlDg3uAlrX/ahQOoAiVmwGl6BjD4=",102 "narHash": "sha256-33UCMmgPL+sz06+iupNkl99hcBABP56ENcxSoKqr0TY=",103 "owner": "oxalica",103 "owner": "oxalica",104 "repo": "rust-overlay",104 "repo": "rust-overlay",105 "rev": "b970af40fdc4bd80fd764796c5f97c15e2b564eb",105 "rev": "a6afdaab4a47d6ecf647a74968e92a51c4a18e5a",106 "type": "github"106 "type": "github"107 },107 },108 "original": {108 "original": {