summary refs log tree commit diff
diff options
context:
space:
mode:
authorIrene Knapp <ireneista@internetsafetylabs.org>2025-12-17 15:16:20 -0800
committerIrene Knapp <ireneista@internetsafetylabs.org>2025-12-18 13:37:52 -0800
commitd15feffcdb262f5e4686297e156319591895a945 (patch)
tree9bf3fd01d8885c0cfba7fec810dca4bb28bf0552
parent01b7b60dc260aa7cf1dab6c15db13a88d5d92ada (diff)
implement the secret-list command
Change-Id: I5e1570940fedf52bb560fd824270e201757004ed
-rw-r--r--checks.nix4
-rw-r--r--flake.lock23
-rw-r--r--flake.nix64
-rw-r--r--src/error.rs36
-rw-r--r--src/main.rs95
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(())
+}
+