Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client-sdks/platform/openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client-sdks/platform/rust/openapi-3.0.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client-sdks/platform/rust/openapi.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion crates/alien-cli/src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,6 @@ pub async fn deploy_task(args: DeployArgs, ctx: ExecutionMode) -> Result<()> {
external_bindings: None,
kubernetes: None,
public_endpoints: None,
logs: None,
};

let create_response = sdk_client
Expand Down
140 changes: 138 additions & 2 deletions crates/alien-cli/src/commands/deployments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use alien_manager_api::SdkResultExt as ManagerSdkResultExt;
use alien_manager_api::SdkResultExtReadingBody as _;
use alien_platform_api::types::{
CreateDeploymentTokenId, CreateDeploymentTokenRequest, CreateDeploymentTokenWorkspace,
CreateDeploymentWorkspace, DeploymentListItemResponse, GetDeploymentId, GetDeploymentWorkspace,
CreateDeploymentWorkspace, DaemonUpdatePolicy, DaemonUpdatePolicyInner, DaemonUpdateSettings,
DeploymentListItemResponse, GetDeploymentId, GetDeploymentWorkspace,
ListDeploymentsIncludeItem, NewDeploymentRequest, PinDeploymentReleaseId,
PinDeploymentReleaseWorkspace, PinReleaseRequest, PinReleaseRequestReleaseId,
};
Expand Down Expand Up @@ -79,6 +80,7 @@ impl DeploymentsArgs {
| DeploymentsCmd::Redeploy { json: true, .. }
| DeploymentsCmd::Pin { json: true, .. }
| DeploymentsCmd::SetChannel { json: true, .. }
| DeploymentsCmd::DaemonUpdates { json: true, .. }
)
}
}
Expand Down Expand Up @@ -222,6 +224,24 @@ pub enum DeploymentsCmd {
#[arg(long)]
json: bool,
},
/// Configure deployment-scoped machine daemon updates
DaemonUpdates {
/// Deployment ID, or <deployment-group-name>/<deployment-name>
id: String,
/// Enable guarded updates. The currently supported value is 1.
#[arg(
long,
conflicts_with = "parallel",
required_unless_present = "parallel"
)]
max_unavailable: Option<u32>,
/// Restore parallel daemon updates.
#[arg(long, conflicts_with = "max_unavailable")]
parallel: bool,
/// Print the resulting setting as machine-readable JSON.
#[arg(long)]
json: bool,
},
/// Redeploy a deployment with the same release
Redeploy {
/// Deployment ID, or <deployment-group-name>/<deployment-name>
Expand Down Expand Up @@ -410,6 +430,57 @@ pub async fn deployments_task(args: DeploymentsArgs, ctx: ExecutionMode) -> Resu
let manager = resolve_manager_client(&ctx, None, !json).await?;
retry_deployment_task(&manager, &id, json).await
}
DeploymentsCmd::DaemonUpdates {
id,
max_unavailable,
parallel,
json,
} => {
if !ctx.is_platform() {
return Err(AlienError::new(ErrorData::ValidationError {
field: "command".to_string(),
message: "Daemon update policy requires platform mode.".to_string(),
}));
}
if max_unavailable.is_some_and(|value| value != 1) {
return Err(AlienError::new(ErrorData::ValidationError {
field: "max-unavailable".to_string(),
message: "The currently supported value is 1.".to_string(),
}));
}
let workspace = ctx.resolve_workspace_with_bootstrap(!json).await?;
let client = ctx.sdk_client().await?;
let deployment = crate::platform_deployment_resolver::resolve(
&ctx, &client, &workspace, &id, None, !json,
)
.await?;
let body = daemon_update_settings(parallel);
let response = client
.update_deployment_daemon_updates()
.id(String::from(deployment.id))
.workspace(&workspace)
.body(&body)
.send()
.await
.into_sdk_error()
.context(ErrorData::ApiRequestFailed {
message: "updating deployment daemon policy".to_string(),
url: None,
})?
.into_inner();
if json {
return print_json(&response);
}
println!(
"{}",
success_line(if parallel {
"Parallel daemon updates enabled."
} else {
"Guarded daemon updates enabled (max unavailable: 1)."
})
);
Ok(())
}
DeploymentsCmd::Redeploy { id, json } => {
#[cfg(feature = "platform")]
if ctx.is_platform() {
Expand Down Expand Up @@ -546,6 +617,18 @@ pub async fn deployments_task(args: DeploymentsArgs, ctx: ExecutionMode) -> Resu
}
}

fn daemon_update_settings(parallel: bool) -> DaemonUpdateSettings {
DaemonUpdateSettings {
daemon_update_policy: if parallel {
DaemonUpdatePolicy(None)
} else {
DaemonUpdatePolicy(Some(DaemonUpdatePolicyInner {
max_unavailable: std::num::NonZeroU64::MIN,
}))
},
}
}

