alien_core/deployment.rs
1use crate::{
2 ExternalBindings, ImagePullCredentials, ManagementConfig, Platform, Stack, StackSettings,
3 StackState,
4};
5use alien_error::AlienError;
6use bon::Builder;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Deployment status in the deployment lifecycle
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "kebab-case")]
14pub enum DeploymentStatus {
15 Pending,
16 InitialSetup,
17 InitialSetupFailed,
18 Provisioning,
19 ProvisioningFailed,
20 Running,
21 RefreshFailed,
22 UpdatePending,
23 Updating,
24 UpdateFailed,
25 DeletePending,
26 Deleting,
27 DeleteFailed,
28 Deleted,
29}
30
31impl DeploymentStatus {
32 /// Check if deployment is synced (current state matches desired state).
33 ///
34 /// When synced, no more deployment steps are needed *for the current operation*.
35 /// Note: This doesn't mean the deployment is "done forever":
36 /// - `Running` → heartbeats continue, updates can come
37 /// - `*Failed` → can be retried
38 /// - `Deleted` → can be recreated
39 ///
40 /// "Synced" means: "we've reached the goal of the current deployment phase"
41 pub fn is_synced(&self) -> bool {
42 matches!(
43 self,
44 DeploymentStatus::Running
45 | DeploymentStatus::InitialSetupFailed
46 | DeploymentStatus::ProvisioningFailed
47 | DeploymentStatus::UpdateFailed
48 | DeploymentStatus::DeleteFailed
49 | DeploymentStatus::RefreshFailed
50 | DeploymentStatus::Deleted
51 )
52 }
53
54 /// Check if deployment is in a failed state that requires retry to proceed.
55 pub fn is_failed(&self) -> bool {
56 matches!(
57 self,
58 DeploymentStatus::InitialSetupFailed
59 | DeploymentStatus::ProvisioningFailed
60 | DeploymentStatus::UpdateFailed
61 | DeploymentStatus::DeleteFailed
62 | DeploymentStatus::RefreshFailed
63 )
64 }
65}
66
67/// Release metadata
68///
69/// Identifies a specific release version and includes the stack definition.
70/// The deployment engine uses this to track which release is currently deployed
71/// and which is the target.
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
74#[serde(rename_all = "camelCase")]
75pub struct ReleaseInfo {
76 /// Release ID (e.g., rel_xyz)
77 pub release_id: String,
78 /// Version string (e.g., 2.1.0)
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub version: Option<String>,
81 /// Short description of the release
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub description: Option<String>,
84 /// Stack definition for this release
85 pub stack: Stack,
86}
87
88/// AWS-specific environment information
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
91#[serde(rename_all = "camelCase")]
92pub struct AwsEnvironmentInfo {
93 /// AWS account ID
94 pub account_id: String,
95 /// AWS region
96 pub region: String,
97}
98
99/// GCP-specific environment information
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
102#[serde(rename_all = "camelCase")]
103pub struct GcpEnvironmentInfo {
104 /// GCP project number (e.g., "123456789012")
105 pub project_number: String,
106 /// GCP project ID (e.g., "my-project")
107 pub project_id: String,
108 /// GCP region
109 pub region: String,
110}
111
112/// Azure-specific environment information
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
115#[serde(rename_all = "camelCase")]
116pub struct AzureEnvironmentInfo {
117 /// Azure tenant ID
118 pub tenant_id: String,
119 /// Azure subscription ID
120 pub subscription_id: String,
121 /// Azure location/region
122 pub location: String,
123}
124
125/// Local platform environment information
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
128#[serde(rename_all = "camelCase")]
129pub struct LocalEnvironmentInfo {
130 /// Hostname of the machine running the agent
131 pub hostname: String,
132 /// Operating system (e.g., "linux", "macos", "windows")
133 pub os: String,
134 /// Architecture (e.g., "x86_64", "aarch64")
135 pub arch: String,
136}
137
138/// Test platform environment information (mock)
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
140#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
141#[serde(rename_all = "camelCase")]
142pub struct TestEnvironmentInfo {
143 /// Test identifier for this environment
144 pub test_id: String,
145}
146
147/// Platform-specific environment information
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
150#[serde(rename_all = "camelCase", tag = "platform")]
151pub enum EnvironmentInfo {
152 /// AWS environment information
153 Aws(AwsEnvironmentInfo),
154 /// GCP environment information
155 Gcp(GcpEnvironmentInfo),
156 /// Azure environment information
157 Azure(AzureEnvironmentInfo),
158 /// Local platform environment information
159 Local(LocalEnvironmentInfo),
160 /// Test platform environment information (mock)
161 Test(TestEnvironmentInfo),
162}
163
164impl EnvironmentInfo {
165 /// Get the platform for this environment info
166 pub fn platform(&self) -> Platform {
167 match self {
168 EnvironmentInfo::Aws(_) => Platform::Aws,
169 EnvironmentInfo::Gcp(_) => Platform::Gcp,
170 EnvironmentInfo::Azure(_) => Platform::Azure,
171 EnvironmentInfo::Local(_) => Platform::Local,
172 EnvironmentInfo::Test(_) => Platform::Test,
173 }
174 }
175}
176
177/// Runtime metadata for deployment
178///
179/// Stores deployment state that needs to persist across step calls.
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182#[serde(rename_all = "camelCase")]
183pub struct RuntimeMetadata {
184 /// Hash of the environment variables snapshot that was last synced to the vault
185 /// Used to avoid redundant sync operations during incremental deployment
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub last_synced_env_vars_hash: Option<String>,
188
189 /// The prepared (mutated) stack from the last successful deployment phase
190 /// This is the stack AFTER mutations have been applied (with service accounts, vault, etc.)
191 /// Used for compatibility checks during updates to compare mutated stacks
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub prepared_stack: Option<Stack>,
194}
195
196/// Certificate status in the certificate lifecycle
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
198#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
199#[serde(rename_all = "kebab-case")]
200pub enum CertificateStatus {
201 Pending,
202 Issued,
203 Renewing,
204 RenewalFailed,
205 Failed,
206 Deleting,
207}
208
209/// DNS record status in the DNS lifecycle
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
212#[serde(rename_all = "lowercase")]
213pub enum DnsRecordStatus {
214 Pending,
215 Active,
216 Updating,
217 Deleting,
218 Failed,
219}
220
221/// Certificate and DNS metadata for a public resource.
222///
223/// Includes decrypted certificate data for issued certificates.
224/// Private keys are deployment-scoped secrets (like environment variables).
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
226#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
227#[serde(rename_all = "camelCase")]
228pub struct ResourceDomainInfo {
229 /// Fully qualified domain name.
230 pub fqdn: String,
231 /// Certificate ID (for tracking/logging).
232 pub certificate_id: String,
233 /// Current certificate status
234 pub certificate_status: CertificateStatus,
235 /// Current DNS record status
236 pub dns_status: DnsRecordStatus,
237 /// Last DNS error message. Present when DNS previously failed, even if status
238 /// was reset to pending for retry. Used to surface actionable error context
239 /// in WaitingForDns failure messages.
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub dns_error: Option<String>,
242 /// Full PEM certificate chain (only present if status is "issued").
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub certificate_chain: Option<String>,
245 /// Decrypted private key (only present if status is "issued").
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub private_key: Option<String>,
248 /// ISO 8601 timestamp when certificate was issued (for renewal detection).
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub issued_at: Option<String>,
251}
252
253/// Domain metadata for auto-managed public resources (no private keys).
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
256#[serde(rename_all = "camelCase")]
257pub struct DomainMetadata {
258 /// Base domain for auto-generated domains (e.g., "vpc.direct").
259 pub base_domain: String,
260 /// Agent public subdomain (e.g., "k8f2j3").
261 pub public_subdomain: String,
262 /// Hosted zone ID for DNS records.
263 pub hosted_zone_id: String,
264 /// Metadata per resource ID.
265 pub resources: HashMap<String, ResourceDomainInfo>,
266}
267
268/// Deployment state
269///
270/// Represents the current state of deployed infrastructure, including release tracking.
271/// This is platform-agnostic - no backend IDs or database relationships.
272///
273/// The deployment engine manages releases internally: when a deployment succeeds,
274/// it promotes `target_release` to `current_release` and clears `target_release`.
275#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
276#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
277#[serde(rename_all = "camelCase")]
278pub struct DeploymentState {
279 /// Current lifecycle phase
280 pub status: DeploymentStatus,
281 /// Target cloud platform (AWS, GCP, Azure, Kubernetes)
282 pub platform: Platform,
283 /// Currently deployed release (None for first deployment)
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub current_release: Option<ReleaseInfo>,
286 /// Target release to deploy (None when synced with current)
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub target_release: Option<ReleaseInfo>,
289 /// Infrastructure resource tracking (which resources exist, their status, outputs)
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub stack_state: Option<StackState>,
292 /// Cloud account details (account ID, project number, region)
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub environment_info: Option<EnvironmentInfo>,
295 /// Deployment-specific data (prepared stacks, phase tracking, etc.)
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub runtime_metadata: Option<RuntimeMetadata>,
298 /// Whether a retry has been requested for a failed deployment
299 /// When true and status is a failed state, the deployment system will retry failed resources
300 #[serde(default, skip_serializing_if = "is_false")]
301 pub retry_requested: bool,
302}
303
304/// Type of environment variable
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
307#[serde(rename_all = "lowercase")]
308pub enum EnvironmentVariableType {
309 /// Plain variable (injected directly into function config)
310 Plain,
311 /// Secret variable (stored in vault, loaded at runtime)
312 Secret,
313}
314
315/// Environment variable for deployment
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
318#[serde(rename_all = "camelCase")]
319pub struct EnvironmentVariable {
320 /// Variable name
321 pub name: String,
322 /// Variable value (decrypted - deployment has access to decryption keys)
323 pub value: String,
324 /// Variable type (plain or secret)
325 #[serde(rename = "type")]
326 pub var_type: EnvironmentVariableType,
327 /// Target resource patterns (null = all resources, Some = wildcard patterns)
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub target_resources: Option<Vec<String>>,
330}
331
332/// Snapshot of environment variables at a point in time
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
335#[serde(rename_all = "camelCase")]
336pub struct EnvironmentVariablesSnapshot {
337 /// Environment variables in the snapshot
338 pub variables: Vec<EnvironmentVariable>,
339 /// Deterministic hash of all variables (for change detection)
340 pub hash: String,
341 /// ISO 8601 timestamp when snapshot was created
342 pub created_at: String,
343}
344
345/// Artifact registry configuration for pulling container images.
346///
347/// Used when the deployment needs to pull images from a manager's artifact registry.
348/// This is required for Local platform and can optionally be used by cloud platforms
349/// instead of native registry mechanisms (ECR/GCR/ACR).
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
351#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
352#[serde(rename_all = "camelCase")]
353pub struct ArtifactRegistryConfig {
354 /// Manager base URL for fetching credentials and accessing the registry
355 pub manager_url: String,
356 /// Optional authentication token (JWT) for agent manager API access
357 /// When present, must be included in Authorization header as "Bearer {token}"
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub auth_token: Option<String>,
360}
361
362/// OTLP log export configuration for a deployment.
363///
364/// When set, all compute workloads (containers and horizond VM agents) export
365/// their logs to the given endpoint via OTLP/HTTP.
366///
367/// The `logs_auth_header` is stored as plain text in DeploymentConfig because
368/// alien-runtime reads `OTEL_EXPORTER_OTLP_HEADERS` at tracing-init time,
369/// before vault secrets load. For horizond, the infra controller writes the
370/// same value to the cloud vault (same pattern as the machine token) and the
371/// startup script fetches it at boot via IAM.
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
374#[serde(rename_all = "camelCase")]
375pub struct OtlpConfig {
376 /// Full OTLP logs endpoint URL.
377 /// Example: "https://<agent-manager-host>/v1/logs"
378 pub logs_endpoint: String,
379 /// Auth header value in "key=value,..." format used for container OTLP env var injection.
380 ///
381 /// `alien-deployment` injects this as the `OTEL_EXPORTER_OTLP_HEADERS` plain env var
382 /// into all containers. It must be plain (not a vault secret) because alien-runtime
383 /// reads `OTEL_EXPORTER_OTLP_HEADERS` at tracing-init time, before vault secrets load.
384 ///
385 /// horizond VM agents do NOT use this field directly. The ContainerCluster infra
386 /// controller writes the same value to the cloud vault (GCP: Secret Manager,
387 /// AWS: Secrets Manager, Azure: Key Vault) and the startup script fetches it at
388 /// boot via IAM — the same pattern as the machine token.
389 ///
390 /// Example: "authorization=Bearer <write-token>"
391 pub logs_auth_header: String,
392 /// Full OTLP metrics endpoint URL (optional).
393 /// When set, horizond exports its own VM/container orchestration metrics here.
394 /// Example: "https://api.axiom.co/v1/metrics"
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub metrics_endpoint: Option<String>,
397 /// Auth header value for the metrics endpoint in "key=value,..." format (optional).
398 ///
399 /// When absent, `logs_auth_header` is reused for metrics — suitable when the same
400 /// credential covers both signals. When present (e.g. Axiom with separate datasets),
401 /// this value is used exclusively for metrics.
402 ///
403 /// Example: "authorization=Bearer <token>,x-axiom-dataset=<metrics-dataset>"
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub metrics_auth_header: Option<String>,
406}
407
408/// Configuration for a single Horizon cluster.
409///
410/// Contains the cluster ID and management token needed to interact with
411/// the Horizon control plane API for container operations.
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
414#[serde(rename_all = "camelCase")]
415pub struct HorizonClusterConfig {
416 /// Cluster ID (deterministic: workspace/project/agent/resourceid)
417 pub cluster_id: String,
418
419 /// Management token for API access (hm_...)
420 /// Used by alien-deployment controllers to create/update containers
421 pub management_token: String,
422 // Note: Machine token (hj_...) is NOT in DeploymentConfig
423 // It's added to environmentVariables snapshot as a built-in secret variable
424 // and synced to vault (Parameter Store/Secret Manager/Key Vault)
425}
426
427/// Horizon configuration for container orchestration.
428///
429/// Contains all the information needed for Alien to interact with Horizon
430/// clusters during deployment. Each ContainerCluster resource gets its own
431/// entry in the clusters map.
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
433#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
434#[serde(rename_all = "camelCase")]
435pub struct HorizonConfig {
436 /// Horizon API base URL (e.g., "https://horizon.alien.dev")
437 pub url: String,
438
439 /// Base URL for downloading the horizond binary, without arch suffix.
440 ///
441 /// Each cloud controller appends `/linux-{arch}/horizond` to construct the
442 /// final download URL used in VM startup scripts.
443 ///
444 /// Production example: "https://releases.alien.dev/horizond/v0.3.0"
445 /// Dev example (ngrok): "https://abc123.ngrok.io"
446 pub horizond_download_base_url: String,
447
448 /// ETag of the horizond binary fetched from the releases server — used as a
449 /// change-detection signal only. nginx auto-generates ETags from mtime+size,
450 /// so every `cargo zigbuild` changes this value and triggers a rolling update.
451 ///
452 /// Optional: when absent (releases server unreachable), change detection
453 /// falls back to URL-only (sufficient for versioned production releases).
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub horizond_binary_hash: Option<String>,
456
457 /// Cluster configurations (one per ContainerCluster resource)
458 /// Key: ContainerCluster resource ID from stack
459 /// Value: Cluster ID and management token for that cluster
460 pub clusters: HashMap<String, HorizonClusterConfig>,
461}
462
463/// Compute backend for Container and Function resources.
464///
465/// Determines how compute workloads are orchestrated on cloud platforms.
466/// When None, the platform default is used (Horizon for cloud platforms).
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
468#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
469#[serde(tag = "type", rename_all = "camelCase")]
470pub enum ComputeBackend {
471 /// VMs with Horizon orchestration (default for cloud platforms)
472 Horizon(HorizonConfig),
473 // Future backends:
474 // /// Deploy to existing Kubernetes cluster (EKS/GKE/AKS)
475 // Kubernetes(KubernetesCredentials),
476 // /// AWS ECS Fargate (serverless containers)
477 // EcsFargate,
478}
479
480/// Deployment configuration
481///
482/// Configuration for how to perform the deployment.
483/// Note: Credentials (ClientConfig) are passed separately to step() function.
484#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
485#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
486#[serde(rename_all = "camelCase")]
487pub struct DeploymentConfig {
488 /// User-customizable deployment settings (network, deployment model, approvals).
489 /// Provided by customer via CloudFormation, Terraform, CLI, or Helm.
490 #[serde(default)]
491 pub stack_settings: StackSettings,
492 /// Platform service account/role that will manage the infrastructure remotely.
493 /// Derived from Agent Manager's ServiceAccount, not user-specified.
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub management_config: Option<ManagementConfig>,
496 /// Environment variables snapshot
497 pub environment_variables: EnvironmentVariablesSnapshot,
498 /// Allow frozen resource changes during updates
499 /// When true, skips the frozen resources compatibility check.
500 /// This requires running with elevated cloud credentials.
501 #[serde(default, skip_serializing_if = "is_false")]
502 pub allow_frozen_changes: bool,
503 /// Artifact registry configuration for pulling container images.
504 /// Required for Local platform, optional for cloud platforms.
505 /// When present, the agent will fetch credentials from the agent manager
506 /// before pulling images, enabling centralized registry access control.
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub artifact_registry: Option<ArtifactRegistryConfig>,
509 /// Compute backend for Container and Function resources.
510 /// When None, the platform default is used (Horizon for cloud platforms).
511 /// Contains cluster IDs and management tokens for container orchestration.
512 /// Machine tokens are stored in environment_variables as built-in secret vars.
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub compute_backend: Option<ComputeBackend>,
515 /// External bindings for pre-existing services.
516 /// Required for Kubernetes platform (all infrastructure resources).
517 /// Optional for cloud platforms (override specific resources).
518 #[serde(default)]
519 pub external_bindings: ExternalBindings,
520 /// Image pull credentials for private container registries.
521 /// Used when pulling images from registries that require authentication.
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub image_pull_credentials: Option<ImagePullCredentials>,
524 /// Public URLs for exposed resources (optional override for all platforms).
525 ///
526 /// - **Kubernetes**: Pre-computed by Helm from services config (highly recommended)
527 /// - **Cloud**: Optional override of domain_metadata or load balancer DNS
528 /// - **Local**: Optional override of dynamic localhost URLs
529 ///
530 /// If not set, platforms determine public URLs from other sources:
531 /// - Cloud: domain_metadata FQDN or load balancer DNS
532 /// - Local: http://localhost:{allocated_port}
533 /// - Kubernetes: None (unless provided by Helm)
534 ///
535 /// Key: resource ID, Value: public URL (e.g., "https://api.acme.com")
536 #[serde(skip_serializing_if = "Option::is_none")]
537 pub public_urls: Option<HashMap<String, String>>,
538 /// Domain metadata for auto-managed public resources (AWS/GCP/Azure).
539 /// Contains certificate data for cloud provider import and renewal detection.
540 /// Not used by Kubernetes (uses TLS Secrets) or Local (no TLS) platforms.
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub domain_metadata: Option<DomainMetadata>,
543 /// OTLP observability configuration for log export (optional).
544 ///
545 /// When set, alien-deployment injects OTEL_EXPORTER_OTLP_* env vars into
546 /// container/function configs, and alien-infra embeds --otlp-logs-* flags
547 /// into horizond VM startup scripts.
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub monitoring: Option<OtlpConfig>,
550}
551
552/// Result of a deployment step
553///
554/// Contains the complete next deployment state along with hints for the platform.
555/// This replaces the old delta-based `DeploymentStateUpdate` approach.
556#[derive(Debug, Clone, Serialize, Deserialize)]
557#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
558#[serde(rename_all = "camelCase")]
559pub struct DeploymentStepResult {
560 /// The complete next deployment state
561 pub state: DeploymentState,
562
563 /// Error that occurred during this step (if any)
564 /// - `None`: No error, step succeeded
565 /// - `Some(error)`: Step failed or encountered an error
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub error: Option<AlienError>,
568
569 /// Suggested delay before next step (optimization hint)
570 /// - `None`: No suggested delay, can poll immediately
571 /// - `Some(ms)`: Wait this many milliseconds before next step
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub suggested_delay_ms: Option<u64>,
574
575 /// Whether to update heartbeat timestamp (monitoring signal)
576 /// - `false`: Don't update heartbeat (default for most steps)
577 /// - `true`: Update lastHeartbeatAt (for successful health checks in Running state)
578 #[serde(default, skip_serializing_if = "is_false")]
579 pub update_heartbeat: bool,
580}
581
582fn is_false(b: &bool) -> bool {
583 !*b
584}