//! 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> { // 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, } }