Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

client: fix seeding

+175 -140
+19 -16
.vscode/launch.json
··· 1 1 { 2 - // Use IntelliSense to learn about possible attributes. 3 - // Hover to view descriptions of existing attributes. 4 - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 2 "version": "0.2.0", 6 3 "configurations": [ 7 4 { 8 5 "type": "lldb", 9 6 "request": "launch", 7 + "name": "dap-sower-seed", 8 + "program": "${workspaceFolder}/target/debug/sower", 9 + "args": [ 10 + "seed", 11 + "download", 12 + "--url=http://localhost:4000", 13 + "--bootstrap-token-file=${workspaceFolder}/.bootstrap.token" 14 + ], 15 + "cwd": "${workspaceFolder}" 16 + }, 17 + { 18 + "type": "lldb", 19 + "request": "launch", 10 20 "name": "Debug executable 'sower'", 11 21 "cargo": { 12 - "args": [ 13 - "build", 14 - "--bin=sower", 15 - "--package=sower" 16 - ], 22 + "args": ["build", "--bin=sower", "--package=sower"], 17 23 "filter": { 18 24 "name": "sower", 19 25 "kind": "bin" 20 26 } 21 27 }, 22 28 "args": [ 23 - "tree", 24 - "daemon" 29 + "seed", 30 + "download", 31 + "--url=http://localhost:4000", 32 + "--bootstrap-token-file=${workspaceFolder}/.bootstrap.token" 25 33 ], 26 34 "cwd": "${workspaceFolder}" 27 35 }, ··· 30 38 "request": "launch", 31 39 "name": "Debug unit tests in executable 'sower'", 32 40 "cargo": { 33 - "args": [ 34 - "test", 35 - "--no-run", 36 - "--bin=sower", 37 - "--package=sower" 38 - ], 41 + "args": ["test", "--no-run", "--bin=sower", "--package=sower"], 39 42 "filter": { 40 43 "name": "sower", 41 44 "kind": "bin"
+33 -31
client/src/main.rs
··· 6 6 use std::fs; 7 7 use std::path::{Path, PathBuf}; 8 8 use tracing::{debug, info}; 9 - use xdg; 10 9 11 10 mod sower; 12 11 use sower::daemon::Daemon; ··· 112 111 113 112 impl Config { 114 113 pub fn bootstrap_token(self) -> Self { 115 - let bootstrap_token = fs::read_to_string(self.bootstrap_token_file.clone().unwrap()).ok(); 116 - if let Some(_) = &bootstrap_token { 114 + if let Some(token_path) = &self.bootstrap_token_file { 115 + let bootstrap_token = fs::read_to_string(token_path).ok(); 117 116 Self { 118 117 bootstrap_token, 119 118 ..self ··· 221 220 } 222 221 223 222 Actions::Seed { action } => { 224 - let seed = tree.latest_seed().await; 223 + dbg!("{}", &tree); 224 + let seed = tree 225 + .seeds 226 + .expect("No seeds loaded into tree") 227 + .desired 228 + .expect("Could not find desired seed"); 225 229 226 230 match action { 227 231 SeedCommands::Activate { mode, .. } => { 228 232 let mode = mode.clone().or(config.mode); 229 - seed.unwrap().activate(mode).expect("failed to activate"); 233 + seed.activate(mode).expect("failed to activate"); 230 234 } 231 235 232 236 SeedCommands::Download {} => { 233 - seed.unwrap().realize().expect("failed to realize"); 237 + seed.realize().expect("failed to realize"); 234 238 } 235 239 } 236 240 } 237 241 238 - Actions::Tree { action } => { 239 - let tree = tree.load_latest().await; 240 - 241 - match action { 242 - TreeCommands::Info {} => info!("{:?}", tree), 243 - 244 - TreeCommands::Reboot { yes } => { 245 - tree.info(); 246 - tree.reboot(yes.clone()) 247 - } 242 + Actions::Tree { action } => match action { 243 + TreeCommands::Info {} => info!("{:?}", tree), 248 244 249 - TreeCommands::Upgrade { mode, reboot, yes } => { 250 - debug!("{:?}", tree); 245 + TreeCommands::Reboot { yes } => { 246 + tree.info(); 247 + tree.reboot(*yes) 248 + } 251 249 252 - let mode = mode.clone().or(config.mode); 253 - let desired = tree.seeds.clone().unwrap().desired.unwrap(); 254 - info!("Activating seed {:?}", &desired); 255 - desired 256 - .realize() 257 - .expect("failed to realize") 258 - .activate(mode) 259 - .expect("failed to activate"); 250 + TreeCommands::Upgrade { mode, reboot, yes } => { 251 + debug!("{:?}", tree); 260 252 261 - let tree = tree.load_seeds(); 262 - debug!("{:?}", tree); 253 + let mode = mode.clone().or(config.mode); 254 + let desired = tree.seeds.clone().unwrap().desired; 255 + match desired { 256 + Some(desired) => { 257 + info!("Activating seed {:?}", &desired); 258 + desired 259 + .realize() 260 + .expect("failed to realize") 261 + .activate(mode) 262 + .expect("failed to activate"); 263 263 264 - if config.reboot.unwrap_or(false) || reboot.clone() { 265 - tree.reboot(yes.clone()); 264 + if config.reboot.unwrap_or(false) || *reboot { 265 + tree.reboot(*yes); 266 + } 266 267 } 268 + None => panic!("No desired seed found"), 267 269 } 268 270 } 269 - } 271 + }, 270 272 } 271 273 272 274 Ok(())
+55 -59
client/src/sower.rs
··· 2 2 3 3 pub mod daemon; 4 4 5 + use anyhow::{anyhow, Result}; 5 6 use clap::ValueEnum; 6 7 use serde::Deserialize; 7 8 use serde::Serialize; ··· 16 17 pub struct Seed { 17 18 pub id: Option<String>, 18 19 pub name: String, 19 - #[serde(rename(deserialize = "type"))] 20 20 pub seed_type: SeedType, 21 21 pub out_path: String, 22 22 } 23 23 24 24 impl Seed { 25 - pub fn realize(&self) -> Result<&Self, String> { 25 + pub fn realize(&self) -> Result<&Self> { 26 26 match run_command("nix-store", vec!["--realize", &self.out_path.clone()]) { 27 27 true => Ok(self), 28 - false => Err(format!("failed to realize: {}", &self.out_path)), 28 + false => Err(anyhow!("{}", &self.out_path)), 29 29 } 30 30 } 31 31 ··· 71 71 } 72 72 } 73 73 74 - fn new_from_path(name: String, seed_type: SeedType, path: &str) -> Self { 74 + fn new_from_path(name: String, seed_type: SeedType, path: &str) -> Result<Self> { 75 75 debug!(path); 76 - let path = fs::canonicalize(Path::new(path)).expect("unable to read link"); 77 - Self { 78 - id: None, 79 - name, 80 - seed_type, 81 - out_path: path.to_string_lossy().to_string(), 76 + match fs::canonicalize(Path::new(path)) { 77 + Ok(path) => Ok(Self { 78 + id: None, 79 + name, 80 + seed_type, 81 + out_path: path.to_string_lossy().to_string(), 82 + }), 83 + Err(e) => Err(e.into()), 82 84 } 83 85 } 84 86 } ··· 128 130 } 129 131 130 132 impl Sower { 131 - pub fn new(config: &Config) -> Result<Sower, Box<dyn std::error::Error>> { 133 + pub fn new(config: &Config) -> Result<Sower> { 132 134 let url = config.url.clone().expect("URL is required"); 133 135 let api_url = format!("{}/api", url); 134 136 let channels_url = format!("{}/client/websocket", url.replace("http", "ws")); ··· 150 152 .await 151 153 { 152 154 Ok(result) => { 153 - if let Ok(seed) = result.json::<Seed>().await { 154 - Some(seed) 155 - } else { 156 - None 155 + debug!("{:?}", result); 156 + match result.json::<Seed>().await { 157 + Ok(seed) => Some(seed), 158 + Err(err) => { 159 + dbg!(err); 160 + None 161 + } 157 162 } 158 163 } 159 - Err(_) => None, 164 + Err(err) => { 165 + debug!("Err: {}", err); 166 + None 167 + } 160 168 } 161 169 } 162 170 } ··· 176 184 pub current: Option<Seed>, 177 185 pub booted: Option<Seed>, 178 186 pub desired: Option<Seed>, 179 - pub profile: Seed, 187 + pub profile: Option<Seed>, 180 188 } 181 189 182 190 impl Tree { 183 - pub async fn new(config: &Config) -> Result<Tree, Box<dyn std::error::Error>> { 191 + pub async fn new(config: &Config) -> Result<Tree> { 184 192 let name = 185 193 config 186 194 .name ··· 195 203 let seed_type = config.seed_type.unwrap(); 196 204 let sower = Sower::new(&config)?; 197 205 198 - Ok(Tree { 206 + let mut tree = Tree { 199 207 name: name.clone(), 200 208 seed_type, 201 209 sower: Some(sower.clone()), 202 210 seeds: None, 203 211 id: None, 204 212 server_id: None, 205 - } 206 - .load_seeds()) 207 - } 213 + }; 208 214 209 - pub fn info(&self) -> () { 210 - dbg!(self); 211 - () 212 - } 215 + tree.load_seeds().await?; 213 216 214 - pub async fn latest_seed(&self) -> Option<Seed> { 215 - self.sower 216 - .as_ref() 217 - .unwrap() 218 - .find_seed(self.name.clone(), self.seed_type.clone()) 219 - .await 217 + Ok(tree) 220 218 } 221 219 222 - pub async fn load_latest(mut self) -> Self { 223 - self.seeds = Some(TreeSeeds { 224 - desired: self.latest_seed().await, 225 - ..self.seeds.unwrap() 226 - }); 227 - self 220 + pub fn info(&self) { 221 + dbg!(self); 228 222 } 229 223 230 - pub fn load_seeds(mut self) -> Self { 224 + pub async fn load_seeds(&mut self) -> Result<()> { 231 225 let booted = match self.seed_type { 232 - SeedType::Nixos => Some(Seed::new_from_path( 233 - self.name.clone(), 234 - self.seed_type.clone(), 235 - "/run/booted-system", 236 - )), 226 + SeedType::Nixos => { 227 + Seed::new_from_path(self.name.clone(), self.seed_type, "/run/booted-system").ok() 228 + } 237 229 SeedType::HomeManager => None, 238 230 SeedType::NixDarwin => None, 239 231 }; 240 232 241 233 let current = match self.seed_type { 242 234 SeedType::HomeManager => None, 243 - SeedType::NixDarwin => Some(Seed::new_from_path( 244 - self.name.clone(), 245 - self.seed_type.clone(), 246 - "/run/current-system", 247 - )), 248 - SeedType::Nixos => Some(Seed::new_from_path( 249 - self.name.clone(), 250 - self.seed_type.clone(), 251 - "/run/current-system", 252 - )), 235 + SeedType::NixDarwin => { 236 + Seed::new_from_path(self.name.clone(), self.seed_type, "/run/current-system").ok() 237 + } 238 + SeedType::Nixos => { 239 + Seed::new_from_path(self.name.clone(), self.seed_type, "/run/current-system").ok() 240 + } 253 241 }; 254 242 255 243 let profile = Seed::new_from_path( 256 244 self.name.clone(), 257 - self.seed_type.clone(), 245 + self.seed_type, 258 246 &self.seed_type.profile_path(), 259 - ); 247 + ) 248 + .ok(); 249 + 250 + let desired = self 251 + .sower 252 + .as_ref() 253 + .unwrap() 254 + .find_seed(self.name.clone(), self.seed_type) 255 + .await; 260 256 261 257 self.seeds = Some(TreeSeeds { 262 258 booted, 263 259 current, 264 260 profile, 265 - desired: None, 261 + desired, 266 262 }); 267 263 268 - self 264 + Ok(()) 269 265 } 270 266 271 267 pub fn reboot(&self, confirm: bool) { ··· 289 285 Self::run_reboot() 290 286 } 291 287 292 - fn reboot_needed() -> std::io::Result<bool> { 288 + fn reboot_needed() -> Result<bool> { 293 289 let profile_paths = &["", "/initrd", "/kernel", "/kernel-modules"]; 294 290 let result = profile_paths.iter().any(|&path| { 295 291 let profile_path = format!("/nix/var/nix/profiles/system{}", path);
+8 -2
client/src/sower/daemon.rs
··· 35 35 &tree.sower.clone().unwrap().channels_url, 36 36 &[( 37 37 "token", 38 - Self::sign_login_jwt(config.bootstrap_token.clone().unwrap(), &tree).unwrap(), 38 + Self::sign_login_jwt( 39 + config 40 + .bootstrap_token 41 + .clone() 42 + .expect("No bootstrap token found"), 43 + &tree, 44 + ) 45 + .unwrap(), 39 46 )], 40 47 ) 41 48 .unwrap(); ··· 61 68 } 62 69 63 70 pub async fn run(&mut self) -> Result<(), anyhow::Error> { 64 - //self.login().await; 65 71 let (private_channel_tx, mut private_channel_rx) = mpsc::channel(1); 66 72 let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel(); 67 73
+2
flake.nix
··· 68 68 pkgs.cargo 69 69 pkgs.cargo-watch 70 70 pkgs.clippy 71 + pkgs.lldb 72 + pkgs.llvm 71 73 pkgs.rust-analyzer 72 74 pkgs.rustc 73 75 pkgs.rustfmt
+8 -1
nix/nixos-client.nix
··· 16 16 17 17 autoreboot = lib.mkEnableOption "automatic rebooting"; 18 18 19 - package = lib.mkOption { type = lib.types.package; }; 19 + credentials = lib.mkOption { 20 + type = lib.types.listOf lib.types.str; 21 + description = "systemd credentials"; 22 + default = [ ]; 23 + }; 20 24 21 25 onCalendar = lib.mkOption { 22 26 type = lib.types.str; 23 27 description = "OnCalendar for systemd timer on linux. See https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html#Calendar%20Events"; 24 28 default = "daily"; 25 29 }; 30 + 31 + package = lib.mkOption { type = lib.types.package; }; 26 32 27 33 settings = lib.mkOption { 28 34 type = lib.types.submodule { ··· 68 74 serviceConfig = { 69 75 ExecStart = "${lib.getExe cfg.package} tree upgrade ${lib.optionalString cfg.autoreboot "--yes"}"; 70 76 Type = "oneshot"; 77 + LoadCredential = cfg.credentials; 71 78 }; 72 79 }; 73 80
+9 -1
nix/nixos-server.nix
··· 17 17 default = pkgs.callPackage ./server-package.nix { }; 18 18 }; 19 19 20 + credentials = lib.mkOption { 21 + type = lib.types.listOf lib.types.str; 22 + description = "systemd credentials"; 23 + default = [ ]; 24 + }; 25 + 20 26 environment = lib.mkOption { 21 27 type = lib.types.attrsOf lib.types.str; 22 28 description = "environment variables to pass to service. Do not set secrets here, but instead use systemd credentials"; ··· 48 54 { 49 55 Type = "notify"; 50 56 WatchdogSec = "10s"; 51 - Restart = "on-failure"; 57 + Restart = lib.mkDefault "on-failure"; 52 58 53 59 DynamicUser = true; 54 60 StateDirectory = "sower"; ··· 63 69 exec ${cfg.package}/bin/sower start 64 70 ''; 65 71 ExecStop = "${cfg.package}/bin/sower stop"; 72 + 73 + LoadCredential = cfg.credentials; 66 74 } 67 75 (lib.optionalAttrs cfg.initSecrets { 68 76 LoadCredential = [
+40 -29
nix/test-end-to-end.nix
··· 12 12 testers.runNixOSTest { 13 13 name = "sower"; 14 14 15 - nodes.server = { 16 - imports = [ ./nixos-module.nix ]; 15 + nodes.server = 16 + { pkgs, ... }: 17 + { 18 + imports = [ ./nixos-module.nix ]; 17 19 18 - config = { 19 - environment.systemPackages = [ curl ]; 20 + config = { 21 + environment.systemPackages = [ curl ]; 20 22 21 - services.sower.client = { 22 - enable = true; 23 - package = client; 24 - settings = { 25 - url = "http://localhost:4000"; 26 - mode = "dry-activate"; 23 + services.sower.client = { 24 + enable = true; 25 + package = client; 26 + credentials = [ "SOWER_BOOTSTRAP_TOKEN_FILE:${pkgs.writeText "token" "aninsecuretoken"}" ]; 27 + settings = { 28 + url = "http://localhost:4000"; 29 + mode = "dry-activate"; 30 + bootstrap_token_file = "${pkgs.writeText "token" "aninsecuretoken"}"; 31 + }; 27 32 }; 28 - }; 29 33 30 - services.sower.server = { 31 - enable = true; 32 - environment = { 33 - SOWER_DATABASE_SOCKET = "/run/postgresql/.s.PGSQL.5432"; 34 - SOWER_HOSTNAME = "localhost"; 35 - SOWER_PUBLIC_PORT = "4000"; 36 - SOWER_PUBLIC_SCHEME = "http"; 34 + services.sower.server = { 35 + enable = true; 36 + credentials = [ 37 + "SOWER_BOOTSTRAP_TOKEN_FILE:${pkgs.writeText "token" "aninsecuretoken"}" 38 + "SOWER_AUTH_OIDC_CLIENT_ID_FILE:${pkgs.writeText "oidc-id" "ok"}" 39 + "SOWER_AUTH_OIDC_CLIENT_SECRET_FILE:${pkgs.writeText "oidc-secret" "ok"}" 40 + ]; 41 + environment = { 42 + SOWER_DATABASE_SOCKET = "/run/postgresql/.s.PGSQL.5432"; 43 + SOWER_HOSTNAME = "localhost"; 44 + SOWER_PUBLIC_PORT = "4000"; 45 + SOWER_PUBLIC_SCHEME = "http"; 46 + SOWER_AUTH_OIDC_BASE_URL = "http://localhost:9000"; 47 + }; 37 48 }; 38 - }; 49 + systemd.services.sower.serviceConfig.Restart = "no"; 39 50 40 - services.postgresql = { 41 - enable = true; 42 - ensureUsers = [ 43 - { 44 - name = "sower"; 45 - ensureDBOwnership = true; 46 - } 47 - ]; 48 - ensureDatabases = [ "sower" ]; 51 + services.postgresql = { 52 + enable = true; 53 + ensureUsers = [ 54 + { 55 + name = "sower"; 56 + ensureDBOwnership = true; 57 + } 58 + ]; 59 + ensureDatabases = [ "sower" ]; 60 + }; 49 61 }; 50 62 }; 51 - }; 52 63 53 64 testScript = '' 54 65 start_all()
+1 -1
priv/repo/migrations/20240524170006_rename_seed_type.exs priv/repo/migrations/20240524170006_rename_tree_seed_type.exs
··· 1 - defmodule Sower.Repo.Migrations.RenameSeedType do 1 + defmodule Sower.Repo.Migrations.RenameTreeSeedType do 2 2 @moduledoc """ 3 3 Updates resources based on their most recent snapshots. 4 4