diff --git a/bin/core/src/cloud/hetzner/common.rs b/bin/core/src/cloud/hetzner/common.rs new file mode 100644 index 000000000..a49dedab0 --- /dev/null +++ b/bin/core/src/cloud/hetzner/common.rs @@ -0,0 +1,87 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerServer { + pub id: i64, + pub image: HetznerImage, + pub name: String, + pub primary_disk_size: f64, + pub private_net: Vec, + pub public_net: HetznerPublicNet, + pub server_type: HetznerServerType, + pub status: String, + pub volumes: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerServerType { + pub architecture: String, + pub cores: i64, + pub cpu_type: String, + pub description: String, + pub disk: f64, + pub id: i64, + pub memory: f64, + pub name: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerPrivateNet { + pub alias_ips: Vec, + pub ip: String, + pub mac_address: String, + pub network: i64, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerPublicNet { + pub firewalls: Vec, + pub ipv4: HetznerIpv4, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerFirewall { + pub id: i64, + pub status: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerIpv4 { + pub blocked: bool, + pub dns_prt: String, + pub ip: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerImage { + pub description: String, + pub name: String, + pub os_flavor: String, + pub os_version: String, + pub rapid_deploy: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerAction { + pub command: String, + pub error: HetznerError, + pub finished: Option, + pub id: i64, + pub progress: i32, + pub resources: Vec, + pub started: String, + pub status: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerError { + pub code: String, + pub message: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HetznerResource { + pub id: i64, + #[serde(rename = "type")] + pub ty: String, +} diff --git a/bin/core/src/cloud/hetzner/create_server.rs b/bin/core/src/cloud/hetzner/create_server.rs new file mode 100644 index 000000000..13be129f6 --- /dev/null +++ b/bin/core/src/cloud/hetzner/create_server.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::common::{HetznerAction, HetznerServer}; + +#[derive(Debug, Clone, Serialize)] +pub struct CreateServerBody { + /// Auto-mount Volumes after attach + pub automount: bool, + /// ID or name of Datacenter to create Server in (must not be used together with location) + pub datacenter: Option, + /// ID or name of Location to create Server in (must not be used together with datacenter) + pub location: Option, + /// Firewalls which should be applied on the Server's public network interface at creation time + pub firewalls: Vec, + /// ID or name of the Image the Server is created from + pub image: String, + /// User-defined labels (key-value pairs) for the Resource + pub labels: HashMap, + /// Name of the Server to create (must be unique per Project and a valid hostname as per RFC 1123) + pub name: String, + /// Network IDs which should be attached to the Server private network interface at the creation time + pub networks: Vec, + /// ID of the Placement Group the server should be in + pub placement_group: i64, + /// Public Network options + pub public_net: PublicNet, + /// ID or name of the Server type this Server should be created with + pub server_type: String, + /// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time + pub ssh_keys: Vec, + /// This automatically triggers a Power on a Server-Server Action after the creation is finished and is returned in the next_actions response object. + pub start_after_create: bool, + /// Cloud-Init user data to use during Server creation. This field is limited to 32KiB. + pub user_data: String, + /// Volume IDs which should be attached to the Server at the creation time. Volumes must be in the same Location. + pub volumes: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct Firewall { + /// ID of the Firewall + pub firewall: i64, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct PublicNet { + /// Attach an IPv4 on the public NIC. If false, no IPv4 address will be attached. + pub enable_ipv4: bool, + /// Attach an IPv6 on the public NIC. If false, no IPv6 address will be attached. + pub enable_ipv6: bool, + /// ID of the ipv4 Primary IP to use. If omitted and enable_ipv4 is true, a new ipv4 Primary IP will automatically be created. + pub ipv4: Option, + /// ID of the ipv6 Primary IP to use. If omitted and enable_ipv6 is true, a new ipv6 Primary IP will automatically be created. + pub ipv6: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CreateServerResponse { + pub action: HetznerAction, + pub next_actions: Vec, + pub root_password: Option, + pub server: HetznerServer, +} diff --git a/bin/core/src/cloud/hetzner/mod.rs b/bin/core/src/cloud/hetzner/mod.rs new file mode 100644 index 000000000..bfcbd6e82 --- /dev/null +++ b/bin/core/src/cloud/hetzner/mod.rs @@ -0,0 +1,60 @@ +use std::sync::OnceLock; + +use anyhow::{anyhow, Context}; +use reqwest::StatusCode; +use serde::{de::DeserializeOwned, Serialize}; + +use self::create_server::{CreateServerBody, CreateServerResponse}; + +pub mod common; +pub mod create_server; + +const BASE_URL: &str = "https://api.hetzner.cloud/v1"; + +pub struct HetznerClient { + client: reqwest::Client, + token: String, +} + +impl HetznerClient { + fn new(token: &str) -> HetznerClient { + HetznerClient { + client: Default::default(), + token: format!("Bearer {token}"), + } + } + + pub async fn create_server( + &self, + body: &CreateServerBody, + ) -> anyhow::Result { + self.post("/servers", body).await + } + + async fn post( + &self, + path: &str, + body: &Body, + ) -> anyhow::Result { + let res = self + .client + .post(format!("{BASE_URL}{path}")) + .json(&body) + .header("authorization", &self.token) + .send() + .await + .context("failed to make request to Hetzner")?; + + let status = res.status(); + + if status == StatusCode::OK { + res.json().await.context("failed to parse response to json") + } else { + let text = res + .text() + .await + .context("failed to get response body as text")?; + Err(anyhow!("FAILED | {path} | {status} | {text}")) + } + } +} diff --git a/bin/core/src/cloud/mod.rs b/bin/core/src/cloud/mod.rs index 99d6dd2c5..2df682b58 100644 --- a/bin/core/src/cloud/mod.rs +++ b/bin/core/src/cloud/mod.rs @@ -1,4 +1,5 @@ pub mod aws; +pub mod hetzner; #[derive(Debug)] pub enum BuildCleanupData {