Skip to content

Commit

Permalink
feat: add CLI for secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
paizin authored and htngr committed Feb 5, 2025
1 parent 711fe13 commit 9ca51e0
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 38 deletions.
52 changes: 51 additions & 1 deletion codchi/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub use self::module::*;
pub use self::secrets::*;

use clap::builder::*;
use clap::*;
Expand Down Expand Up @@ -228,6 +229,20 @@ cargo build
module_paths: Vec<ModuleAttrPath>,
},

#[command(subcommand)]
#[clap(
about = "Manage secrets of code machines.",
long_about = r#"
Sometimes there is the need for user-specific variables and / or secrets, which should not be
included inside the .nix files inside a git repository, but they're still needed for the
development environment. An example is a GitHub / GitLab token which is used to automatically
setup mail and username for Git's config.
See <https://codchi.dev/config/secrets> for more information.
"#
)]
Secrets(SecretsCmd),

#[command(subcommand)]
#[clap(
aliases = &["mod"],
Expand Down Expand Up @@ -476,7 +491,9 @@ See the following docs on how to register the completions with your shell:
#[clap(hide = true)]
Tray {},

#[clap(about = "Export the file system of a code machine including NixOS configuration and codchi secrets.")]
#[clap(
about = "Export the file system of a code machine including NixOS configuration and codchi secrets."
)]
Tar {
/// Name of the code machine
name: String,
Expand All @@ -490,6 +507,39 @@ See the following docs on how to register the completions with your shell:
Store(StoreCmd),
}

mod secrets {
use super::*;

#[derive(Debug, Subcommand, Clone)]
pub enum SecretsCmd {
/// Lists secrets of a code machine
#[clap(aliases = &["ls"])]
List {
/// Name of the code machine
name: String,
},

/// Get the current value of a secret.
Get {
/// Name of the code machine
machine_name: String,

/// Name of the secret
secret_name: String,
},

/// Change the value of an existing secret. The machine needs to be restarted after
/// modifying secrets.
Set {
/// Name of the code machine
machine_name: String,

/// Name of the secret
secret_name: String,
},
}
}

mod module {
use super::*;
use core::fmt;
Expand Down
6 changes: 0 additions & 6 deletions codchi/src/config/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,4 @@ impl MachineConfig {

pub type CodchiModule = FlakeUrl<Required>;

#[derive(Clone, Debug, Deserialize)]
pub struct EnvSecret {
pub name: String,
pub description: String,
}

pub type MachineModules = HashMap<ModuleName, CodchiModule>;
35 changes: 32 additions & 3 deletions codchi/src/logging/output.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::{fmt::Display, io::stdout};

use crate::config::{MachineModules, MachineStatus, Mod, ModLsOutput, StatusOutput};
use crate::platform::{ConfigStatus, Machine};
use crate::{
config::{MachineModules, MachineStatus, Mod, ModLsOutput, StatusOutput},
secrets::EnvSecret,
};
use itertools::Itertools;
use serde::Serialize;

use crate::platform::{ConfigStatus, Machine};

pub trait CodchiOutput<A: Serialize> {
fn to_output(&self) -> A;
fn human_output(x: A) -> impl Display;
Expand Down Expand Up @@ -91,3 +93,30 @@ impl CodchiOutput<ModLsOutput> for MachineModules {
table
}
}

impl CodchiOutput<Vec<EnvSecret>> for Vec<EnvSecret> {
fn to_output(&self) -> Vec<EnvSecret> {
self.clone()
}

fn human_output(out: Vec<EnvSecret>) -> impl Display {
use comfy_table::*;

let mut table = Table::new();
table.load_preset(presets::UTF8_FULL).set_header(vec![
Cell::new("Name"),
Cell::new("Description"),
Cell::new("Value"),
]);

for secret in out {
table.add_row(vec![
Cell::new(&secret.name),
Cell::new(&secret.description.trim()),
Cell::new(&secret.value.unwrap_or_default()),
]);
}

table
}
}
90 changes: 86 additions & 4 deletions codchi/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ use crate::{
use clap::{CommandFactory, Parser};
use config::{git_url::GitUrl, CodchiConfig, MachineConfig};
use console::style;
use itertools::Itertools;
use log::Level;
use logging::{set_progress_status, CodchiOutput};
use platform::{store_debug_shell, ConfigStatus, Host, MachineDriver};
use logging::{hide_progress, set_progress_status, CodchiOutput};
use platform::{store_debug_shell, ConfigStatus, Host, MachineDriver, PlatformStatus};
use secrets::MachineSecrets;
use std::{
env,
io::IsTerminal,
panic::{self, PanicInfo},
process::exit,
sync::{mpsc::channel, OnceLock},
Expand All @@ -28,6 +31,7 @@ pub mod consts;
pub mod logging;
pub mod module;
pub mod platform;
pub mod secrets;
pub mod tray;
pub mod util;

Expand Down Expand Up @@ -151,6 +155,7 @@ Thank you kindly!"#
// all other commands
match &cli.command.unwrap_or(Cmd::Status {}) {
Cmd::Status {} => Machine::list(true)?.print(cli.json),

Cmd::Init {
machine_name,
url,
Expand All @@ -177,6 +182,7 @@ Thank you kindly!"#
})()
.inspect_err(|_| interrupt_machine_creation(machine_name))?;
}

Cmd::Clone {
machine_name,
dir,
Expand Down Expand Up @@ -211,24 +217,95 @@ Thank you kindly!"#
"Machine '{machine_name}' is ready! Use `codchi exec {machine_name}` to start it."
);
}

Cmd::Rebuild { no_update, name } => {
Machine::by_name(name, true)?.build(*no_update)?;
log::info!("Machine {name} rebuilt successfully!");
}

Cmd::Exec { name, cmd } => Machine::by_name(name, true)?.exec(cmd)?,

Cmd::Delete {
name,
i_am_really_sure,
} => Machine::by_name(name, true)?.delete(*i_am_really_sure)?,

Cmd::Duplicate {
source_name,
target_name,
} => Machine::by_name(source_name, true)?.duplicate(target_name)?,

Cmd::Secrets(cmd) => match cmd {
cli::SecretsCmd::List { name } => {
let secrets = progress_scope! {
set_progress_status("Evaluating secrets...");
Machine::by_name(name, true)?.eval_env_secrets()
}?;
secrets.into_values().collect_vec().print(cli.json);
}
cli::SecretsCmd::Get {
machine_name,
secret_name,
} => {
let (_, cfg) = MachineConfig::open_existing(machine_name, false)?;
match cfg.secrets.get(secret_name) {
Some(value) => {
println!("{value}");
}
None => {
anyhow::bail!(
"Machine '{machine_name}' has no secret named '{secret_name}'."
);
}
}
}
cli::SecretsCmd::Set {
machine_name,
secret_name,
} => {
set_progress_status("Evaluating secrets...");
let machine = Machine::by_name(machine_name, true)?;
let secrets = machine.eval_env_secrets()?;
hide_progress();
match secrets.get(secret_name) {
Some(secret) => {
let (lock, mut cfg) = MachineConfig::open_existing(machine_name, true)?;
let value = secret.prompt_value()?;
cfg.secrets.insert(secret_name.clone(), value);
cfg.write(lock)?;
}
None => {
anyhow::bail!(
"Machine '{machine_name}' has no secret named '{secret_name}'."
);
}
}
if machine.platform_status == PlatformStatus::Running {
if std::io::stdin().is_terminal()
&& inquire::Confirm::new(&format!(
"Machine '{machine_name}' needs to be restarted in order to apply the new \
secret. Is this OK? [y/n]",
))
.prompt()
.recover_err(|err| match err {
inquire::InquireError::NotTTY => Ok(false),
err => Err(err),
})?
{
set_progress_status("Stopping machine '{machine_name}'...");
machine.stop(false)?;
} else {
log::warn!("The changed secret will not be applied until the machine is restarted.");
}
}
log::info!("Success!");
}
},

Cmd::Module(cmd) => match cmd {
cli::ModuleCmd::List { name } => {
let json = cli.json;
let (_, cfg) = MachineConfig::open_existing(name, false)?;
cfg.modules.print(json);
cfg.modules.print(cli.json);
}
cli::ModuleCmd::Add {
machine_name,
Expand Down Expand Up @@ -270,14 +347,19 @@ Thank you kindly!"#
alert_dirty(module::delete(name, module_name)?)
}
},

Cmd::GC {
delete_old,
all,
machines,
} => Driver::store().gc(delete_old.map(|x| x.unwrap_or_default()), *all, machines)?,

Cmd::Tray {} => tray::run()?,

Cmd::Completion { .. } => unreachable!(),

Cmd::Tar { .. } => unreachable!(),

Cmd::Store(_) => unreachable!(),
}
if CodchiConfig::get().tray.autostart {
Expand Down
27 changes: 7 additions & 20 deletions codchi/src/platform/machine.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use super::{
platform::HostImpl, Host, LinuxCommandBuilder, LinuxCommandTarget, LinuxUser, NixDriver,
};
use super::{platform::HostImpl, Host, LinuxCommandBuilder, LinuxCommandTarget, LinuxUser};
use crate::{
cli::{CODCHI_DRIVER_MODULE, DEBUG},
config::{ConfigResult, EnvSecret, FlakeLocation, MachineConfig},
config::{ConfigResult, FlakeLocation, MachineConfig},
consts::{self, host, ToPath},
logging::{hide_progress, log_progress, set_progress_status, with_suspended_progress},
logging::{hide_progress, log_progress, set_progress_status},
platform::{self, CommandExt, Driver, Store},
progress_scope,
secrets::MachineSecrets,
util::{PathExt, ResultExt, UtilExt},
};
use anyhow::{bail, Context, Result};
Expand Down Expand Up @@ -365,12 +364,10 @@ git add flake.*
self.cmd().run("pkill", &["sleep"]).wait_ok()
);
}
let secrets: HashMap<String, EnvSecret> = Driver::store().cmd().eval(
consts::store::DIR_CONFIG.join_machine(&self.config.name),
"nixosConfigurations.default.config.codchi.secrets.env",
)?;

