provisioning-platform/crates/ontoref-compute/examples/lian-build-hetzner-shim.rs

98 lines
3.5 KiB
Rust
Raw Permalink Normal View History

//! Co-validation shim: prove that `HetznerCloudProvider` can replace lian-build's
//! bespoke Hetzner spawn/status/destroy without touching the trait.
//!
//! Run with:
//! ```sh
//! HCLOUD_TOKEN=... HETZNER_DEFAULT_LOCATION=nbg1 \
//! cargo run --example lian-build-hetzner-shim --features hetzner
//! ```
use std::time::Duration;
use ontoref_compute::sizing::{hetzner_tier, tier_spec, SizeTier};
use ontoref_compute::{ComputeProvider, InstanceSpec, InstanceStatus};
#[cfg(not(feature = "hetzner"))]
compile_error!(
"This example requires the `hetzner` feature. \
Run: cargo run --example lian-build-hetzner-shim --features hetzner"
);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Construct provider from environment (same env lian-build uses).
#[cfg(feature = "hetzner")]
{
use ontoref_compute::providers::hetzner::HetznerCloudProvider;
use tokio::time::sleep;
let provider = HetznerCloudProvider::from_env()?;
// 2. Pick a tier via the lian-build policy shim (lives here in the example;
// in real lian-build it lives in lian-build/src/sizing_policy.rs).
let tier = language_default("rust");
let ts = tier_spec(tier);
println!(
"tier={tier:?} hetzner_type={} cpu={} mem_mb={}",
hetzner_tier(tier),
ts.vcpus,
ts.memory_mb
);
// 3. Build a spec using the constructor (InstanceSpec is #[non_exhaustive]).
let mut spec = InstanceSpec::new("ubuntu-24.04", ts.vcpus, ts.memory_mb as u32);
spec.disk_gb = ts.disk_gb as u32;
// location: None picks up HETZNER_DEFAULT_LOCATION from provider env.
spec.ssh_key = std::env::var("SSH_KEY_NAME").ok();
spec.labels.insert("lian-build/shim".into(), "true".into());
spec.ttl = Some(Duration::from_secs(15 * 60));
spec.timeout = Some(Duration::from_secs(60));
// 4. Spawn.
let inst = provider.spawn(spec).await?;
println!("spawned: handle={} ip={:?}", inst.handle, inst.ip);
// 5. Poll status until Running or 2 minutes elapse.
let started = std::time::Instant::now();
loop {
let s = provider.status(&inst.handle).await?;
println!(" status={s:?}");
match s {
InstanceStatus::Running => break,
InstanceStatus::Failed
| InstanceStatus::Succeeded
| InstanceStatus::Unknown => break,
_ if started.elapsed() > Duration::from_secs(120) => {
eprintln!("timed out waiting for Running");
break;
}
_ => sleep(Duration::from_secs(5)).await,
}
}
// 6. Destroy.
provider.destroy(&inst.handle).await?;
println!("destroyed: {}", inst.handle);
// 7. Verify destroy is idempotent (same call lian-build's cleanup makes).
provider.destroy(&inst.handle).await?;
println!("idempotent destroy: ok");
}
Ok(())
}
/// Maps a language identifier to a `SizeTier`.
///
/// This function lives in lian-build in production. It is inlined here to
/// demonstrate that `language_default` is NOT part of `ontoref-compute` — it is
/// build-system policy that belongs in the caller.
fn language_default(language: &str) -> SizeTier {
match language {
"rust" | "cpp" | "scala" => SizeTier::M,
"python" | "ruby" | "javascript" => SizeTier::S,
"nu" | "bash" | "sh" => SizeTier::XS,
_ => SizeTier::S,
}
}