use std::net::IpAddr;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::config::ServicePort;
use crate::ssh::SshKeypair;
use crate::wg::WireguardManager;
#[derive(Debug, Serialize, Deserialize)]
pub struct CloudConfig {
users: Vec<CloudConfigUser>,
package_update: bool,
package_upgrade: bool,
ssh_keys: std::collections::HashMap<String, String>,
write_files: Vec<CloudConfigFile>,
packages: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
runcmd: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CloudConfigFile {
content: String,
owner: String,
path: String,
permissions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
defer: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CloudConfigUser {
name: String,
groups: Vec<String>,
sudo: String,
shell: String,
ssh_authorized_keys: Vec<String>,
}
impl CloudConfig {
pub fn new(
ssh_client_keypair: &SshKeypair,
ssh_server_keypair: &SshKeypair,
wg_mgr: &WireguardManager,
services: &[ServicePort],
) -> Result<Self> {
let cc_template = include_str!("../../files/cloudinit.cfg");
let mut cc = serde_yaml::from_str::<CloudConfig>(cc_template)?;
cc.ssh_keys.insert(
"ed25519_public".to_string(),
ssh_server_keypair.public.to_string(),
);
cc.ssh_keys.insert(
"ed25519_private".to_string(),
ssh_server_keypair.private.to_string(),
);
let wg = CloudConfigFile {
content: wg_mgr.remote_device.config(&[])?,
owner: String::from("root:root"),
permissions: String::from("0644"),
path: String::from("/tmp/innisfree.conf"),
defer: None,
};
cc.write_files.push(wg);
let nginx = CloudConfigFile {
content: nginx_streams(services, wg_mgr.local_device.interface.address)?,
owner: String::from("root:root"),
permissions: String::from("0644"),
path: String::from("/etc/nginx/conf.d/stream/innisfree.conf"),
defer: None,
};
cc.write_files.push(nginx);
cc.users[0].ssh_authorized_keys = vec![ssh_client_keypair.public.to_string()];
Ok(cc)
}
pub fn authorize_ssh_keys<I, S>(&mut self, public_keys: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.users[0]
.ssh_authorized_keys
.extend(public_keys.into_iter().map(Into::into));
}
}
impl TryInto<String> for CloudConfig {
type Error = anyhow::Error;
fn try_into(self) -> anyhow::Result<String> {
Ok(format!("#cloud-config\n{}", serde_yaml::to_string(&self)?))
}
}
fn nginx_streams(services: &[ServicePort], dest_ip: IpAddr) -> Result<String> {
let nginx_config = include_str!("../../files/stream.conf.j2");
let mut context = tera::Context::new();
context.insert("services", services);
context.insert("dest_ip", &dest_ip.to_string());
tera::Tera::one_off(nginx_config, &context, false).context("Template generation failed")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cloudconfig_has_header() -> Result<()> {
let kp1 = SshKeypair::new()?;
let kp2 = SshKeypair::new()?;
let wg_mgr = WireguardManager::new("foo-test")?;
let ports = vec![];
let cc = CloudConfig::new(&kp1, &kp2, &wg_mgr, &ports)?;
let user_data: String = cc.try_into()?;
assert!(user_data.starts_with("#cloud-config\n"));
Ok(())
}
}