set_progress_status("Evaluating secrets...");
let secrets = self.eval_env_secrets()?;

let (lock, mut cfg) = MachineConfig::open_existing(&self.config.name, true)?;
let old_secrets = &cfg.secrets;
let mut all_secrets = HashMap::new();
Expand All @@ -381,17 +378,7 @@ git add flake.*
all_secrets.insert(secret.name.clone(), existing.clone());
}
None => {
let value = with_suspended_progress(|| {
inquire::Password::new(&format!(
"Please enter secret '{}' (Toggle input mask with <Ctrl+R>):",
secret.name
))
.without_confirmation()
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.with_help_message(secret.description.trim())
.with_display_toggle_enabled()
.prompt()
})?;
let value = secret.prompt_value()?;
all_secrets.insert(secret.name.clone(), value);
}
}
Expand Down
49 changes: 49 additions & 0 deletions codchi/src/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use crate::{
consts::{self, ToPath},
logging::with_suspended_progress,
platform::{Driver, Machine, NixDriver, Store},
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EnvSecret {
pub name: String,
pub description: String,
pub value: Option<String>,
}

impl EnvSecret {
pub fn prompt_value(&self) -> anyhow::Result<String> {
let value = with_suspended_progress(|| {
inquire::Password::new(&format!(
"Please enter secret '{}' (Toggle input mask with <Ctrl+R>):",
self.name
))
.without_confirmation()
.with_display_mode(inquire::PasswordDisplayMode::Masked)
.with_help_message(self.description.trim())
.with_display_toggle_enabled()
.prompt()
})?;
Ok(value)
}
}

pub trait MachineSecrets {
/// combines secret definitions from the NixOS config with the values set by the user
fn eval_env_secrets(&self) -> anyhow::Result<HashMap<String, EnvSecret>>;
}

impl MachineSecrets for Machine {
fn eval_env_secrets(&self) -> anyhow::Result<HashMap<String, EnvSecret>> {
let mut secrets: HashMap<String, EnvSecret> = Driver::store().cmd().eval(
consts::store::DIR_CONFIG.join_machine(&self.config.name),
"nixosConfigurations.default.config.codchi.secrets.env",
)?;
for (name, secret) in secrets.iter_mut() {
secret.value = self.config.secrets.get(name).cloned();
}
Ok(secrets)
}
}
Loading

0 comments on commit 9ca51e0

Please sign in to comment.