From 4e1001f54317f3b1e2a2c57e0ee6e13e7587fa2f Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Sat, 29 Aug 2026 18:28:44 -0700 Subject: [PATCH 1/4] feat(deploy-cli): update active hosted compute --- Cargo.lock | 1 + crates/alien-deploy-cli/Cargo.toml | 3 + crates/alien-deploy-cli/src/commands/up.rs | 118 +++++++++++++++++++++ 3 files changed, 122 insertions(+) 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), From 87f5096ddfa39a0a0d631ced517fd0d026148b50 Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Sat, 29 Aug 2026 18:30:28 -0700 Subject: [PATCH 2/4] chore: retrigger checks on compliant branch From ac6f3b12edef13117e7afe133531d00c01144fe9 Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Sat, 29 Aug 2026 18:34:16 -0700 Subject: [PATCH 3/4] fix(deploy-cli): reject mixed hosted setting updates --- crates/alien-deploy-cli/src/commands/up.rs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/alien-deploy-cli/src/commands/up.rs b/crates/alien-deploy-cli/src/commands/up.rs index ce7060514..dc7288922 100644 --- a/crates/alien-deploy-cli/src/commands/up.rs +++ b/crates/alien-deploy-cli/src/commands/up.rs @@ -344,6 +344,28 @@ mod tests { mock.assert_async().await; } + #[test] + fn hosted_compute_update_rejects_other_setting_changes() { + let current: StackSettings = serde_json::from_value(serde_json::json!({ + "deploymentModel": "push", + "compute": { "pools": {} }, + "network": { "type": "use-default" } + })) + .expect("valid current stack settings"); + let requested: StackSettings = serde_json::from_value(serde_json::json!({ + "deploymentModel": "push", + "compute": { + "pools": { + "workers": { "mode": "fixed", "machines": 2, "machine": "m7i.large" } + } + }, + "network": { "type": "create" } + })) + .expect("valid requested stack settings"); + + assert!(has_non_compute_changes(¤t, &requested)); + } + #[test] fn stable_channel_accepts_exact_semver_tag() { assert_eq!( @@ -1329,6 +1351,12 @@ pub async fn up_command(args: UpArgs, embedded_config: Option<&DeployCliConfig>) message: "Failed to deserialize current stack settings".to_string(), })? .unwrap_or_default(); + if has_non_compute_changes(¤t_stack_settings, &stack_settings) { + return Err(AlienError::new(ErrorData::ConfigurationError { + message: "An active hosted deployment can update compute settings only; apply other deployment-setting changes separately." + .to_string(), + })); + } if current_stack_settings.compute != stack_settings.compute { update_hosted_compute_settings( &resolved.base_url, @@ -2683,6 +2711,12 @@ pub(crate) fn create_manager_http_client(token: &str) -> Result }) } +fn has_non_compute_changes(current: &StackSettings, requested: &StackSettings) -> bool { + let mut requested_without_compute = requested.clone(); + requested_without_compute.compute = current.compute.clone(); + requested_without_compute != *current +} + async fn update_hosted_compute_settings( base_url: &str, token: &str, From eb8cffd465141fb3460058b933ac481029fd8c48 Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Sat, 29 Aug 2026 18:38:33 -0700 Subject: [PATCH 4/4] fix(deploy-cli): compare only explicit hosted settings --- crates/alien-deploy-cli/src/commands/up.rs | 42 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/alien-deploy-cli/src/commands/up.rs b/crates/alien-deploy-cli/src/commands/up.rs index dc7288922..792540e7f 100644 --- a/crates/alien-deploy-cli/src/commands/up.rs +++ b/crates/alien-deploy-cli/src/commands/up.rs @@ -363,7 +363,12 @@ mod tests { })) .expect("valid requested stack settings"); - assert!(has_non_compute_changes(¤t, &requested)); + assert!(has_explicit_non_compute_changes( + ¤t, &requested, true, false, false + )); + assert!(!has_explicit_non_compute_changes( + ¤t, &requested, false, false, false + )); } #[test] @@ -1351,7 +1356,26 @@ pub async fn up_command(args: UpArgs, embedded_config: Option<&DeployCliConfig>) message: "Failed to deserialize current stack settings".to_string(), })? .unwrap_or_default(); - if has_non_compute_changes(¤t_stack_settings, &stack_settings) { + let explicit_network = args.network.network_mode != NetworkMode::Auto + || deploy_config + .as_ref() + .and_then(|config| config.network.as_ref()) + .is_some(); + let explicit_updates = deploy_config + .as_ref() + .and_then(|config| config.updates.as_ref()) + .is_some(); + let explicit_telemetry = deploy_config + .as_ref() + .and_then(|config| config.telemetry.as_ref()) + .is_some(); + if has_explicit_non_compute_changes( + ¤t_stack_settings, + &stack_settings, + explicit_network, + explicit_updates, + explicit_telemetry, + ) { return Err(AlienError::new(ErrorData::ConfigurationError { message: "An active hosted deployment can update compute settings only; apply other deployment-setting changes separately." .to_string(), @@ -2711,10 +2735,16 @@ pub(crate) fn create_manager_http_client(token: &str) -> Result }) } -fn has_non_compute_changes(current: &StackSettings, requested: &StackSettings) -> bool { - let mut requested_without_compute = requested.clone(); - requested_without_compute.compute = current.compute.clone(); - requested_without_compute != *current +fn has_explicit_non_compute_changes( + current: &StackSettings, + requested: &StackSettings, + network: bool, + updates: bool, + telemetry: bool, +) -> bool { + (network && requested.network != current.network) + || (updates && requested.updates != current.updates) + || (telemetry && requested.telemetry != current.telemetry) } async fn update_hosted_compute_settings(