lmrc_hetzner/
lib.rs

1//! # Hetzner Cloud Client
2//!
3//! A comprehensive, async Hetzner Cloud API client with builders, validation, and extensive error handling.
4//!
5//! ## Features
6//!
7//! - **Type-safe builders** with compile-time validation
8//! - **Comprehensive error handling** with detailed error contexts
9//! - **Async/await** support with Tokio
10//! - **Input validation** for IP addresses, ports, names, etc.
11//! - **Full API coverage** for servers, networks, firewalls, load balancers, and SSH keys
12//!
13//! ## Quick Start
14//!
15//! ```no_run
16//! use lmrc_hetzner::HetznerClient;
17//!
18//! #[tokio::main]
19//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
20//!     // Create a client from environment variable
21//!     let client = HetznerClient::from_env()?;
22//!
23//!     // Or build with custom configuration
24//!     let client = HetznerClient::builder()
25//!         .api_token("your-api-token".to_string())
26//!         .build()?;
27//!
28//!     Ok(())
29//! }
30//! ```
31
32pub mod adapter;
33pub mod client;
34pub mod config;
35pub mod differ;
36pub mod domain;
37pub mod firewalls;
38pub mod floating_ips;
39pub mod loadbalancers;
40pub mod networks;
41pub mod ports;
42pub mod provisioner;
43pub mod retry;
44pub mod servers;
45pub mod ssh_keys;
46pub mod state;
47pub mod validation;
48pub mod volumes;
49
50pub use adapter::HetznerAdapter;
51pub use client::{HetznerClient, HetznerClientBuilder};
52pub use config::NetworkConfig;
53pub use differ::HetznerDiffer;
54pub use firewalls::FirewallManager;
55pub use floating_ips::FloatingIpManager;
56pub use loadbalancers::LoadBalancerManager;
57pub use networks::NetworkManager;
58pub use provisioner::HetznerProvisioner;
59pub use servers::ServerManager;
60pub use ssh_keys::SshKeyManager;
61pub use state::StateManager;
62pub use volumes::VolumeManager;
63
64use serde::{Deserialize, Serialize};
65use thiserror::Error;
66
67/// Comprehensive error type for Hetzner Cloud API operations.
68///
69/// This error type provides detailed context about what went wrong, including
70/// HTTP status codes, API error details, and contextual information.
71///
72/// # Examples
73///
74/// ```no_run
75/// use lmrc_hetzner::{HetznerClient, HetznerError};
76///
77/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78/// let client = HetznerClient::builder()
79///     .api_token("your-token".to_string())
80///     .build()?;
81///
82/// match client.get::<serde_json::Value>("/servers").await {
83///     Ok(servers) => println!("Got servers: {:?}", servers),
84///     Err(HetznerError::Unauthorized) => {
85///         eprintln!("Invalid API token");
86///     }
87///     Err(HetznerError::RateLimited { retry_after }) => {
88///         eprintln!("Rate limited, retry after: {:?}", retry_after);
89///     }
90///     Err(e) => eprintln!("Other error: {}", e),
91/// }
92/// # Ok(())
93/// # }
94/// ```
95#[derive(Error, Debug)]
96pub enum HetznerError {
97    /// HTTP request failed due to network or connection issues.
98    ///
99    /// This typically indicates network problems, DNS failures, or connection timeouts.
100    #[error("HTTP request failed: {0}")]
101    Http(#[from] reqwest::Error),
102
103    /// Hetzner API returned an error response.
104    ///
105    /// Contains the error code and message from the API.
106    #[error("API error [{code}]: {message}")]
107    Api {
108        /// Error code from Hetzner API (e.g., "invalid_input", "rate_limit_exceeded")
109        code: String,
110        /// Human-readable error message
111        message: String,
112    },
113
114    /// The requested resource was not found (HTTP 404).
115    ///
116    /// This could mean the resource doesn't exist or was already deleted.
117    #[error("Resource not found: {resource_type} with identifier '{identifier}'")]
118    NotFound {
119        /// Type of resource (e.g., "server", "network", "firewall")
120        resource_type: String,
121        /// The identifier used to lookup the resource
122        identifier: String,
123    },
124
125    /// Resource already exists with the same name (HTTP 409).
126    #[error("Resource already exists: {resource_type} named '{name}'")]
127    AlreadyExists {
128        /// Type of resource that already exists
129        resource_type: String,
130        /// Name of the existing resource
131        name: String,
132    },
133
134    /// Invalid input parameter or configuration.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use lmrc_hetzner::HetznerError;
140    ///
141    /// let err = HetznerError::InvalidParameter {
142    ///     parameter: "ip_range".to_string(),
143    ///     reason: "Must be a valid CIDR notation (e.g., 10.0.0.0/16)".to_string(),
144    /// };
145    /// ```
146    #[error("Invalid parameter '{parameter}': {reason}")]
147    InvalidParameter {
148        /// Name of the invalid parameter
149        parameter: String,
150        /// Explanation of why it's invalid
151        reason: String,
152    },
153
154    /// Validation failed for input data.
155    #[error("Validation failed: {0}")]
156    Validation(String),
157
158    /// Operation timed out waiting for completion.
159    #[error("Operation timeout: {operation} exceeded {timeout_secs}s")]
160    Timeout {
161        /// Description of the operation that timed out
162        operation: String,
163        /// Timeout duration in seconds
164        timeout_secs: u64,
165    },
166
167    /// JSON serialization or deserialization failed.
168    #[error("Serialization error: {0}")]
169    Serialization(#[from] serde_json::Error),
170
171    /// Authentication failed (HTTP 401).
172    ///
173    /// Usually indicates an invalid or expired API token.
174    #[error("Authentication failed: invalid or missing API token")]
175    Unauthorized,
176
177    /// Permission denied (HTTP 403).
178    ///
179    /// The API token is valid but lacks permission for this operation.
180    #[error("Permission denied: {details}")]
181    Forbidden {
182        /// Details about what permission is lacking
183        details: String,
184    },
185
186    /// Rate limit exceeded (HTTP 429).
187    ///
188    /// The API has rate limits. Wait before retrying.
189    #[error("Rate limit exceeded, retry after {retry_after:?}")]
190    RateLimited {
191        /// Suggested time to wait before retrying
192        retry_after: Option<u64>,
193    },
194
195    /// Server error from Hetzner API (HTTP 5xx).
196    #[error("Server error (HTTP {status_code}): {details}")]
197    ServerError {
198        /// HTTP status code
199        status_code: u16,
200        /// Error details if available
201        details: String,
202    },
203
204    /// Invalid API token format.
205    #[error("Invalid API token format: {0}")]
206    InvalidToken(String),
207
208    /// Builder configuration error.
209    #[error("Builder error: {0}")]
210    BuilderError(String),
211}
212
213/// Result type alias for operations that may return a [`HetznerError`].
214pub type Result<T> = std::result::Result<T, HetznerError>;
215
216/// Common response wrapper from Hetzner API
217#[allow(dead_code)]
218#[derive(Debug, Serialize, Deserialize)]
219pub struct ApiResponse<T> {
220    #[serde(flatten)]
221    pub data: T,
222}
223
224/// Error response from Hetzner API
225#[derive(Debug, Serialize, Deserialize)]
226pub struct ApiError {
227    pub error: ErrorDetails,
228}
229
230#[derive(Debug, Serialize, Deserialize)]
231pub struct ErrorDetails {
232    pub code: String,
233    pub message: String,
234}
235
236/// Server resource
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Server {
239    pub id: u64,
240    pub name: String,
241    pub status: String,
242    pub public_net: PublicNet,
243    pub private_net: Vec<PrivateNet>,
244    pub server_type: ServerType,
245    pub datacenter: Datacenter,
246    pub image: Option<Image>,
247    #[serde(default)]
248    pub labels: std::collections::HashMap<String, String>,
249    pub created: String,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct PublicNet {
254    pub ipv4: IpInfo,
255    pub ipv6: Option<IpInfo>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct IpInfo {
260    pub ip: String,
261    #[serde(default)]
262    pub blocked: bool,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct PrivateNet {
267    pub network: u64,
268    pub ip: String,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ServerType {
273    pub id: u64,
274    pub name: String,
275    pub description: String,
276    pub cores: u32,
277    pub memory: f64,
278    pub disk: u64,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct Datacenter {
283    pub id: u64,
284    pub name: String,
285    pub description: String,
286    pub location: Location,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct Location {
291    pub id: u64,
292    pub name: String,
293    pub description: String,
294    pub country: String,
295    pub city: String,
296    pub latitude: f64,
297    pub longitude: f64,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct Image {
302    pub id: Option<u64>,
303    pub name: Option<String>,
304    pub description: String,
305    #[serde(rename = "type")]
306    pub image_type: String,
307}
308
309/// Network resource
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct Network {
312    pub id: u64,
313    pub name: String,
314    pub ip_range: String,
315    pub subnets: Vec<Subnet>,
316    #[serde(default)]
317    pub labels: std::collections::HashMap<String, String>,
318    pub created: String,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct Subnet {
323    pub ip_range: String,
324    pub network_zone: String,
325    #[serde(rename = "type")]
326    pub subnet_type: String,
327}
328
329/// Firewall resource
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Firewall {
332    pub id: u64,
333    pub name: String,
334    pub rules: Vec<FirewallRule>,
335    pub applied_to: Vec<FirewallAppliedTo>,
336    #[serde(default)]
337    pub labels: std::collections::HashMap<String, String>,
338    pub created: String,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct FirewallRule {
343    pub direction: String,
344    pub protocol: String,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub port: Option<String>,
347    pub source_ips: Vec<String>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub destination_ips: Option<Vec<String>>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct FirewallAppliedTo {
354    #[serde(rename = "type")]
355    pub apply_type: String,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub server: Option<FirewallServerRef>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct FirewallServerRef {
362    pub id: u64,
363}
364
365/// Load Balancer resource
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct LoadBalancer {
368    pub id: u64,
369    pub name: String,
370    pub public_net: LbPublicNet,
371    pub private_net: Vec<LbPrivateNet>,
372    pub load_balancer_type: LoadBalancerType,
373    pub location: Location,
374    pub algorithm: Algorithm,
375    pub services: Vec<LbService>,
376    pub targets: Vec<LbTarget>,
377    #[serde(default)]
378    pub labels: std::collections::HashMap<String, String>,
379    pub created: String,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct LbPublicNet {
384    pub ipv4: IpInfo,
385    pub ipv6: Option<IpInfo>,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct LbPrivateNet {
390    pub network: u64,
391    pub ip: String,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct LoadBalancerType {
396    pub id: u64,
397    pub name: String,
398    pub description: String,
399    pub max_connections: u64,
400    pub max_targets: u64,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct Algorithm {
405    #[serde(rename = "type")]
406    pub algorithm_type: String,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct LbService {
411    pub protocol: String,
412    pub listen_port: u16,
413    pub destination_port: u16,
414    pub proxyprotocol: bool,
415    pub http: Option<HttpConfig>,
416    pub health_check: HealthCheck,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct HttpConfig {
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub certificates: Option<Vec<u64>>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub sticky_sessions: Option<bool>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct HealthCheck {
429    pub protocol: String,
430    pub port: u16,
431    pub interval: u64,
432    pub timeout: u64,
433    pub retries: u64,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub http: Option<HttpHealthCheck>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct HttpHealthCheck {
440    pub domain: Option<String>,
441    pub path: String,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub response: Option<String>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub status_codes: Option<Vec<String>>,
446    pub tls: bool,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct LbTarget {
451    #[serde(rename = "type")]
452    pub target_type: String,
453    pub server: Option<LbTargetServer>,
454    pub use_private_ip: bool,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct LbTargetServer {
459    pub id: u64,
460}
461
462/// SSH Key resource
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct SshKey {
465    pub id: u64,
466    pub name: String,
467    pub fingerprint: String,
468    pub public_key: String,
469    #[serde(default)]
470    pub labels: std::collections::HashMap<String, String>,
471    pub created: String,
472}
473
474/// Action resource (for async operations)
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct Action {
477    pub id: u64,
478    pub command: String,
479    pub status: String,
480    pub progress: u8,
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub error: Option<ActionError>,
483    pub started: String,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub finished: Option<String>,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct ActionError {
490    pub code: String,
491    pub message: String,
492}
493
494impl Action {
495    #[allow(dead_code)]
496    pub fn is_finished(&self) -> bool {
497        self.status == "success" || self.status == "error"
498    }
499
500    #[allow(dead_code)]
501    pub fn is_success(&self) -> bool {
502        self.status == "success"
503    }
504
505    #[allow(dead_code)]
506    pub fn is_error(&self) -> bool {
507        self.status == "error"
508    }
509}