diff options
| -rw-r--r-- | checks.nix | 4 | ||||
| -rw-r--r-- | flake.lock | 23 | ||||
| -rw-r--r-- | flake.nix | 64 | ||||
| -rw-r--r-- | src/error.rs | 36 | ||||
| -rw-r--r-- | src/main.rs | 95 |
5 files changed, 203 insertions, 19 deletions
diff --git a/checks.nix b/checks.nix index 3e1b773..ec97a0c 100644 --- a/checks.nix +++ b/checks.nix @@ -236,8 +236,8 @@ let }).${keyPath} ''; - expected = builtins.toJSON expected; - }; + expected = builtins.toJSON expected; + }; in { # Verify that the nix evaluator test harness works. diff --git a/flake.lock b/flake.lock index 4cdf4b6..b61434b 100644 --- a/flake.lock +++ b/flake.lock @@ -34,7 +34,28 @@ "root": { "inputs": { "crane": "crane", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765939271, + "narHash": "sha256-7F/d+ZrTYyOxnBZcleQZjOOEWc1IMXR/CLLRLLsVtHo=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8028944c1339469124639da276d403d8ab7929a8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index c101218..1666ae6 100644 --- a/flake.nix +++ b/flake.nix @@ -14,17 +14,72 @@ ref = "nixos-25.05"; }; + # Crane is a library used to manage Rust builds from nix. + # + # It doesn't take any inputs itself (the mkLib call, below, uses + # dependency injection, which removes that need), so we don't have to set + # up any "follows" relations. crane = { type = "github"; owner = "ipetkov"; repo = "crane"; }; + + # The Rust overlay is a nix "overlay" which provides different versions of + # the Rust compiler toolchain. We need a feature from the nightly versions + # of the Rust compiler, so we can't use the stable builds that nixpkgs + # provides. There are quite a few projects out there that package Rust + # toolchains for nix, but this one interacts well with Crane, so it's the + # one we use. + # + # This one does take nixpkgs as a flake input, so we set up the "follows" + # relation to avoid having two distinct versions of nixpkgs in use, which + # would be expensive at best and cause version-skew errors at worst. + rust-overlay = { + type = "github"; + owner = "oxalica"; + repo = "rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, crane }: + outputs = { self, nixpkgs, crane, rust-overlay }: let supportedSystems = [ "aarch64-linux" "x86_64-linux" ]; forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + + nixpkgsFor = forAllSystems (system: import nixpkgs { + inherit system; + + # We apply the Rust overlay to the entire package set, but it will + # use a stable version of Rust for most packages, except where we + # explicitly override that behavior. + # + # This may result in increased build times since the upstream cache of + # pre-built nix packages is less likely to have things. Unfortunately, + # there's not a convenient or concise way to use an overlay + # selectively; this is one reason to not use overlays when more modern + # styles of nix architecture are available. There just isn't a way to + # avoid that with our requirements right now. + # + # Hopefully, we can remove the overlay (but keep Crane) when the + # nightly features we use have all reached stable status. Please try + # hard to avoid adding dependencies on new nightly-only Rust features. + overlays = [ (import rust-overlay) ]; + }); + + # Since we rely on a Rust nightly feature (see the features annotation + # at the top of main.rs), we need to invoke Crane with a nightly version + # of the Rust compiler. There are many possible ways to achieve that, + # most with roughly the same result. + # + # One thing to note is that, by using Crane's overrideToolchain feature + # here, we make sure we're only using nightly Rust for our own package. + # Using it for the entire distro would be easy to do by accident, and + # would be terrible for build times and upgrade reliability. + toolchainFor = pkgs: pkgs.rust-bin.selectLatestNightlyWith + (toolchain: toolchain.default); + craneLibFor = pkgs: (crane.mkLib pkgs).overrideToolchain toolchainFor; + in { nixosModules.default = { ... }: { imports = [ @@ -36,7 +91,7 @@ }; packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { - default = (crane.mkLib pkgs).buildPackage { + default = (craneLibFor pkgs).buildPackage { src = ./.; }; }); @@ -44,8 +99,7 @@ devShells = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - cargo - rustc + (toolchainFor pkgs) ]; }; }); diff --git a/src/error.rs b/src/error.rs index 86e7928..199a0f4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,15 @@ #![forbid(unsafe_code)] +use std::fmt::{ Display, Formatter }; pub type Result<T> = std::result::Result<T, Error>; #[derive(Debug)] pub enum Error { - IO(std::io::Error), + CommandLine(::clap::error::Error), Internal(String), + IO(std::io::Error), + UTF8, } impl From<std::io::Error> for Error { @@ -15,3 +18,34 @@ impl From<std::io::Error> for Error { } } +impl From<::clap::error::Error> for Error { + fn from(value: ::clap::error::Error) -> Self { + Error::CommandLine(value) + } +} + +impl From<std::string::FromUtf8Error> for Error { + fn from(_value: std::string::FromUtf8Error) -> Self { + Error::UTF8 + } +} + +impl Display for Error { + fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::CommandLine(error) => { + write!(fmt, "{}", error.to_string()) + }, + Error::Internal(string) => { + write!(fmt, "{}", string) + }, + Error::IO(error) => { + write!(fmt, "{}", error.to_string()) + }, + Error::UTF8 => { + write!(fmt, "Invalid UTF8 received") + }, + } + } +} + diff --git a/src/main.rs b/src/main.rs index 80645df..2ebc97c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,39 @@ #![forbid(unsafe_code)] +#![feature(gethostname)] use crate::commands::{ Command, CommandMap, Mode }; use crate::error::*; use std::io::Write; +use std::process; + +use ::clap::Parser; mod commands; mod error; +#[derive(Parser, Debug)] +struct CommandLineParameters { + flake_path: String, + + #[arg(long = "store")] + nix_store_path: Option<String>, +} + + struct TopLevelMode { + flake_path: String, + configuration_name: String, command_map: Box<CommandMap<TopLevelMode>>, } impl TopLevelMode { - fn new() -> Result<Self> { + fn new(flake_path: String, configuration_name: String) -> Result<Self> { let mut command_map = Box::new(CommandMap::new()); command_map.insert(Command::new("help", |mode, _params| { + println!("list List all defined secrets."); + println!(""); println!("help Print this message."); println!("quit Exit the program."); @@ -27,7 +44,15 @@ impl TopLevelMode { Ok(None) }))?; - Ok(TopLevelMode { command_map }) + command_map.insert(Command::<TopLevelMode>::new("list", |mode, _params| { + let _secrets = read_secret_metadata(&mode.flake_path, + &mode.configuration_name)?; + // TODO do something with them + + Ok(Some(mode)) + }))?; + + Ok(TopLevelMode { flake_path, configuration_name, command_map }) } } @@ -63,11 +88,26 @@ fn default_empty_command() -> Result<()> { } -fn main() -> Result<()> { - help(); +fn main() -> () { + match main_h() { + Ok(()) => { }, + Err(error) => { + println!("{}", error.to_string()); + }, + } +} + + +fn main_h() -> Result<()> { + let options = CommandLineParameters::try_parse()?; + println!("{:?}", options); + + let hostname: String = std::net::hostname()? + .into_string().map_err(|_| Error::UTF8)?; let mut current_mode: Option<Box<dyn Mode>> - = Some(Box::new(TopLevelMode::new()?)); + = Some(Box::new(TopLevelMode::new(options.flake_path.clone(), + hostname)?)); while let Some(mode) = current_mode { if let Some(input) = prompt(mode.prompt_text()?)? { @@ -83,11 +123,6 @@ fn main() -> Result<()> { } -fn help() { - println!("Type a command (there are none yet)"); -} - - fn prompt(raw: &str) -> Result<Option<String>> { let mut stdout = std::io::stdout(); stdout.write_all(format!("\n{} ", raw).as_bytes())?; @@ -96,3 +131,43 @@ fn prompt(raw: &str) -> Result<Option<String>> { Ok(std::io::stdin().lines().next().transpose()?) } + +fn read_secret_metadata(flake_path: &str, configuration_name: &str) + -> Result<()> +{ + let mut nix_expression = String::new(); + nix_expression.push_str("let config = (builtins.getFlake \""); + nix_expression.push_str(flake_path); + nix_expression.push_str("\")"); + nix_expression.push_str(".nixosConfigurations."); + nix_expression.push_str(configuration_name); + nix_expression.push_str(".config; in "); + nix_expression.push_str("if config ? \"secrets\" "); + nix_expression.push_str("then config.secrets.export "); + nix_expression.push_str("else { }"); + + let nix_output = process::Command::new("nix") + .env_clear() + .args([ + "--extra-experimental-features", + "nix-command", + "eval", + "--impure", + "--json", + "--expr", + &nix_expression, + ]) + .output()?; + if !nix_output.status.success() { + println!("{}", String::from_utf8(nix_output.stderr)?); + return Err(Error::Internal("nix subprocess failed".to_string())); + } + + let nix_json = String::from_utf8(nix_output.stdout)?; + + println!("{}", nix_json); + // TODO parse the JSON + + Ok(()) +} + |