diff --git a/Cargo.lock b/Cargo.lock index be75b6d18..cce951b77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ dependencies = [ "getrandom 0.2.17", "hex", "hostname 0.4.2", + "httpmock", "libc", "reqwest 0.12.28", "serde", diff --git a/crates/alien-deploy-cli/Cargo.toml b/crates/alien-deploy-cli/Cargo.toml index 6399ae078..32b5f4bde 100644 --- a/crates/alien-deploy-cli/Cargo.toml +++ b/crates/alien-deploy-cli/Cargo.toml @@ -67,3 +67,6 @@ libc = "0.2" [target.'cfg(windows)'.dependencies] fs2 = "0.4" + +[dev-dependencies] +httpmock = { workspace = true } diff --git a/crates/alien-deploy-cli/src/commands/up.rs b/crates/alien-deploy-cli/src/commands/up.rs index 011299b7c..ce7060514 100644 --- a/crates/alien-deploy-cli/src/commands/up.rs +++ b/crates/alien-deploy-cli/src/commands/up.rs @@ -278,6 +278,7 @@ impl From for NetworkSettings { mod tests { use super::*; use clap::Parser; + use httpmock::{Method::PATCH, MockServer}; use std::io::Write; #[test] @@ -295,6 +296,54 @@ mod tests { assert!(!requires_install_context(Platform::Test)); } + #[tokio::test] + async fn hosted_compute_update_uses_deployment_token_and_exact_payload() { + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(PATCH) + .path("/v1/deployments/dep_example/compute") + .header("authorization", "Bearer deployment-secret") + .json_body(serde_json::json!({ + "compute": { + "pools": { + "workers": { + "mode": "fixed", + "machines": 2, + "machine": "m7i.large" + } + } + } + })); + then.status(200).json_body(serde_json::json!({ + "outcome": "accepted", + "operation": null + })); + }) + .await; + let compute: ComputeSettings = serde_json::from_value(serde_json::json!({ + "pools": { + "workers": { + "mode": "fixed", + "machines": 2, + "machine": "m7i.large" + } + } + })) + .expect("valid compute settings"); + + update_hosted_compute_settings( + &server.base_url(), + "deployment-secret", + "dep_example", + &compute, + ) + .await + .expect("hosted update should succeed"); + + mock.assert_async().await; + } + #[test] fn stable_channel_accepts_exact_semver_tag() { assert_eq!( @@ -1263,6 +1312,44 @@ pub async fn up_command(args: UpArgs, embedded_config: Option<&DeployCliConfig>) })? .into_inner(); + let hosted_platform = + manager_url.trim_end_matches('/') != resolved.base_url.trim_end_matches('/'); + if current_deployment.status == "running" + && init.deployment_model == DeploymentModel::Push + && hosted_platform + && stack_settings.compute.is_some() + { + let current_stack_settings: StackSettings = current_deployment + .stack_settings + .clone() + .map(serde_json::from_value) + .transpose() + .into_alien_error() + .context(ErrorData::ConfigurationError { + message: "Failed to deserialize current stack settings".to_string(), + })? + .unwrap_or_default(); + if current_stack_settings.compute != stack_settings.compute { + update_hosted_compute_settings( + &resolved.base_url, + &effective_token, + &deployment_id, + stack_settings.compute.as_ref().expect("checked above"), + ) + .await?; + output::success(&format!( + "Deployment '{}' compute update was accepted by the hosted manager.", + name + )); + return Ok(()); + } + output::success(&format!( + "Deployment '{}' already has the requested compute settings.", + name + )); + return Ok(()); + } + if let Some(public_endpoints) = public_endpoints.as_ref() { let release_id = current_deployment .desired_release_id @@ -2596,6 +2683,37 @@ pub(crate) fn create_manager_http_client(token: &str) -> Result }) } +async fn update_hosted_compute_settings( + base_url: &str, + token: &str, + deployment_id: &str, + compute: &ComputeSettings, +) -> Result<()> { + let client = create_manager_http_client(token)?; + let url = format!( + "{}/v1/deployments/{}/compute", + base_url.trim_end_matches('/'), + urlencoding::encode(deployment_id) + ); + let response = client + .patch(&url) + .json(&serde_json::json!({ "compute": compute })) + .send() + .await + .into_alien_error() + .context(ErrorData::ConfigurationError { + message: "Failed to update hosted deployment compute settings".to_string(), + })?; + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(AlienError::new(ErrorData::ConfigurationError { + message: format!("Hosted compute update failed with HTTP {status}: {body}"), + })); + } + Ok(()) +} + fn parse_deployment_status(raw_status: &str) -> Result { match raw_status.to_ascii_lowercase().as_str() { "pending" => Ok(DeploymentStatus::Pending),