use crate::cloud_init::{self, RenderInput};
use crate::config::MachineConfig;
use crate::provider::{Location, MachineProvider, ProjectId, ServerId, ServerSpec};
use anyhow::{Context, Result};
use std::path::Path;
#[derive(Debug)]
pub struct ProvisionRequest {
pub machine_name: String,
pub server_type: String,
pub location: Location,
pub user_data: String,
pub ssh_keys: Vec<u64>,
}
pub fn build_request(
workspace_root: &Path,
machine: &MachineConfig,
yubaba_url: String,
yubaba_sha256: String,
yubaba_channel: String,
headscale_preauth_key: Option<String>,
mesh_url: Option<String>,
cloudflared_token: Option<String>,
yubaba_cosign_identity_regexp: Option<String>,
) -> Result<ProvisionRequest> {
let template = cloud_init::load_template(workspace_root)?;
let input = RenderInput {
machine,
yubaba_url,
yubaba_sha256,
yubaba_channel,
headscale_preauth_key,
mesh_url,
cloudflared_token,
yubaba_cosign_identity_regexp,
};
let user_data = cloud_init::render(&template, &input)?;
machine.validate()?;
let location = Location::try_from(machine.location())
.with_context(|| format!("machine '{}' has unknown location", machine.name))?;
Ok(ProvisionRequest {
machine_name: machine.name.clone(),
server_type: machine.server_type().to_string(),
location,
user_data,
ssh_keys: machine.ssh_keys.clone(),
})
}
pub async fn execute(
provider: &dyn MachineProvider,
project: &ProjectId,
req: &ProvisionRequest,
) -> Result<ServerId> {
let spec = ServerSpec {
name: req.machine_name.clone(),
server_type: req.server_type.clone(),
image: "debian-12".into(),
location: req.location.clone(),
ssh_keys: req.ssh_keys.clone(),
};
provider.create_server(project, &spec, &req.user_data).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cloud_init;
use crate::config::{BucketSpec, MachineConfig};
fn sample_machine() -> MachineConfig {
MachineConfig {
name: "noisetable-pdx-1".into(),
provider: "hetzner".into(),
location: Some("pdx".into()),
server_type: Some("cpx22".into()),
hosts_mirrors: vec!["noisetable".into(), "yah".into()],
mesh_tags: vec!["tag:region-pdx".into(), "tag:tier-t2".into()],
region: None,
zone: None,
arch: None,
bucket: Some(BucketSpec {
name: "noisetable-assets-pdx-1".into(),
public_read: false,
}),
vendor: None,
nickname: None,
legacy_hostkey_fingerprint: None,
registration: Default::default(),
ssh_keys: vec![],
cloudflared: None,
hosts_operator_bridge: false,
connect: None,
allocatable: None,
taints: vec![],
}
}
fn build_req_defaults(
dir: &std::path::Path,
machine: &MachineConfig,
extra_url: Option<String>,
extra_cf: Option<String>,
) -> crate::provision::ProvisionRequest {
build_request(
dir,
machine,
"https://example.com/yah-yubaba".into(),
"deadbeef".into(),
cloud_init::DEFAULT_YUBABA_CHANNEL.into(),
Some("KEY".into()),
extra_url,
extra_cf,
None,
)
.unwrap()
}
#[test]
fn build_request_renders_user_data_and_picks_location() {
let machine = sample_machine();
let dir = tempfile::tempdir().unwrap();
let req = build_req_defaults(dir.path(), &machine, None, None);
assert_eq!(req.machine_name, "noisetable-pdx-1");
assert_eq!(req.location, Location::Pdx);
assert!(req.user_data.contains("https://example.com/yah-yubaba"));
assert!(req.user_data.contains("deadbeef"));
assert!(req.user_data.contains("KEY"));
assert!(req.user_data.contains("tag:region-pdx,tag:tier-t2"));
}
#[test]
fn build_request_with_mesh_url_adds_login_server() {
let machine = sample_machine();
let dir = tempfile::tempdir().unwrap();
let req = build_req_defaults(
dir.path(),
&machine,
Some("https://mesh.example.com".into()),
None,
);
assert!(req
.user_data
.contains("--login-server https://mesh.example.com"));
}
#[test]
fn build_request_standalone_omits_join_block() {
let machine = sample_machine();
let dir = tempfile::tempdir().unwrap();
let req = build_request(
dir.path(),
&machine,
"https://example.com/yah-yubaba".into(),
"deadbeef".into(),
cloud_init::DEFAULT_YUBABA_CHANNEL.into(),
None, None, None,
None,
)
.unwrap();
assert!(
!req.user_data.contains("tailscale up --auth-key"),
"standalone node must not emit the tailscale-up join block"
);
assert!(!req.user_data.contains("--login-server https://"));
}
#[test]
fn build_request_rejects_unknown_location() {
let mut machine = sample_machine();
machine.location = Some("moon".into());
let dir = tempfile::tempdir().unwrap();
let err = build_request(
dir.path(),
&machine,
"x".into(),
"y".into(),
"stable".into(),
Some("z".into()),
None,
None,
None,
)
.unwrap_err()
.to_string();
assert!(err.contains("unknown location"), "unexpected: {err}");
}
#[test]
fn build_request_threads_cosign_identity_into_render() {
let machine = sample_machine();
let dir = tempfile::tempdir().unwrap();
let req = build_request(
dir.path(),
&machine,
"https://cdn.yah.dev/yubaba/0.9.0/x86_64-unknown-linux-musl/yah-yubaba-x86_64-unknown-linux-musl.tar.gz".into(),
"deadbeef".into(),
cloud_init::DEFAULT_YUBABA_CHANNEL.into(),
None,
None,
None,
Some(r"^https://github\.com/yah-ai/yah/".into()),
)
.unwrap();
assert!(
req.user_data
.contains("cosign verify-blob --certificate-identity-regexp"),
"verify-blob runcmd missing once identity_regexp is threaded"
);
assert!(
req.user_data.contains(r"^https://github\.com/yah-ai/yah/"),
"identity_regexp value missing from rendered output"
);
}
}