Files
firezone/rust/cli/src/main.rs
Thomas Eizinger a0a47d6d16 chore(fz-cli): expand enable to enable-service (#10715)
Just `enable` is a bit short when we might also add other sub-commands
like `enable-auto-updates`.
2025-10-27 04:33:48 +00:00

162 lines
4.4 KiB
Rust

#![expect(clippy::print_stdout, reason = "We are a CLI.")]
use std::{process::Command, sync::LazyLock};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use secrecy::{ExposeSecret as _, SecretString};
use tracing_subscriber::{EnvFilter, util::SubscriberInitExt};
const ETC_FIREZONE_GATEWAY_TOKEN: &str = "/etc/firezone/gateway-token";
static DRY_RUN: LazyLock<bool> = LazyLock::new(|| {
std::env::var("FZ_DRY_RUN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or_default()
});
fn main() -> Result<()> {
let _guard = tracing_subscriber::fmt()
.without_time()
.with_target(false)
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stdout)
.set_default();
let cli = Cli::parse();
use Component::*;
use GatewayCommand::*;
match cli.component {
Gateway(Authenticate { replace }) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
if let Ok(existing) = std::fs::read_to_string(ETC_FIREZONE_GATEWAY_TOKEN)
&& !existing.trim().is_empty()
&& !replace
{
anyhow::bail!(
"Found existing token at {ETC_FIREZONE_GATEWAY_TOKEN}, use --replace to overwrite"
);
}
let token = loop {
println!("Paste the token from the portal's deploy page:");
let token =
rpassword::read_password().context("Failed to read token from stdin")?;
if token.trim().is_empty() {
continue;
}
break SecretString::new(token);
};
write_to_file(ETC_FIREZONE_GATEWAY_TOKEN, token)?;
println!("Successfully installed token");
println!("Tip: You can now start the Gateway with `firezone gateway enable-service`");
}
Gateway(EnableService) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
run("systemctl", "enable --now firezone-gateway.service")
.context("Failed to enable `firezone-gateway.service`")?;
println!("Successfully enabled `firezone-gateway.service`");
}
Gateway(DisableService) => {
anyhow::ensure!(cfg!(target_os = "linux"), "Only supported Linux right now");
anyhow::ensure!(is_root(), "Must be executed as root");
run("systemctl", "disable firezone-gateway.service")
.context("Failed to disable `firezone-gateway.service`")?;
println!("Successfully disabled `firezone-gateway.service`");
}
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(name = "firezone", bin_name = "firezone", about, long_about = None)]
struct Cli {
#[command(subcommand)]
component: Component,
}
#[derive(Debug, Subcommand)]
enum Component {
#[command(subcommand)]
Gateway(GatewayCommand),
}
#[derive(Debug, Subcommand)]
enum GatewayCommand {
/// Securely store the Gateway's token on disk.
Authenticate {
/// If an existing token is found, replace it.
#[arg(long, default_value_t = false)]
replace: bool,
},
/// Enable the Gateway's systemd service.
EnableService,
/// Disable the Gateway's systemd service.
DisableService,
}
#[cfg(target_os = "linux")]
fn is_root() -> bool {
if *DRY_RUN {
return true;
}
nix::unistd::Uid::current().is_root()
}
#[cfg(not(target_os = "linux"))]
fn is_root() -> bool {
true
}
fn write_to_file(path: &str, content: SecretString) -> Result<()> {
tracing::debug!("Writing {} bytes to {path}", content.expose_secret().len());
check_dry_run()?;
std::fs::write(path, content.expose_secret())
.with_context(|| format!("Failed to write to `{path}`"))?;
Ok(())
}
fn run(bin: &str, args: &str) -> Result<()> {
tracing::debug!("Running `{bin} {args}`");
check_dry_run()?;
let output = Command::new(bin)
.args(args.split_ascii_whitespace())
.output()?;
anyhow::ensure!(
output.status.success(),
"`{bin} {args}` exited with {}",
output.status
);
Ok(())
}
fn check_dry_run() -> Result<()> {
anyhow::ensure!(!*DRY_RUN, "Aborting because `FZ_DRY_RUN=true`");
Ok(())
}