Skip to content

Commit 3627c6c

Browse files
committed
feat: nixos-install target
Signed-off-by: Lach <[email protected]>
1 parent 3972fee commit 3627c6c

File tree

6 files changed

+180
-78
lines changed

6 files changed

+180
-78
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmds/fleet/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ indicatif = { version = "0.17", optional = true }
4747
nix-eval.workspace = true
4848
nom = "7.1.3"
4949
fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" }
50-
indoc = "2.0.6"
5150

5251
[features]
5352
default = ["indicatif"]

cmds/fleet/src/cmds/build_systems.rs

+82-71
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, str::FromStr, time::Duration};
1+
use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf, time::Duration};
22

33
use anyhow::{anyhow, bail, Result};
44
use clap::{Parser, ValueEnum};
55
use fleet_base::{
6-
host::{Config, ConfigHost},
6+
host::{Config, ConfigHost, DeployKind},
77
opts::FleetOpts,
88
};
99
use itertools::Itertools as _;
@@ -131,6 +131,13 @@ async fn deploy_task(
131131
specialisation: Option<String>,
132132
disable_rollback: bool,
133133
) -> Result<()> {
134+
let deploy_kind = host.deploy_kind().await?;
135+
if deploy_kind == DeployKind::NixosInstall
136+
&& !matches!(action, DeployAction::Boot | DeployAction::Upload)
137+
{
138+
bail!("nixos-install deploy kind only supports boot and upload actions");
139+
}
140+
134141
let mut failed = false;
135142

136143
// TODO: Lockfile, to prevent concurrent system switch?
@@ -177,39 +184,74 @@ async fn deploy_task(
177184
}
178185
}
179186
}
180-
181-
if action.should_switch_profile() && !failed {
182-
info!("switching system profile generation");
183-
// It would also be possible to update profile atomically during copy:
184-
// https://github.com/NixOS/nix/pull/11657
185-
let mut cmd = host.cmd("nix").await?;
186-
cmd.arg("build");
187-
cmd.comparg("--profile", "/nix/var/nix/profiles/system");
188-
cmd.arg(&built);
189-
if let Err(e) = cmd.sudo().run_nix().await {
190-
error!("failed to switch system profile generation: {e}");
187+
if deploy_kind == DeployKind::NixosInstall {
188+
info!(
189+
"running nixos-install to switch profile, install bootloader, and perform activation"
190+
);
191+
let mut cmd = host.cmd("nixos-install").await?;
192+
cmd.arg("--system").arg(&built).args([
193+
// Channels here aren't fleet host system channels, but channels embedded in installation cd, which might be old.
194+
// It is possible to copy host channels, but I would prefer non-flake nix just to be unsupported.
195+
"--no-channel-copy",
196+
"--root",
197+
"/mnt",
198+
]);
199+
if let Err(e) = cmd.sudo().run().await {
200+
error!("failed to execute nixos-install: {e}");
191201
failed = true;
192202
}
193-
}
203+
} else {
204+
if action.should_switch_profile() && !failed {
205+
info!("switching system profile generation");
194206

195-
// FIXME: Connection might be disconnected after activation run
207+
// To avoid even more problems, using nixos-install for now.
208+
// // nix build is unable to work with --store argument for some reason, and nix until 2.26 didn't support copy with --profile argument,
209+
// // falling back to using nix-env command
210+
// // After stable NixOS starts using 2.26 - use `nix --store /mnt copy --from /mnt --profile ...` here, and instead of nix build below.
211+
// let mut cmd = host.cmd("nix-env").await?;
212+
// cmd.args([
213+
// "--store",
214+
// "/mnt",
215+
// "--profile",
216+
// "/mnt/nix/var/nix/profiles/system",
217+
// "--set",
218+
// ])
219+
// .arg(&built);
220+
// if let Err(e) = cmd.sudo().run_nix().await {
221+
// error!("failed to switch system profile generation: {e}");
222+
// failed = true;
223+
// }
224+
// It would also be possible to update profile atomically during copy:
225+
// https://github.com/NixOS/nix/pull/11657
226+
let mut cmd = host.nix_cmd().await?;
227+
cmd.arg("build");
228+
cmd.comparg("--profile", "/nix/var/nix/profiles/system");
229+
cmd.arg(&built);
230+
if let Err(e) = cmd.sudo().run_nix().await {
231+
error!("failed to switch system profile generation: {e}");
232+
failed = true;
233+
}
234+
}
196235

197-
if action.should_activate() && !failed {
198-
let _span = info_span!("activating").entered();
199-
info!("executing activation script");
200-
let specialised = if let Some(specialisation) = specialisation {
201-
let mut specialised = built.join("specialisation");
202-
specialised.push(specialisation);
203-
specialised
204-
} else {
205-
built.clone()
206-
};
207-
let switch_script = specialised.join("bin/switch-to-configuration");
208-
let mut cmd = host.cmd(switch_script).in_current_span().await?;
209-
cmd.arg(action.name().expect("upload.should_activate == false"));
210-
if let Err(e) = cmd.sudo().run().in_current_span().await {
211-
error!("failed to activate: {e}");
212-
failed = true;
236+
// FIXME: Connection might be disconnected after activation run
237+
238+
if action.should_activate() && !failed {
239+
let _span = info_span!("activating").entered();
240+
info!("executing activation script");
241+
let specialised = if let Some(specialisation) = specialisation {
242+
let mut specialised = built.join("specialisation");
243+
specialised.push(specialisation);
244+
specialised
245+
} else {
246+
built.clone()
247+
};
248+
let switch_script = specialised.join("bin/switch-to-configuration");
249+
let mut cmd = host.cmd(switch_script).in_current_span().await?;
250+
cmd.arg(action.name().expect("upload.should_activate == false"));
251+
if let Err(e) = cmd.sudo().run().in_current_span().await {
252+
error!("failed to activate: {e}");
253+
failed = true;
254+
}
213255
}
214256
}
215257
if action.should_create_rollback_marker() {
@@ -333,24 +375,6 @@ impl BuildSystems {
333375
}
334376
}
335377

336-
#[derive(Clone, PartialEq, Copy)]
337-
enum DeployKind {
338-
// NixOS => NixOS managed by fleet
339-
UpgradeToFleet,
340-
// NixOS managed by fleet => NixOS managed by fleet
341-
Fleet,
342-
}
343-
impl FromStr for DeployKind {
344-
type Err = anyhow::Error;
345-
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
346-
match s {
347-
"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),
348-
"fleet" => Ok(Self::Fleet),
349-
v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\""),
350-
}
351-
}
352-
}
353-
354378
impl Deploy {
355379
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
356380
let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
@@ -367,8 +391,9 @@ impl Deploy {
367391
let local_host = config.local_host();
368392
let opts = opts.clone();
369393
let batch = batch.clone();
370-
let mut deploy_kind: Option<DeployKind> =
371-
opts.action_attr(&host, "deploy_kind").await?;
394+
if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
395+
host.set_deploy_kind(deploy_kind);
396+
};
372397

373398
set.spawn_local(
374399
(async move {
@@ -381,28 +406,14 @@ impl Deploy {
381406
return;
382407
}
383408
};
384-
if deploy_kind == None {
385-
let is_fleet_managed = match host.file_exists("/etc/FLEET_HOST").await {
386-
Ok(v) => v,
387-
Err(e) => {
388-
error!("failed to query remote system kind: {}", e);
389-
return;
390-
},
391-
};
392-
if !is_fleet_managed {
393-
error!(indoc::indoc!{"
394-
host is not marked as managed by fleet
395-
if you're not trying to lustrate/install system from scratch,
396-
you should either
397-
1. manually create /etc/FLEET_HOST file on the target host,
398-
2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet
399-
3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos
400-
"});
409+
410+
let deploy_kind = match host.deploy_kind().await {
411+
Ok(v) => v,
412+
Err(e) => {
413+
error!("failed to query target deploy kind: {e}");
401414
return;
402415
}
403-
deploy_kind = Some(DeployKind::Fleet);
404-
}
405-
let deploy_kind = deploy_kind.expect("deploy_kind is set");
416+
};
406417

407418
// TODO: Make disable_rollback a host attribute instead
408419
let mut disable_rollback = self.disable_rollback;

crates/fleet-base/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ clap = { workspace = true, features = ["derive"] }
1313
fleet-shared.workspace = true
1414
futures = "0.3.30"
1515
hostname = "0.4.0"
16+
indoc = "2.0.6"
1617
itertools = "0.13.0"
1718
nix-eval.workspace = true
1819
nixlike.workspace = true

crates/fleet-base/src/host.rs

+87-4
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,35 @@ pub enum EscalationStrategy {
5858
Su,
5959
}
6060

61+
#[derive(Clone, PartialEq, Copy)]
62+
pub enum DeployKind {
63+
/// NixOS => NixOS managed by fleet
64+
UpgradeToFleet,
65+
/// NixOS managed by fleet => NixOS managed by fleet
66+
Fleet,
67+
/// Remote host has /mnt, /mnt/boot mounted,
68+
/// generated config is added to fleet configuration.
69+
NixosInstall,
70+
}
71+
72+
impl FromStr for DeployKind {
73+
type Err = anyhow::Error;
74+
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
75+
match s {
76+
"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),
77+
"fleet" => Ok(Self::Fleet),
78+
"nixos-install" => Ok(Self::NixosInstall),
79+
v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\""),
80+
}
81+
}
82+
}
6183
pub struct ConfigHost {
6284
config: Config,
6385
pub name: String,
6486
groups: OnceCell<Vec<String>>,
6587

88+
deploy_kind: OnceCell<DeployKind>,
89+
6690
pub host_config: Option<Value>,
6791
pub nixos_config: OnceCell<Value>,
6892
pub pkgs_override: Option<Value>,
@@ -73,6 +97,40 @@ pub struct ConfigHost {
7397
}
7498
// TODO: Move command helpers away with connectivity refactor
7599
impl ConfigHost {
100+
pub fn set_deploy_kind(&self, kind: DeployKind) {
101+
self.deploy_kind
102+
.set(kind)
103+
.ok()
104+
.expect("deploy kind is already set");
105+
}
106+
pub async fn deploy_kind(&self) -> Result<DeployKind> {
107+
if let Some(kind) = self.deploy_kind.get() {
108+
return Ok(kind.clone());
109+
}
110+
let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {
111+
Ok(v) => v,
112+
Err(e) => {
113+
bail!("failed to query remote system kind: {}", e);
114+
}
115+
};
116+
if !is_fleet_managed {
117+
bail!(indoc::indoc! {"
118+
host is not marked as managed by fleet
119+
if you're not trying to lustrate/install system from scratch,
120+
you should either
121+
1. manually create /etc/FLEET_HOST file on the target host,
122+
2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet
123+
3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos
124+
"});
125+
}
126+
// TOCTOU is possible
127+
let _ = self.deploy_kind.set(DeployKind::Fleet);
128+
Ok(self
129+
.deploy_kind
130+
.get()
131+
.expect("deploy kind is just set")
132+
.clone())
133+
}
76134
pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {
77135
// Prefer sudo, as run0 has some gotchas with polkit
78136
// and too many repeating prompts.
@@ -189,6 +247,16 @@ impl ConfigHost {
189247
Ok(MyCommand::new_on(escalation, cmd, session))
190248
}
191249
}
250+
pub async fn nix_cmd(&self) -> Result<MyCommand> {
251+
let mut nix = self.cmd("nix").await?;
252+
nix.args([
253+
"--extra-experimental-features",
254+
"nix-command",
255+
"--extra-experimental-features",
256+
"flakes",
257+
]);
258+
Ok(nix)
259+
}
192260

193261
pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {
194262
ensure!(data.encrypted, "secret is not encrypted");
@@ -231,10 +299,23 @@ impl ConfigHost {
231299
EscalationStrategy::Su,
232300
"nix",
233301
);
234-
nix.arg("copy")
235-
.arg("--substitute-on-destination")
236-
.comparg("--to", format!("ssh-ng://{}", self.name))
237-
.arg(path);
302+
nix.arg("copy").arg("--substitute-on-destination");
303+
304+
match self.deploy_kind().await? {
305+
DeployKind::Fleet | DeployKind::UpgradeToFleet => {
306+
nix.comparg("--to", format!("ssh-ng://{}", self.name));
307+
}
308+
DeployKind::NixosInstall => {
309+
nix
310+
// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon
311+
.arg("--no-check-sigs")
312+
.comparg(
313+
"--to",
314+
format!("ssh-ng://root@{}-install?remote-store=/mnt", self.name),
315+
);
316+
}
317+
}
318+
nix.arg(path);
238319
nix.run_nix().await.context("nix copy")?;
239320
Ok(path.to_owned())
240321
}
@@ -354,6 +435,7 @@ impl Config {
354435

355436
local: true,
356437
session: OnceLock::new(),
438+
deploy_kind: OnceCell::new(),
357439
}
358440
}
359441

@@ -372,6 +454,7 @@ impl Config {
372454
// TODO: Remove with connectivit refactor
373455
local: self.localhost == name,
374456
session: OnceLock::new(),
457+
deploy_kind: OnceCell::new(),
375458
})
376459
}
377460
pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {

modules/nixos/meta.nix

+9-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,13 @@ in
1313
];
1414

1515
# Version of environment (fleet scripts such as rollback) already installed on the host
16-
config.environment.etc.FLEET_HOST.text = "1";
16+
config = {
17+
environment.etc.FLEET_HOST.text = "1";
18+
19+
# Flake/nix command support is assumed by fleet, lets add it here to avoid potential problems.
20+
nix.settings.experimental-features = [
21+
"nix-command"
22+
"flakes"
23+
];
24+
};
1725
}

0 commit comments

Comments
 (0)