Begin working on porting much of the opentofu related code
into rust. Mainly this should give proper argument parsing and error handling, and also remove some of all the scattered shell scripts.
This commit is contained in:
parent
30cf1f407a
commit
e6a152e95c
9 changed files with 319 additions and 25 deletions
13
rust/Cargo.lock
generated
13
rust/Cargo.lock
generated
|
@ -795,6 +795,19 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "provision"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"common",
|
||||
"hakari",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
|
|
|
@ -141,7 +141,7 @@ pub fn read_to_string(path: &Path) -> anyhow::Result<String> {
|
|||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn create_link(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||
std::os::unix::fs::symlink(to, from).with_context(|| {
|
||||
std::os::unix::fs::symlink(from, to).with_context(|| {
|
||||
format!(
|
||||
"Could not create symbolic link from {from} to {to}",
|
||||
from = from.display(),
|
||||
|
|
14
rust/program/provision/Cargo.toml
Normal file
14
rust/program/provision/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "provision"
|
||||
edition = "2024"
|
||||
version = "1.0.0"
|
||||
metadata.crane.name = "provision"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
common = { path = "../../lib/common" }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
hakari = { version = "0.1", path = "../../lib/hakari" }
|
281
rust/program/provision/src/main.rs
Normal file
281
rust/program/provision/src/main.rs
Normal file
|
@ -0,0 +1,281 @@
|
|||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
net::Ipv4Addr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
fn main() {
|
||||
common::entrypoint(program);
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Creates the instance.
|
||||
CreateInstance(CreateInstance),
|
||||
/// Updates the provisioned resources for the instance.
|
||||
Update(UpdateInstance),
|
||||
/// Configures the software installed on he instance.
|
||||
Configure(ConfigureInstance),
|
||||
/// Destroys the instance, but leaves behind persistent data, unless forcing deletion.
|
||||
Destroy(DestroyInstance),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
pub struct CreateInstance {
|
||||
/// Name of the instance to create.
|
||||
instance: String,
|
||||
|
||||
#[arg(short = 's')]
|
||||
skip_sanitity_checks: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
pub struct UpdateInstance {
|
||||
/// Name of the instance to update.
|
||||
instance: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
pub struct ConfigureInstance {
|
||||
/// Name of the instance to configure.
|
||||
instance: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
pub struct DestroyInstance {
|
||||
/// Name of the instance to destroy.
|
||||
instance: String,
|
||||
}
|
||||
|
||||
fn program() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
let flake_path = common::env::read_path_env("FLAKE_PATH")?;
|
||||
match args.command {
|
||||
Commands::CreateInstance(instance) => create_instance(instance, &flake_path),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ProvisioningData {
|
||||
persistence: OpenTofuConfig,
|
||||
compute: OpenTofuConfig,
|
||||
compute_with_persistence_attached: OpenTofuConfig,
|
||||
configuration: OpenTofuConfig,
|
||||
secrets_source: SecretsSource,
|
||||
|
||||
image_username: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
enum SecretsSource {
|
||||
#[serde(rename = "bitwarden")]
|
||||
Bitwarden,
|
||||
#[serde(rename = "vault")]
|
||||
Vault,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenTofuConfig {
|
||||
config: Option<PathBuf>,
|
||||
endpoints: Vec<String>,
|
||||
}
|
||||
|
||||
struct WorkDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl WorkDir {
|
||||
pub fn try_new(template: &str) -> anyhow::Result<Self> {
|
||||
let mut proc = common::proc::Command::new("mktemp");
|
||||
proc.args(["-dt", template]);
|
||||
let path: PathBuf = proc.try_spawn_to_string()?.into();
|
||||
common::fs::create_dir_recursive(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
pub fn cleanup(self) -> anyhow::Result<()> {
|
||||
common::fs::remove_dir_recursive(&self.path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenTofuConfig {
|
||||
pub fn init<'e, K: AsRef<str>, V: AsRef<str>>(
|
||||
&self,
|
||||
instance: &str,
|
||||
env_map: &'e BTreeMap<K, V>,
|
||||
) -> anyhow::Result<Option<OpenTofuInstance<'e, K, V>>> {
|
||||
let Some(config) = self.config.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let work_dir = WorkDir::try_new(&format!("{instance}-provision.XXXXXX"))?;
|
||||
let config_path: PathBuf = work_dir.path.join("config.tf.json");
|
||||
common::fs::create_link(config, &config_path)?;
|
||||
|
||||
let mut init_proc = common::proc::Command::new("tofu");
|
||||
let chdir_arg = format!("-chdir={}", work_dir.path.display());
|
||||
for (key, value) in env_map {
|
||||
init_proc.env(key.as_ref(), value.as_ref());
|
||||
}
|
||||
init_proc.args([&chdir_arg, "init"]);
|
||||
init_proc.try_spawn_to_bytes()?;
|
||||
Ok(Some(OpenTofuInstance {
|
||||
work_dir,
|
||||
env_map,
|
||||
chdir_arg,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenTofuInstance<'e, K, V> {
|
||||
work_dir: WorkDir,
|
||||
chdir_arg: String,
|
||||
env_map: &'e BTreeMap<K, V>,
|
||||
}
|
||||
|
||||
impl<'e, K: AsRef<str>, V: AsRef<str>> OpenTofuInstance<'e, K, V> {
|
||||
pub fn run(&mut self, action: &str) -> anyhow::Result<()> {
|
||||
let mut proc = common::proc::Command::new("tofu");
|
||||
for (key, value) in self.env_map {
|
||||
proc.env(key.as_ref(), value.as_ref());
|
||||
}
|
||||
proc.args([&self.chdir_arg, action]);
|
||||
proc.try_spawn_to_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn output<D: for<'de> serde::Deserialize<'de>>(&mut self) -> anyhow::Result<D> {
|
||||
let mut proc = common::proc::Command::new("tofu");
|
||||
for (key, value) in self.env_map {
|
||||
proc.env(key.as_ref(), value.as_ref());
|
||||
}
|
||||
proc.args([&self.chdir_arg, "output", "-json"]);
|
||||
proc.try_spawn_to_json()
|
||||
}
|
||||
|
||||
pub fn cleanup(self) -> anyhow::Result<()> {
|
||||
self.work_dir.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvisioningData {
|
||||
pub fn try_new(instance: &str, flake_path: &Path) -> anyhow::Result<Self> {
|
||||
let mut proc = common::proc::Command::new("nix");
|
||||
let base_attr = format!(
|
||||
"{}#nixosConfigurations.\"{instance}\".config.khscodes.infrastructure.provisioning",
|
||||
flake_path.display()
|
||||
);
|
||||
let script = r#"
|
||||
let
|
||||
data = prov: { inherit (prov) config endpoints; };
|
||||
in
|
||||
p: {
|
||||
persistence = data p.persistence;
|
||||
compute = data p.compute;
|
||||
compute_with_persistence_attached = data p.combinedPersistenceAttachAndCompute;
|
||||
configuration = data p.configuration;
|
||||
secrets_source = p.secretsSource;
|
||||
image_username = p.imageUsername;
|
||||
}
|
||||
"#;
|
||||
proc.args(["eval", "--json", &base_attr, "--apply", script]);
|
||||
|
||||
let result = proc.stderr_inherit().try_spawn_to_json()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn nixos_install(
|
||||
flake_path: &Path,
|
||||
instance: &str,
|
||||
username: &str,
|
||||
ip_addr: &Ipv4Addr,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut proc = common::proc::Command::new("nixos-install");
|
||||
let flake_arg = format!("{}#{instance}", flake_path.display());
|
||||
let host_arg = format!("{username}@{ip_addr}");
|
||||
proc.args(["--flake", &flake_arg, "--target-host", &host_arg]);
|
||||
|
||||
if !proc.stderr_inherit().try_spawn_stdout_inherit()?.success() {
|
||||
return Err(anyhow::format_err!("nixos-install didn't succeed"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_instance(c: CreateInstance, flake_path: &Path) -> anyhow::Result<()> {
|
||||
let data = ProvisioningData::try_new(&c.instance, flake_path)?;
|
||||
if data.compute.config.is_none() {
|
||||
return Err(anyhow::format_err!(
|
||||
"No compute resources allocated for {}",
|
||||
c.instance
|
||||
));
|
||||
}
|
||||
if !c.skip_sanitity_checks {
|
||||
log::info!("Building system configuration to ensure it is installable");
|
||||
// First lets make sure we can build what we're trying to create
|
||||
let mut proc = common::proc::Command::new("nix");
|
||||
proc.args([
|
||||
"build",
|
||||
"--no-link",
|
||||
&format!(
|
||||
"{}#nixosConfigurations.\"{}\".config.system.build.toplevel",
|
||||
flake_path.display(),
|
||||
c.instance
|
||||
),
|
||||
]);
|
||||
proc.stderr_inherit().try_spawn_to_bytes()?;
|
||||
}
|
||||
// TODO: Gather all needed endpoints and load the environments as needed from either Bitwarden or
|
||||
// vault. Can probably get rid of the nix bw implementation and merge the rust helper program into this one
|
||||
// to remove unneeded stuff.
|
||||
let env = BTreeMap::<String, String>::new();
|
||||
if let Some(mut persistence) = data.persistence.init(&c.instance, &env)? {
|
||||
persistence.run("apply")?;
|
||||
persistence.cleanup()?;
|
||||
}
|
||||
let mut compute = data
|
||||
.compute
|
||||
.init(&c.instance, &env)?
|
||||
.expect("Verified earlier that config is not none");
|
||||
compute.run("apply")?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Output {
|
||||
ipv4_address: Ipv4AddrValue,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Ipv4AddrValue {
|
||||
value: Ipv4Addr,
|
||||
}
|
||||
|
||||
let output: Output = compute.output()?;
|
||||
compute.cleanup()?;
|
||||
|
||||
nixos_install(
|
||||
flake_path,
|
||||
&c.instance,
|
||||
&data.image_username,
|
||||
&output.ipv4_address.value,
|
||||
)?;
|
||||
|
||||
if let Some(mut compute_with_persistence) = data
|
||||
.compute_with_persistence_attached
|
||||
.init(&c.instance, &env)?
|
||||
{
|
||||
compute_with_persistence.run("apply")?;
|
||||
compute_with_persistence.cleanup()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue