Skip to main content

alien_core/
stack_settings.rs

1//!
2//! Defines stack-level settings and management configurations for different cloud platforms.
3//! These settings customize deployment behavior and cross-account/cross-tenant access patterns.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::{KubernetesCloudReference, KubernetesClusterOwnership};
9
10/// AWS management configuration extracted from stack settings
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase")]
14pub struct AwsManagementConfig {
15    /// The managing AWS IAM role ARN that can assume cross-account roles
16    pub managing_role_arn: String,
17}
18
19/// GCP management configuration extracted from stack settings
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[serde(rename_all = "camelCase")]
23pub struct GcpManagementConfig {
24    /// Service account email for management roles
25    pub service_account_email: String,
26}
27
28/// Azure management configuration extracted from stack settings
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
31#[serde(rename_all = "camelCase")]
32pub struct AzureManagementConfig {
33    /// The managing Azure Tenant ID for cross-tenant access
34    pub managing_tenant_id: String,
35    /// OIDC issuer URL trusted by the target-side managed identity.
36    pub oidc_issuer: String,
37    /// OIDC subject claim trusted by the target-side managed identity.
38    pub oidc_subject: String,
39}
40
41/// Management configuration for different cloud platforms.
42///
43/// Platform-derived configuration for cross-account/cross-tenant access.
44/// This is NOT user-specified - it's derived from the Manager's ServiceAccount.
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
47#[serde(rename_all = "camelCase", tag = "platform")]
48pub enum ManagementConfig {
49    /// AWS management configuration
50    Aws(AwsManagementConfig),
51    /// GCP management configuration  
52    Gcp(GcpManagementConfig),
53    /// Azure management configuration
54    Azure(AzureManagementConfig),
55    /// Kubernetes management configuration (minimal for now)
56    Kubernetes,
57}
58
59/// Network configuration for the stack.
60///
61/// Controls how VPC/VNet networking is provisioned. Users configure this in
62/// `StackSettings`; the Network resource itself is auto-generated by preflights.
63///
64/// ## Egress policy
65///
66/// Container cluster VMs are configured for egress based on the mode:
67///
68/// - `UseDefault` → VMs get ephemeral public IPs (no NAT is provisioned)
69/// - `Create` → VMs use private IPs; Alien provisions a NAT gateway for outbound access
70/// - `ByoVpc*` / `ByoVnet*` → no public IPs assigned; customer manages egress
71///
72/// For production workloads, use `Create`. For fast dev/test iteration, `UseDefault` is
73/// sufficient. For environments with existing VPCs, use the appropriate `ByoVpc*` variant.
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
76#[serde(rename_all = "camelCase", tag = "type")]
77pub enum NetworkSettings {
78    /// Use the cloud provider's default VPC/network.
79    ///
80    /// Designed for fast dev/test provisioning. No isolated VPC is created, so there
81    /// is nothing to wait for or clean up. VMs receive ephemeral public IPs for internet
82    /// access — no NAT gateway is provisioned.
83    ///
84    /// - **AWS**: Discovers the account's default VPC. Subnets are public with auto-assigned IPs.
85    /// - **GCP**: Discovers the project's `default` network and regional subnet. Instance
86    ///   templates include an `AccessConfig` to assign an ephemeral external IP.
87    /// - **Azure**: Azure has no default VNet, so one is created along with a NAT Gateway.
88    ///   VMs stay private and use NAT for egress.
89    ///
90    /// Not recommended for production. Use `Create` instead.
91    #[serde(rename = "use-default")]
92    UseDefault,
93
94    /// Create a new isolated VPC/VNet with a managed NAT gateway.
95    ///
96    /// All networking infrastructure is provisioned by Alien and cleaned up on delete.
97    /// VMs use private IPs only; all outbound traffic routes through the NAT gateway.
98    ///
99    /// Recommended for production deployments.
100    #[serde(rename = "create")]
101    Create {
102        /// VPC/VNet CIDR block. If not specified, auto-generated from stack ID
103        /// to reduce conflicts (e.g., "10.{hash}.0.0/16").
104        #[serde(skip_serializing_if = "Option::is_none")]
105        cidr: Option<String>,
106
107        /// Number of availability zones (default: 2).
108        #[serde(default = "default_availability_zones")]
109        availability_zones: u8,
110    },
111
112    /// Use an existing VPC (AWS).
113    ///
114    /// Alien validates the references but creates no networking infrastructure.
115    /// The customer is responsible for routing and egress (NAT, proxy, VPN, etc.).
116    #[serde(rename = "byo-vpc-aws")]
117    ByoVpcAws {
118        /// The ID of the existing VPC
119        vpc_id: String,
120        /// IDs of public subnets (required for public ingress)
121        public_subnet_ids: Vec<String>,
122        /// IDs of private subnets
123        private_subnet_ids: Vec<String>,
124        /// Optional security group IDs to use
125        #[serde(default)]
126        security_group_ids: Vec<String>,
127    },
128
129    /// Use an existing VPC (GCP).
130    ///
131    /// Alien validates the references but creates no networking infrastructure.
132    /// The customer is responsible for routing and egress (Cloud NAT, proxy, VPN, etc.).
133    #[serde(rename = "byo-vpc-gcp")]
134    ByoVpcGcp {
135        /// The name of the existing VPC network
136        network_name: String,
137        /// The name of the subnet to use
138        subnet_name: String,
139        /// The region of the subnet
140        region: String,
141    },
142
143    /// Use an existing VNet (Azure).
144    ///
145    /// Alien validates the references but creates no networking infrastructure.
146    /// The customer is responsible for routing and egress (NAT Gateway, proxy, VPN, etc.).
147    #[serde(rename = "byo-vnet-azure")]
148    ByoVnetAzure {
149        /// The full resource ID of the existing VNet
150        vnet_resource_id: String,
151        /// Name of the public subnet within the VNet
152        public_subnet_name: String,
153        /// Name of the private subnet within the VNet
154        private_subnet_name: String,
155        /// Name of the dedicated classic Application Gateway subnet within the VNet.
156        #[serde(default, skip_serializing_if = "Option::is_none")]
157        application_gateway_subnet_name: Option<String>,
158    },
159}
160
161fn default_availability_zones() -> u8 {
162    2
163}
164
165/// Deployment model: how updates are delivered to the remote environment.
166#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
167#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
168#[serde(rename_all = "camelCase")]
169pub enum DeploymentModel {
170    /// Manager pushes updates via cross-account access.
171    /// Available for AWS, GCP, Azure only.
172    #[default]
173    Push,
174    /// Agent in remote environment pulls updates.
175    /// Available for all platforms (AWS, GCP, Azure, Kubernetes, Local).
176    Pull,
177}
178
179/// How updates are delivered to the deployment.
180#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182#[serde(rename_all = "kebab-case")]
183pub enum UpdatesMode {
184    /// Updates deploy automatically (default).
185    #[default]
186    Auto,
187    /// Updates require explicit approval before deployment.
188    ApprovalRequired,
189}
190
191/// How telemetry (logs, metrics, traces) is handled.
192#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
193#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
194#[serde(rename_all = "kebab-case")]
195pub enum TelemetryMode {
196    /// No telemetry permissions. Data will not be collected.
197    Off,
198    /// Telemetry flows automatically (default).
199    #[default]
200    Auto,
201    /// Telemetry requires explicit approval before collection begins.
202    ApprovalRequired,
203}
204
205impl TelemetryMode {
206    /// Returns true if telemetry is enabled (Auto or ApprovalRequired).
207    pub fn is_enabled(&self) -> bool {
208        !matches!(self, TelemetryMode::Off)
209    }
210}
211
212/// How heartbeat health checks are handled.
213#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
214#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
215#[serde(rename_all = "kebab-case")]
216pub enum HeartbeatsMode {
217    /// No heartbeat permissions. Health checks disabled.
218    Off,
219    /// Heartbeat enabled (default).
220    #[default]
221    On,
222}
223
224impl HeartbeatsMode {
225    /// Returns true if heartbeat is enabled.
226    pub fn is_enabled(&self) -> bool {
227        matches!(self, HeartbeatsMode::On)
228    }
229}
230
231/// Domain configuration for the stack.
232///
233/// When `custom_domains` is set, the specified resources use customer-provided
234/// domains and certificates. Otherwise, Alien auto-generates domains.
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
236#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
237#[serde(rename_all = "camelCase")]
238pub struct DomainSettings {
239    /// Custom domain configuration per resource ID.
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub custom_domains: Option<HashMap<String, CustomDomainConfig>>,
242}
243
244/// Custom domain configuration for a single resource.
245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
247#[serde(rename_all = "camelCase")]
248pub struct CustomDomainConfig {
249    /// Fully qualified domain name to use.
250    pub domain: String,
251    /// Customer-provided certificate reference.
252    pub certificate: CustomCertificateConfig,
253}
254
255/// Platform-specific certificate references for custom domains.
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
257#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
258#[serde(rename_all = "camelCase")]
259pub struct CustomCertificateConfig {
260    /// AWS ACM certificate ARN
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub aws: Option<AwsCustomCertificateConfig>,
263    /// GCP Certificate Manager certificate name
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub gcp: Option<GcpCustomCertificateConfig>,
266    /// Azure Key Vault certificate ID
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub azure: Option<AzureCustomCertificateConfig>,
269    /// Kubernetes TLS Secret reference for Secret-backed route profiles.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub kubernetes: Option<KubernetesCustomCertificateConfig>,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
275#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
276#[serde(rename_all = "camelCase")]
277pub struct AwsCustomCertificateConfig {
278    pub certificate_arn: String,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
282#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
283#[serde(rename_all = "camelCase")]
284pub struct GcpCustomCertificateConfig {
285    pub certificate_name: String,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
290#[serde(rename_all = "camelCase")]
291pub struct AzureCustomCertificateConfig {
292    pub key_vault_certificate_id: String,
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub key_vault_resource_id: Option<String>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
298#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
299#[serde(rename_all = "camelCase")]
300pub struct KubernetesCustomCertificateConfig {
301    /// Existing TLS Secret containing `tls.crt` and `tls.key`.
302    pub tls_secret_ref: KubernetesTlsSecretRef,
303}
304
305/// Kubernetes runtime substrate configuration.
306///
307/// This controls how setup chooses the cluster backing `Platform::Kubernetes`
308/// deployments. When omitted, cloud-backed Kubernetes deployments default to a
309/// managed cluster and generic/on-prem Kubernetes defaults to an external
310/// cluster.
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
313#[serde(rename_all = "camelCase")]
314pub struct KubernetesSettings {
315    /// Cluster selection or creation settings.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub cluster: Option<KubernetesClusterSettings>,
318    /// Public HTTPS exposure contract shared by setup, Helm, and runtime.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub exposure: Option<KubernetesExposureSettings>,
321}
322
323/// Kubernetes cluster setup settings.
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
325#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
326#[serde(rename_all = "camelCase")]
327pub struct KubernetesClusterSettings {
328    /// Whether Alien should create the cluster, use a setup-owned existing
329    /// cluster, or bind to an external/on-prem cluster.
330    pub ownership: KubernetesClusterOwnership,
331    /// Namespace where the Alien chart and application resources run.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub namespace: Option<String>,
334    /// Optional provider-specific cloud identity for existing clusters.
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub cloud: Option<KubernetesCloudReference>,
337}
338
339/// Kubernetes public HTTPS exposure mode.
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
341#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
342#[serde(rename_all = "camelCase", tag = "mode")]
343pub enum KubernetesExposureSettings {
344    /// Do not create Alien-managed external routing.
345    Disabled,
346    /// Use Alien-generated DNS and Platform-managed certificate material.
347    Generated {
348        /// Runtime route profile to materialize.
349        route: KubernetesRouteProfile,
350        /// How managed certificate material reaches the route profile.
351        certificate: KubernetesCertificateMode,
352    },
353    /// Use a customer hostname and customer-owned certificate reference.
354    Custom {
355        /// Hostname routed by the Kubernetes public endpoint.
356        domain: String,
357        /// Runtime route profile to materialize.
358        route: KubernetesRouteProfile,
359        /// Customer-owned certificate reference consumed by the route profile.
360        certificate: KubernetesCertificateMode,
361    },
362}
363
364/// Kubernetes route API selected for public endpoints.
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
366#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
367#[serde(rename_all = "camelCase", tag = "routeApi")]
368pub enum KubernetesRouteProfile {
369    /// `networking.k8s.io/v1` Ingress route profile.
370    Ingress(KubernetesIngressRouteProfile),
371    /// Gateway API `Gateway` + `HTTPRoute` route profile.
372    Gateway(KubernetesGatewayRouteProfile),
373}
374
375/// Shared Ingress route profile values.
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
377#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
378#[serde(rename_all = "camelCase")]
379pub struct KubernetesIngressRouteProfile {
380    /// Route controller identifier, for example `eks.amazonaws.com/alb`.
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub controller: Option<String>,
383    /// `spec.ingressClassName` for generated Ingresses.
384    pub ingress_class_name: String,
385    /// Labels applied to route objects.
386    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
387    pub labels: HashMap<String, String>,
388    /// Annotations applied to route objects.
389    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
390    pub annotations: HashMap<String, String>,
391    /// Provider-specific route options that are required by the selected class.
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub provider: Option<KubernetesRouteProviderOptions>,
394}
395
396/// Shared Gateway API route profile values.
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
398#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
399#[serde(rename_all = "camelCase")]
400pub struct KubernetesGatewayRouteProfile {
401    /// Route controller identifier, for example a cloud Gateway controller.
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub controller: Option<String>,
404    /// GatewayClass selected for generated Gateways.
405    pub gateway_class_name: String,
406    /// Listener port, usually 443.
407    pub listener_port: u16,
408    /// Labels applied to route objects.
409    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
410    pub labels: HashMap<String, String>,
411    /// Annotations applied to route objects.
412    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
413    pub annotations: HashMap<String, String>,
414    /// Provider-specific route options that are required by the selected class.
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub provider: Option<KubernetesRouteProviderOptions>,
417}
418
419/// Provider-specific route options required by supported managed profiles.
420#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
421#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
422#[serde(rename_all = "camelCase", tag = "provider")]
423pub enum KubernetesRouteProviderOptions {
424    /// AWS ALB route options for EKS.
425    #[serde(rename_all = "camelCase")]
426    AwsAlb {
427        /// Internet-facing or internal ALB scheme.
428        scheme: String,
429        /// ALB target type, usually `ip`.
430        target_type: String,
431        /// Optional ALB IP address type, such as `dualstack`.
432        #[serde(default, skip_serializing_if = "Option::is_none")]
433        ip_address_type: Option<String>,
434        /// Explicit subnet IDs when the profile cannot rely on controller discovery.
435        #[serde(default, skip_serializing_if = "Vec::is_empty")]
436        subnet_ids: Vec<String>,
437    },
438    /// GKE Gateway route options.
439    #[serde(rename_all = "camelCase")]
440    GkeGateway {
441        /// Optional static address name for the Gateway frontend.
442        #[serde(default, skip_serializing_if = "Option::is_none")]
443        static_address_name: Option<String>,
444    },
445    /// Azure Application Gateway for Containers route options.
446    #[serde(rename_all = "camelCase")]
447    AzureApplicationGatewayForContainers {
448        /// Optional ALB namespace when using BYO Application Gateway resources.
449        #[serde(default, skip_serializing_if = "Option::is_none")]
450        alb_namespace: Option<String>,
451        /// Optional ALB name when using BYO Application Gateway resources.
452        #[serde(default, skip_serializing_if = "Option::is_none")]
453        alb_name: Option<String>,
454        /// Public or internal frontend exposure.
455        frontend: String,
456    },
457}
458
459/// Certificate publication or reference mode for Kubernetes public endpoints.
460#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
461#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
462#[serde(rename_all = "camelCase", tag = "mode")]
463pub enum KubernetesCertificateMode {
464    /// Platform-managed cert imported into AWS ACM by the runtime.
465    #[serde(rename_all = "camelCase")]
466    ManagedAcmImport {
467        /// ACM region. Defaults to the deployment region when omitted.
468        #[serde(default, skip_serializing_if = "Option::is_none")]
469        region: Option<String>,
470        /// Tags applied to runtime-imported ACM certificates.
471        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
472        tags: HashMap<String, String>,
473    },
474    /// Customer-provided AWS ACM certificate ARN.
475    #[serde(rename_all = "camelCase")]
476    AwsAcmArn {
477        /// Existing ACM certificate ARN.
478        certificate_arn: String,
479    },
480    /// Platform-managed cert written to a Kubernetes TLS Secret.
481    #[serde(rename_all = "camelCase")]
482    ManagedTlsSecret {
483        /// Secret name template. Runtime may substitute resource/deployment tokens.
484        secret_name_template: String,
485    },
486    /// Customer-provided Kubernetes TLS Secret.
487    TlsSecretRef(KubernetesTlsSecretRef),
488    /// No TLS certificate should be configured by Alien.
489    None,
490}
491
492/// Namespace-scoped Kubernetes TLS Secret reference.
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
494#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
495#[serde(rename_all = "camelCase")]
496pub struct KubernetesTlsSecretRef {
497    /// Secret name.
498    pub secret_name: String,
499    /// Secret namespace. Defaults to the release namespace when omitted.
500    #[serde(default, skip_serializing_if = "Option::is_none")]
501    pub namespace: Option<String>,
502}
503
504/// User-customizable deployment settings specified at deploy time.
505///
506/// These settings are provided by the customer via CloudFormation parameters,
507/// Terraform attributes, CLI flags, or Helm values. They customize how the
508/// deployment runs and what capabilities are enabled.
509///
510/// **Key distinction**: StackSettings is user-customizable, while ManagementConfig
511/// is platform-derived (from the Manager's ServiceAccount).
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
513#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
514#[serde(rename_all = "camelCase")]
515pub struct StackSettings {
516    /// Network configuration for the stack (VPC/VNet settings).
517    /// If `None`, an isolated VPC with NAT is auto-created when the stack has resources
518    /// that require networking (e.g., containers). Set explicitly to customize:
519    /// `UseDefault` for the provider's default network (fast, dev/test only),
520    /// `Create` for an isolated VPC with managed NAT (production), or `ByoVpc*`
521    /// to reference an existing customer-managed VPC.
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub network: Option<NetworkSettings>,
524
525    /// Domain configuration (future).
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    pub domains: Option<DomainSettings>,
528
529    /// Kubernetes runtime substrate configuration.
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub kubernetes: Option<KubernetesSettings>,
532
533    /// Deployment model: push (Manager) or pull (Agent).
534    /// Default: Push.
535    /// - Push: Manager drives updates. For cloud platforms, requires cross-account
536    ///   credentials established during initial setup. For push-mode local
537    ///   deployments (currently `alien dev`), the manager has direct access —
538    ///   no bootstrap needed.
539    /// - Pull: Agent in the target environment drives updates via polling.
540    ///   Required for Kubernetes and remote local deployments.
541    #[serde(default, skip_serializing_if = "is_default_deployment_model")]
542    pub deployment_model: DeploymentModel,
543
544    /// How updates are delivered.
545    /// - auto: Updates deploy automatically (default)
546    /// - approval-required: Updates wait for explicit approval
547    #[serde(default, skip_serializing_if = "is_default_updates_mode")]
548    pub updates: UpdatesMode,
549
550    /// How telemetry (logs, metrics, traces) is handled.
551    /// - off: No telemetry permissions
552    /// - auto: Telemetry flows automatically (default)
553    /// - approval-required: Telemetry waits for explicit approval
554    #[serde(default, skip_serializing_if = "is_default_telemetry_mode")]
555    pub telemetry: TelemetryMode,
556
557    /// How heartbeat health checks are handled.
558    /// - off: No heartbeat permissions
559    /// - on: Heartbeat enabled (default)
560    #[serde(default, skip_serializing_if = "is_default_heartbeats_mode")]
561    pub heartbeats: HeartbeatsMode,
562
563    /// External bindings for pre-existing infrastructure.
564    /// Allows using existing resources (MinIO, Redis, shared Container Apps
565    /// Environment, etc.) instead of having Alien provision them.
566    /// Required for Kubernetes platform, optional for cloud platforms.
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
569    pub external_bindings: Option<crate::ExternalBindings>,
570}
571
572fn is_default_deployment_model(model: &DeploymentModel) -> bool {
573    *model == DeploymentModel::default()
574}
575
576fn is_default_updates_mode(mode: &UpdatesMode) -> bool {
577    *mode == UpdatesMode::default()
578}
579
580fn is_default_telemetry_mode(mode: &TelemetryMode) -> bool {
581    *mode == TelemetryMode::default()
582}
583
584fn is_default_heartbeats_mode(mode: &HeartbeatsMode) -> bool {
585    *mode == HeartbeatsMode::default()
586}