async fn machines_inventory_task(
client: &alien_platform_api::Client,
workspace: &str,
Expand Down Expand Up @@ -1887,7 +1970,6 @@ async fn create_deployment_task(
external_bindings: None,
kubernetes: None,
public_endpoints: None,
logs: None,
};

let request = NewDeploymentRequest {
Expand Down Expand Up @@ -2296,6 +2378,60 @@ mod tests {
));
}

#[test]
fn daemon_update_policy_requires_an_explicit_mode() {
let rolling = DeploymentsArgs::try_parse_from([
"deployments",
"daemon-updates",
"production/api",
"--max-unavailable",
"1",
"--json",
])
.expect("rolling update policy should parse");
assert!(matches!(
rolling.cmd,
DeploymentsCmd::DaemonUpdates {
max_unavailable: Some(1),
parallel: false,
json: true,
..
}
));

let parallel = DeploymentsArgs::try_parse_from([
"deployments",
"daemon-updates",
"production/api",
"--parallel",
])
.expect("parallel policy should parse");
assert!(matches!(
parallel.cmd,
DeploymentsCmd::DaemonUpdates {
max_unavailable: None,
parallel: true,
..
}
));

assert!(DeploymentsArgs::try_parse_from([
"deployments",
"daemon-updates",
"production/api"
])
.is_err());

assert_eq!(
serde_json::to_value(daemon_update_settings(false)).unwrap(),
serde_json::json!({ "daemonUpdatePolicy": { "maxUnavailable": 1 } })
);
assert_eq!(
serde_json::to_value(daemon_update_settings(true)).unwrap(),
serde_json::json!({ "daemonUpdatePolicy": null })
);
}

#[test]
fn ready_wait_requires_release_convergence() {
let running = parse_deployment_status("running").expect("known status");
Expand Down
4 changes: 4 additions & 0 deletions crates/alien-core/src/bin/schema_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ use utoipa::OpenApi;
ObservedCounts,
ResourceHeartbeat,
ResourceHeartbeatData,
DaemonRolloutHeartbeatData,
DaemonRolloutHeartbeatStatus,
DaemonMachineRolloutHeartbeatData,
DaemonMachineRolloutHeartbeatStatus,
HeartbeatBackend,
HeartbeatCollectionIssue,
HeartbeatCollectionIssueReason,
Expand Down
55 changes: 55 additions & 0 deletions crates/alien-core/src/heartbeat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,8 @@ pub struct AwsDaemonHeartbeatData {
#[serde(default)]
pub observed_image: Option<String>,
pub latest_update_timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout: Option<DaemonRolloutHeartbeatData>,
pub daemon_instances: Vec<ManagedRuntimeUnitStatus>,
pub events: Vec<ManagedRuntimeEventSnapshot>,
}
Expand All @@ -791,6 +793,8 @@ pub struct GcpDaemonHeartbeatData {
#[serde(default)]
pub observed_image: Option<String>,
pub latest_update_timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout: Option<DaemonRolloutHeartbeatData>,
pub daemon_instances: Vec<ManagedRuntimeUnitStatus>,
pub events: Vec<ManagedRuntimeEventSnapshot>,
}
Expand All @@ -815,6 +819,8 @@ pub struct AzureDaemonHeartbeatData {
#[serde(default)]
pub observed_image: Option<String>,
pub latest_update_timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout: Option<DaemonRolloutHeartbeatData>,
pub daemon_instances: Vec<ManagedRuntimeUnitStatus>,
pub events: Vec<ManagedRuntimeEventSnapshot>,
}
Expand All @@ -839,10 +845,59 @@ pub struct MachinesDaemonHeartbeatData {
#[serde(default)]
pub observed_image: Option<String>,
pub latest_update_timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout: Option<DaemonRolloutHeartbeatData>,
pub daemon_instances: Vec<ManagedRuntimeUnitStatus>,
pub events: Vec<ManagedRuntimeEventSnapshot>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DaemonRolloutHeartbeatData {
pub id: String,
pub target_version: String,
pub status: DaemonRolloutHeartbeatStatus,
pub max_unavailable: u32,
pub started_at: String,
pub deadline_at: String,
pub updated_at: String,
pub machines: Vec<DaemonMachineRolloutHeartbeatData>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum DaemonRolloutHeartbeatStatus {
Running,
Succeeded,
Failed,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DaemonMachineRolloutHeartbeatData {
pub machine_id: String,
pub status: DaemonMachineRolloutHeartbeatStatus,
pub replica_id: Option<String>,
pub error: Option<String>,
pub updated_at: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum DaemonMachineRolloutHeartbeatStatus {
Queued,
Stopping,
Starting,
Verifying,
Succeeded,
Failed,
Skipped,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/generated/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading