alien_core/deployment/config.rs
1//! Deployment configuration and OTLP observability settings.
2
3use crate::{ExternalBindings, ManagementConfig, StackSettings};
4use bon::Builder;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::{is_false, ComputeBackend, DomainMetadata, EnvironmentVariablesSnapshot};
9
10/// Deployment configuration
11///
12/// Configuration for how to perform the deployment.
13/// Note: Credentials (ClientConfig) are passed separately to step() function.
14#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
15#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
16#[serde(rename_all = "camelCase")]
17pub struct DeploymentConfig {
18 /// Human-readable deployment name for cloud console metadata.
19 ///
20 /// This is separate from the physical resource prefix in StackState. It is
21 /// used only for display text such as IAM role descriptions, service
22 /// account descriptions, and custom role titles.
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub deployment_name: Option<String>,
25 /// User-customizable deployment settings (network, deployment model, approvals).
26 /// Provided by customer via CloudFormation, Terraform, CLI, or Helm.
27 #[serde(default)]
28 pub stack_settings: StackSettings,
29 /// Platform service account/role that will manage the infrastructure remotely.
30 /// Derived from Manager's ServiceAccount, not user-specified.
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub management_config: Option<ManagementConfig>,
33 /// Environment variables snapshot
34 pub environment_variables: EnvironmentVariablesSnapshot,
35 /// Deployer-provided stack input values, keyed by input id. A resource
36 /// gated with `.enabled(input)` and a Live lifecycle follows these
37 /// values; a missing key falls back to the input's declared boolean
38 /// default. Suppliers must not place secret-kind input values here:
39 /// this map serializes and debug-prints unredacted.
40 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
41 #[builder(default)]
42 pub input_values: HashMap<String, serde_json::Value>,
43 /// Allow frozen resource changes during updates
44 /// When true, skips the frozen resources compatibility check.
45 /// This requires running with elevated cloud credentials.
46 #[serde(default, skip_serializing_if = "is_false")]
47 pub allow_frozen_changes: bool,
48 /// Compute backend for Container and Worker resources.
49 /// When None, the platform default is used for cloud platforms.
50 /// Contains cluster IDs and management tokens for container orchestration.
51 /// Worker runtime credentials are provided through cloud identity and vault-backed secrets.
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub compute_backend: Option<ComputeBackend>,
54 /// External bindings for pre-existing services.
55 /// Required for Kubernetes platform (all infrastructure resources).
56 /// Optional for cloud platforms (override specific resources).
57 #[serde(default)]
58 pub external_bindings: ExternalBindings,
59 /// Cloud platform that owns imported base infrastructure for a Kubernetes
60 /// runtime deployment.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub base_platform: Option<crate::Platform>,
63 /// DNS-style label domain used for Kubernetes resource ownership labels.
64 ///
65 /// Defaults to `alien.dev` when absent. Whitelabeled Operator builds set this
66 /// so generated workloads and optional log collectors share the same label
67 /// namespace.
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub label_domain: Option<String>,
70 /// Kubernetes label selector that narrows which raw resources the observe
71 /// pass reports (e.g. `app.kubernetes.io/part-of=my-app`). `None` observes
72 /// everything in the namespace. Ignored by cloud observers.
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub observe_label_selector: Option<String>,
75 /// When true the observe pass reports raw resources across every namespace
76 /// (cluster scope); otherwise it stays within the operator's own namespace.
77 /// The label selector, if any, still filters within whichever scope applies.
78 /// Ignored by cloud observers.
79 #[serde(default)]
80 #[builder(default)]
81 pub observe_all_namespaces: bool,
82 /// Public endpoint URLs for exposed resources (optional override).
83 ///
84 /// Use this only when a caller already knows the public URL. Managed public
85 /// endpoint flows should prefer `domain_metadata` plus controller-reported
86 /// load balancer outputs so DNS, certificate renewal, and route readiness
87 /// stay tied to the resource state.
88 ///
89 /// If not set, platforms determine public endpoint URLs from other sources:
90 /// - Managed DNS/TLS flows: `domain_metadata` FQDN or load balancer DNS
91 /// - Local: `http://localhost:{allocated_port}`
92 /// - Custom or disabled exposure: no public endpoint URL unless a controller reports one
93 ///
94 /// Outer key: resource ID. Inner key: endpoint name. Value: public URL.
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub public_endpoints: Option<HashMap<String, HashMap<String, String>>>,
97 /// Domain metadata for auto-managed public resources.
98 ///
99 /// Contains generated hostnames, DNS record state, certificate material,
100 /// and renewal markers for platforms that use managed public endpoints.
101 /// Kubernetes uses this only when its exposure mode is `generated`; BYO and
102 /// disabled Kubernetes exposure do not receive managed domain metadata.
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub domain_metadata: Option<DomainMetadata>,
105 /// OTLP observability configuration for log export (optional).
106 ///
107 /// When set, worker runtimes export captured application logs through this
108 /// endpoint. Container orchestrators may use it for their node-level log
109 /// collectors, but app container configs must not receive the auth header.
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub monitoring: Option<OtlpConfig>,
112 /// Manager base URL (e.g., "https://manager.alien.dev").
113 ///
114 /// The manager IS the container registry — its `/v2/` endpoint serves as
115 /// the OCI Distribution API. Controllers derive the proxy host from this
116 /// to configure pull auth (RegistryCredentials, imagePullSecrets).
117 ///
118 /// When None (e.g., `alien dev`), controllers use image URIs as-is.
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub manager_url: Option<String>,
121 /// Deployment token for pull authentication with the manager's registry.
122 ///
123 /// Used by controllers to configure registry credentials so cloud platforms
124 /// and K8s can pull images from the manager's `/v2/` endpoint.
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub deployment_token: Option<String>,
127 /// Native image registry host+prefix for platforms that require it.
128 ///
129 /// Lambda (ECR) and Cloud Run (GAR) require native registry URIs. Other
130 /// runtimes, including Azure Container Apps, pull through the manager's
131 /// registry proxy.
132 ///
133 /// Derived by the manager from the artifact registry binding:
134 /// - ECR: `{account_id}.dkr.ecr.{region}.amazonaws.com/{repository_prefix}`
135 /// - GAR: `{region}-docker.pkg.dev/{project_id}/{repository_name}`
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub native_image_host: Option<String>,
138}
139
140/// Resource-attribute key marking OTLP telemetry as Alien system-component
141/// output (infrastructure daemons and internal runtimes) rather than user
142/// workload. Log consumers — the CLI log viewer and the dashboard — hide
143/// telemetry carrying this attribute by default. The value is the string
144/// `"true"`.
145///
146/// System components set this on their own telemetry's resource attributes so
147/// consumers can filter generically, without enumerating component names.
148pub const ALIEN_SYSTEM_RESOURCE_ATTRIBUTE: &str = "alien.system";
149
150/// OTLP log export configuration for a deployment.
151///
152/// When set, injected compute runtimes export captured application logs
153/// through the given endpoint via OTLP/HTTP; which resources are injected
154/// is platform-dependent. Workers read auth headers from a runtime-only
155/// secret. Runtime-less Containers and Daemons receive standard OTEL auth
156/// variables only at the final hosting boundary: Local passes them directly
157/// to the process and Kubernetes projects them from a per-workload Secret.
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
160#[serde(rename_all = "camelCase")]
161pub struct OtlpConfig {
162 /// Full OTLP logs endpoint URL.
163 /// Example: "https://<manager-host>/v1/logs"
164 pub logs_endpoint: String,
165 /// Auth header value in "key=value,..." format.
166 /// Example: "authorization=Bearer <write-token>"
167 pub logs_auth_header: String,
168 /// Full OTLP metrics endpoint URL (optional).
169 /// When set, the worker runtime exports its own VM/container orchestration metrics here.
170 /// Example: "https://api.axiom.co/v1/metrics"
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub metrics_endpoint: Option<String>,
173 /// Auth header value for the metrics endpoint in "key=value,..." format (optional).
174 ///
175 /// When absent, `logs_auth_header` is reused for metrics -- suitable when the same
176 /// credential covers both signals. When present (e.g. Axiom with separate datasets),
177 /// this value is used exclusively for metrics.
178 ///
179 /// Example: "authorization=Bearer <token>,x-axiom-dataset=<metrics-dataset>"
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub metrics_auth_header: Option<String>,
182 /// Resource attributes attached to every OTLP signal emitted for this deployment.
183 ///
184 /// Platform managers use this for stable identity such as `alien.workspace_id`,
185 /// `alien.project_id`, `alien.deployment_group_id`, and `alien.deployment_id`.
186 /// Runtime-specific resource attributes such as `service.name` remain owned by
187 /// the runtime/exporter.
188 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
189 pub resource_attributes: HashMap<String, String>,
190}
191
192#[cfg(test)]
193mod input_values_tests {
194 use super::*;
195
196 /// Older actors serialize configs without the field; it must deserialize
197 /// to an empty map, and an empty map must not serialize at all.
198 #[test]
199 fn input_values_default_to_empty_and_stay_off_the_wire() {
200 let json = serde_json::json!({
201 "stackSettings": serde_json::to_value(crate::StackSettings::default()).unwrap(),
202 "environmentVariables": {
203 "variables": [],
204 "hash": "hash",
205 "createdAt": "2026-01-01T00:00:00Z",
206 },
207 "externalBindings": {},
208 });
209 let config: DeploymentConfig =
210 serde_json::from_value(json).expect("old shape deserializes");
211 assert!(config.input_values.is_empty());
212
213 let round = serde_json::to_value(&config).expect("serializes");
214 assert!(
215 round.get("inputValues").is_none(),
216 "empty map must not serialize"
217 );
218 }
219}