Skip to content

Commit cdadc8c

Browse files
Alex Holmbergclaude
authored andcommitted
fix(hetzner): use availability API for real-time capacity data
Switch back to /api/deployments/availability/* endpoints which check Hetzner's datacenter API for ACTUAL capacity: - /api/deployments/availability/locations - Returns locations with server types that are CURRENTLY available (not just exist) - /api/deployments/availability/server-types - Returns server types sorted by price, filtered to those actually in stock The previous /api/v1/cloud-runner/hetzner/options endpoint returned ALL locations and server types without checking real-time capacity, leading to incorrect recommendations (e.g., showing cx23 as cheapest when it's actually out of stock). Also expanded credential error detection patterns to catch: - 401 status codes - 412 (failedPrecondition) status codes - Various "token" related messages Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c91a09e commit cdadc8c

1 file changed

Lines changed: 31 additions & 46 deletions

File tree

src/wizard/cloud_provider_data.rs

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -193,38 +193,32 @@ fn server_type_to_dynamic(st: &ServerTypeSummary) -> DynamicMachineType {
193193
}
194194
}
195195

196-
/// Fetch Hetzner regions dynamically with real-time availability
196+
/// Fetch Hetzner regions dynamically with REAL-TIME availability
197197
///
198-
/// Uses the /api/v1/cloud-runner/hetzner/options endpoint (same as frontend).
199-
/// Returns regions with availability info directly from Hetzner API.
200-
/// The agent uses this to make smart deployment decisions based on actual capacity.
198+
/// Uses the /api/deployments/availability/locations endpoint which checks
199+
/// Hetzner's datacenter API for actual capacity - not just what exists.
200+
/// Returns only regions where server types are CURRENTLY available.
201201
///
202202
/// # Errors
203203
/// Returns error if credentials are missing or API call fails.
204204
pub async fn get_hetzner_regions_dynamic(
205205
client: &PlatformApiClient,
206206
project_id: &str,
207207
) -> HetznerFetchResult<Vec<DynamicCloudRegion>> {
208-
match client.get_hetzner_options(project_id).await {
209-
Ok(options) => {
210-
let regions: Vec<DynamicCloudRegion> = options.locations.iter().map(|loc| {
211-
DynamicCloudRegion {
212-
id: loc.name.clone(),
213-
name: loc.city.clone(),
214-
location: loc.country.clone(),
215-
network_zone: loc.network_zone.clone(),
216-
// Find server types available at this location
217-
available_server_types: options.server_types.iter()
218-
.filter(|st| st.available_locations.contains(&loc.name))
219-
.map(|st| st.name.clone())
220-
.collect(),
221-
}
222-
}).collect();
223-
HetznerFetchResult::Success(regions)
208+
match client.get_hetzner_locations(project_id).await {
209+
Ok(locations) => {
210+
HetznerFetchResult::Success(locations.iter().map(location_to_dynamic_region).collect())
224211
}
225212
Err(e) => {
226213
let error_msg = e.to_string();
227-
if error_msg.contains("credentials") || error_msg.contains("Unauthorized") || error_msg.contains("token") {
214+
// Check for various credential-related error patterns
215+
if error_msg.contains("credentials")
216+
|| error_msg.contains("Unauthorized")
217+
|| error_msg.contains("token")
218+
|| error_msg.contains("API token")
219+
|| error_msg.contains("401")
220+
|| error_msg.contains("412") // failedPrecondition
221+
{
228222
HetznerFetchResult::NoCredentials
229223
} else {
230224
HetznerFetchResult::ApiError(error_msg)
@@ -233,42 +227,33 @@ pub async fn get_hetzner_regions_dynamic(
233227
}
234228
}
235229

236-
/// Fetch Hetzner server types dynamically with pricing and availability
230+
/// Fetch Hetzner server types dynamically with REAL-TIME availability and pricing
237231
///
238-
/// Uses the /api/v1/cloud-runner/hetzner/options endpoint (same as frontend).
239-
/// Returns server types sorted by monthly price (cheapest first) with
240-
/// real-time availability per region. The agent uses this for cost-optimized
241-
/// resource selection.
232+
/// Uses the /api/deployments/availability/server-types endpoint which returns
233+
/// server types sorted by price with ACTUAL availability per datacenter.
234+
/// Only returns server types that are currently in stock.
242235
///
243236
/// # Errors
244237
/// Returns error if credentials are missing or API call fails.
245238
pub async fn get_hetzner_server_types_dynamic(
246239
client: &PlatformApiClient,
247240
project_id: &str,
248-
_preferred_location: Option<&str>,
241+
preferred_location: Option<&str>,
249242
) -> HetznerFetchResult<Vec<DynamicMachineType>> {
250-
match client.get_hetzner_options(project_id).await {
251-
Ok(options) => {
252-
let mut server_types: Vec<DynamicMachineType> = options.server_types.iter()
253-
.filter(|st| !st.deprecated)
254-
.map(|st| DynamicMachineType {
255-
id: st.name.clone(),
256-
name: st.name.clone(),
257-
cores: st.cores,
258-
memory_gb: st.memory,
259-
disk_gb: st.disk,
260-
price_monthly: st.price_monthly,
261-
price_hourly: st.price_monthly / 730.0, // Approximate hourly from monthly
262-
available_in: st.available_locations.clone(),
263-
})
264-
.collect();
265-
// Sort by price (cheapest first)
266-
server_types.sort_by(|a, b| a.price_monthly.partial_cmp(&b.price_monthly).unwrap());
267-
HetznerFetchResult::Success(server_types)
243+
match client.get_hetzner_server_types(project_id, preferred_location).await {
244+
Ok(server_types) => {
245+
HetznerFetchResult::Success(server_types.iter().map(server_type_to_dynamic).collect())
268246
}
269247
Err(e) => {
270248
let error_msg = e.to_string();
271-
if error_msg.contains("credentials") || error_msg.contains("Unauthorized") || error_msg.contains("token") {
249+
// Check for various credential-related error patterns
250+
if error_msg.contains("credentials")
251+
|| error_msg.contains("Unauthorized")
252+
|| error_msg.contains("token")
253+
|| error_msg.contains("API token")
254+
|| error_msg.contains("401")
255+
|| error_msg.contains("412") // failedPrecondition
256+
{
272257
HetznerFetchResult::NoCredentials
273258
} else {
274259
HetznerFetchResult::ApiError(error_msg)

0 commit comments

Comments
 (0)