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        /// Name of the dedicated subnet that hosts Private Endpoints (e.g. for a
159        /// Postgres Flexible Server). A Private Endpoint must not share the private
160        /// subnet, which is already claimed by the Container Apps environment's
161        /// `infrastructure_subnet_id`. Required only when the stack contains a
162        /// Postgres resource; otherwise unused.
163        #[serde(default, skip_serializing_if = "Option::is_none")]
164        private_endpoint_subnet_name: Option<String>,
165    },
166}
167
168fn default_availability_zones() -> u8 {
169    2
170}
171
172/// Deployment-time compute choices for Alien-managed compute pools.
173///
174/// Application source declares portable pool requirements. This settings
175/// object stores the concrete choices made for one deployment, such as the
176/// provider machine type and selected machine counts.
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
178#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
179#[serde(rename_all = "camelCase")]
180pub struct ComputeSettings {
181    /// Selected compute choices keyed by pool ID.
182    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
183    pub pools: HashMap<String, ComputePoolSelection>,
184}
185
186/// User-selected deployment settings for one compute pool.
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
188#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
189#[serde(rename_all = "camelCase", tag = "mode")]
190pub enum ComputePoolSelection {
191    /// Fixed number of machines.
192    Fixed {
193        /// Number of machines to run.
194        machines: u32,
195        /// Provider machine type selected for this deployment.
196        #[serde(default, skip_serializing_if = "Option::is_none")]
197        machine: Option<String>,
198    },
199    /// Autoscaling machine pool.
200    Autoscale {
201        /// Minimum machine count.
202        min: u32,
203        /// Maximum machine count.
204        max: u32,
205        /// Provider machine type selected for this deployment.
206        #[serde(default, skip_serializing_if = "Option::is_none")]
207        machine: Option<String>,
208    },
209}
210
211impl ComputePoolSelection {
212    /// Selected provider machine type, when this platform needs one.
213    pub fn machine(&self) -> Option<&str> {
214        match self {
215            Self::Fixed { machine, .. } | Self::Autoscale { machine, .. } => machine.as_deref(),
216        }
217    }
218
219    /// Selected minimum machine count.
220    pub fn min_size(&self) -> u32 {
221        match self {
222            Self::Fixed { machines, .. } => *machines,
223            Self::Autoscale { min, .. } => *min,
224        }
225    }
226
227    /// Selected maximum machine count.
228    pub fn max_size(&self) -> u32 {
229        match self {
230            Self::Fixed { machines, .. } => *machines,
231            Self::Autoscale { max, .. } => *max,
232        }
233    }
234
235    /// Whether the selection has internally valid scale bounds.
236    pub fn validate(&self) -> std::result::Result<(), String> {
237        match self {
238            Self::Fixed { machines, .. } => {
239                if *machines == 0 {
240                    Err("fixed compute pools must select at least one machine".to_string())
241                } else {
242                    Ok(())
243                }
244            }
245            Self::Autoscale { min, max, .. } => {
246                if min > max {
247                    Err(format!(
248                        "autoscaling compute pool minimum ({min}) cannot exceed maximum ({max})"
249                    ))
250                } else {
251                    Ok(())
252                }
253            }
254        }
255    }
256}
257
258/// Deployment model: how updates are delivered to the remote environment.
259#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
260#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
261#[serde(rename_all = "camelCase")]
262pub enum DeploymentModel {
263    /// Manager pushes updates via cross-account access.
264    /// Available for AWS, GCP, Azure only.
265    #[default]
266    Push,
267    /// Agent in remote environment pulls updates.
268    /// Available for all platforms (AWS, GCP, Azure, Kubernetes, Local).
269    Pull,
270}
271
272/// How updates are delivered to the deployment.
273#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
274#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
275#[serde(rename_all = "kebab-case")]
276pub enum UpdatesMode {
277    /// Updates deploy automatically (default).
278    #[default]
279    Auto,
280    /// Updates require explicit approval before deployment.
281    ApprovalRequired,
282}
283
284/// How telemetry (logs, metrics, traces) is handled.
285#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
286#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
287#[serde(rename_all = "kebab-case")]
288pub enum TelemetryMode {
289    /// No telemetry permissions. Data will not be collected.
290    Off,
291    /// Telemetry flows automatically (default).
292    #[default]
293    Auto,
294    /// Telemetry requires explicit approval before collection begins.
295    ApprovalRequired,
296}
297
298impl TelemetryMode {
299    /// Returns true if telemetry is enabled (Auto or ApprovalRequired).
300    pub fn is_enabled(&self) -> bool {
301        !matches!(self, TelemetryMode::Off)
302    }
303}
304
305/// How heartbeat health checks are handled.
306#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
307#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
308#[serde(rename_all = "kebab-case")]
309pub enum HeartbeatsMode {
310    /// No heartbeat permissions. Health checks disabled.
311    Off,
312    /// Heartbeat enabled (default).
313    #[default]
314    On,
315}
316
317impl HeartbeatsMode {
318    /// Returns true if heartbeat is enabled.
319    pub fn is_enabled(&self) -> bool {
320        matches!(self, HeartbeatsMode::On)
321    }
322}
323
324/// Domain configuration for the stack.
325///
326/// When `custom_domains` is set, the specified resources use customer-provided
327/// domains and certificates. Otherwise, Alien auto-generates domains.
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
329#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
330#[serde(rename_all = "camelCase")]
331pub struct DomainSettings {
332    /// Custom domain configuration per resource ID.
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub custom_domains: Option<HashMap<String, CustomDomainConfig>>,
335    /// Public endpoint DNS target selection for machines deployments.
336    ///
337    /// When omitted, machines deployments publish healthy machine public
338    /// addresses directly. Use `LoadBalancer` when an external load balancer
339    /// fronts the machines and Alien should publish a CNAME to that target.
340    #[serde(default, skip_serializing_if = "Option::is_none")]
341    pub public_endpoint_target: Option<PublicEndpointTargetSettings>,
342}
343
344/// DNS target mode for public endpoints.
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
346#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
347#[serde(rename_all = "camelCase", tag = "mode")]
348pub enum PublicEndpointTargetSettings {
349    /// Publish DNS records directly to healthy machine public IP addresses.
350    MachineAddresses,
351    /// Publish a CNAME record to an external load balancer.
352    #[serde(rename_all = "camelCase")]
353    LoadBalancer {
354        /// DNS name or URL for the external load balancer.
355        cname_target: String,
356    },
357}
358
359/// Custom domain configuration for a single resource.
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
362#[serde(rename_all = "camelCase")]
363pub struct CustomDomainConfig {
364    /// Fully qualified domain name to use.
365    pub domain: String,
366    /// Customer-provided certificate reference.
367    pub certificate: CustomCertificateConfig,
368}
369
370/// Platform-specific certificate references for custom domains.
371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
372#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
373#[serde(rename_all = "camelCase")]
374pub struct CustomCertificateConfig {
375    /// AWS ACM certificate ARN
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub aws: Option<AwsCustomCertificateConfig>,
378    /// GCP Certificate Manager certificate name
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub gcp: Option<GcpCustomCertificateConfig>,
381    /// Azure Key Vault certificate ID
382    #[serde(default, skip_serializing_if = "Option::is_none")]
383    pub azure: Option<AzureCustomCertificateConfig>,
384    /// Kubernetes TLS Secret reference for Secret-backed route profiles.
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub kubernetes: Option<KubernetesCustomCertificateConfig>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
390#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
391#[serde(rename_all = "camelCase")]
392pub struct AwsCustomCertificateConfig {
393    pub certificate_arn: String,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
397#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
398#[serde(rename_all = "camelCase")]
399pub struct GcpCustomCertificateConfig {
400    pub certificate_name: String,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
404#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
405#[serde(rename_all = "camelCase")]
406pub struct AzureCustomCertificateConfig {
407    pub key_vault_certificate_id: String,
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    pub key_vault_resource_id: Option<String>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
414#[serde(rename_all = "camelCase")]
415pub struct KubernetesCustomCertificateConfig {
416    /// Existing TLS Secret containing `tls.crt` and `tls.key`.
417    pub tls_secret_ref: KubernetesTlsSecretRef,
418}
419
420/// Kubernetes runtime substrate configuration.
421///
422/// This controls how setup chooses the cluster backing `Platform::Kubernetes`
423/// deployments. When omitted, cloud-backed Kubernetes deployments default to a
424/// managed cluster and generic/on-prem Kubernetes defaults to an external
425/// cluster.
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
427#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
428#[serde(rename_all = "camelCase")]
429pub struct KubernetesSettings {
430    /// Cluster selection or creation settings.
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub cluster: Option<KubernetesClusterSettings>,
433    /// Public HTTPS exposure contract shared by setup, Helm, and runtime.
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    pub exposure: Option<KubernetesExposureSettings>,
436}
437
438/// Kubernetes cluster setup settings.
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
440#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
441#[serde(rename_all = "camelCase")]
442pub struct KubernetesClusterSettings {
443    /// Whether Alien should create the cluster, use a setup-owned existing
444    /// cluster, or bind to an external/on-prem cluster.
445    pub ownership: KubernetesClusterOwnership,
446    /// Namespace where the Alien chart and application resources run.
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub namespace: Option<String>,
449    /// Optional provider-specific cloud identity for existing clusters.
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub cloud: Option<KubernetesCloudReference>,
452}
453
454/// Kubernetes public HTTPS exposure mode.
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
456#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
457#[serde(rename_all = "camelCase", tag = "mode")]
458pub enum KubernetesExposureSettings {
459    /// Do not create Alien-managed external routing.
460    Disabled,
461    /// Use Alien-generated DNS and Platform-managed certificate material.
462    Generated {
463        /// Runtime route profile to materialize.
464        route: KubernetesRouteProfile,
465        /// How managed certificate material reaches the route profile.
466        certificate: KubernetesCertificateMode,
467    },
468    /// Use a customer hostname and customer-owned certificate reference.
469    Custom {
470        /// Hostname routed by the Kubernetes public endpoint.
471        domain: String,
472        /// Runtime route profile to materialize.
473        route: KubernetesRouteProfile,
474        /// Customer-owned certificate reference consumed by the route profile.
475        certificate: KubernetesCertificateMode,
476    },
477}
478
479/// Kubernetes route API selected for public endpoints.
480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
481#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
482#[serde(rename_all = "camelCase", tag = "routeApi")]
483pub enum KubernetesRouteProfile {
484    /// `networking.k8s.io/v1` Ingress route profile.
485    Ingress(KubernetesIngressRouteProfile),
486    /// Gateway API `Gateway` + `HTTPRoute` route profile.
487    Gateway(KubernetesGatewayRouteProfile),
488}
489
490/// Shared Ingress route profile values.
491#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
492#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
493#[serde(rename_all = "camelCase")]
494pub struct KubernetesIngressRouteProfile {
495    /// Route controller identifier, for example `eks.amazonaws.com/alb`.
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub controller: Option<String>,
498    /// `spec.ingressClassName` for generated Ingresses.
499    pub ingress_class_name: String,
500    /// Labels applied to route objects.
501    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
502    pub labels: HashMap<String, String>,
503    /// Annotations applied to route objects.
504    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
505    pub annotations: HashMap<String, String>,
506    /// Provider-specific route options that are required by the selected class.
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub provider: Option<KubernetesRouteProviderOptions>,
509}
510
511/// Shared Gateway API route profile values.
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
513#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
514#[serde(rename_all = "camelCase")]
515pub struct KubernetesGatewayRouteProfile {
516    /// Route controller identifier, for example a cloud Gateway controller.
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub controller: Option<String>,
519    /// GatewayClass selected for generated Gateways.
520    pub gateway_class_name: String,
521    /// Listener port, usually 443.
522    pub listener_port: u16,
523    /// Labels applied to route objects.
524    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
525    pub labels: HashMap<String, String>,
526    /// Annotations applied to route objects.
527    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
528    pub annotations: HashMap<String, String>,
529    /// Provider-specific route options that are required by the selected class.
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub provider: Option<KubernetesRouteProviderOptions>,
532}
533
534/// Provider-specific route options required by supported managed profiles.
535#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
536#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
537#[serde(rename_all = "camelCase", tag = "provider")]
538pub enum KubernetesRouteProviderOptions {
539    /// AWS ALB route options for EKS.
540    #[serde(rename_all = "camelCase")]
541    AwsAlb {
542        /// Internet-facing or internal ALB scheme.
543        scheme: String,
544        /// ALB target type, usually `ip`.
545        target_type: String,
546        /// Optional ALB IP address type, such as `dualstack`.
547        #[serde(default, skip_serializing_if = "Option::is_none")]
548        ip_address_type: Option<String>,
549        /// Explicit subnet IDs when the profile cannot rely on controller discovery.
550        #[serde(default, skip_serializing_if = "Vec::is_empty")]
551        subnet_ids: Vec<String>,
552    },
553    /// GKE Gateway route options.
554    #[serde(rename_all = "camelCase")]
555    GkeGateway {
556        /// Optional static address name for the Gateway frontend.
557        #[serde(default, skip_serializing_if = "Option::is_none")]
558        static_address_name: Option<String>,
559    },
560    /// Azure Application Gateway for Containers route options.
561    #[serde(rename_all = "camelCase")]
562    AzureApplicationGatewayForContainers {
563        /// Optional ALB namespace when using BYO Application Gateway resources.
564        #[serde(default, skip_serializing_if = "Option::is_none")]
565        alb_namespace: Option<String>,
566        /// Optional ALB name when using BYO Application Gateway resources.
567        #[serde(default, skip_serializing_if = "Option::is_none")]
568        alb_name: Option<String>,
569        /// Public or internal frontend exposure.
570        frontend: String,
571    },
572}
573
574/// Certificate publication or reference mode for Kubernetes public endpoints.
575#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
576#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
577#[serde(rename_all = "camelCase", tag = "mode")]
578pub enum KubernetesCertificateMode {
579    /// Platform-managed cert imported into AWS ACM by the runtime.
580    #[serde(rename_all = "camelCase")]
581    ManagedAcmImport {
582        /// ACM region. Defaults to the deployment region when omitted.
583        #[serde(default, skip_serializing_if = "Option::is_none")]
584        region: Option<String>,
585        /// Tags applied to runtime-imported ACM certificates.
586        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
587        tags: HashMap<String, String>,
588    },
589    /// Customer-provided AWS ACM certificate ARN.
590    #[serde(rename_all = "camelCase")]
591    AwsAcmArn {
592        /// Existing ACM certificate ARN.
593        certificate_arn: String,
594    },
595    /// Platform-managed cert written to a Kubernetes TLS Secret.
596    #[serde(rename_all = "camelCase")]
597    ManagedTlsSecret {
598        /// Secret name template. Runtime may substitute resource/deployment tokens.
599        secret_name_template: String,
600    },
601    /// Customer-provided Kubernetes TLS Secret.
602    TlsSecretRef(KubernetesTlsSecretRef),
603    /// No TLS certificate should be configured by Alien.
604    None,
605}
606
607/// Namespace-scoped Kubernetes TLS Secret reference.
608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
609#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
610#[serde(rename_all = "camelCase")]
611pub struct KubernetesTlsSecretRef {
612    /// Secret name.
613    pub secret_name: String,
614    /// Secret namespace. Defaults to the release namespace when omitted.
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub namespace: Option<String>,
617}
618
619/// User-customizable deployment settings specified at deploy time.
620///
621/// These settings are provided by the customer via CloudFormation parameters,
622/// Terraform attributes, CLI flags, or Helm values. They customize how the
623/// deployment runs and what capabilities are enabled.
624///
625/// **Key distinction**: StackSettings is user-customizable, while ManagementConfig
626/// is platform-derived (from the Manager's ServiceAccount).
627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
628#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
629#[serde(rename_all = "camelCase")]
630pub struct StackSettings {
631    /// Network configuration for the stack (VPC/VNet settings).
632    /// If `None`, an isolated VPC with NAT is auto-created when the stack has resources
633    /// that require networking (e.g., containers). Set explicitly to customize:
634    /// `UseDefault` for the provider's default network (fast, dev/test only),
635    /// `Create` for an isolated VPC with managed NAT (production), or `ByoVpc*`
636    /// to reference an existing customer-managed VPC.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub network: Option<NetworkSettings>,
639
640    /// Domain configuration (future).
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub domains: Option<DomainSettings>,
643
644    /// Kubernetes runtime substrate configuration.
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub kubernetes: Option<KubernetesSettings>,
647
648    /// Deployment-time compute selections for Alien-managed compute pools.
649    ///
650    /// This is where provider machine names such as EC2 instance types, GCE
651    /// machine types, or Azure VM SKUs belong. Application source should
652    /// declare portable requirements instead.
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub compute: Option<ComputeSettings>,
655
656    /// Deployment model: push (Manager) or pull (Agent).
657    /// Default: Push.
658    /// - Push: Manager drives updates. For cloud platforms, requires cross-account
659    ///   credentials established during initial setup. For push-mode local
660    ///   deployments (currently `alien dev`), the manager has direct access —
661    ///   no bootstrap needed.
662    /// - Pull: Agent in the target environment drives updates via polling.
663    ///   Required for Kubernetes and remote local deployments.
664    #[serde(default, skip_serializing_if = "is_default_deployment_model")]
665    pub deployment_model: DeploymentModel,
666
667    /// How updates are delivered.
668    /// - auto: Updates deploy automatically (default)
669    /// - approval-required: Updates wait for explicit approval
670    #[serde(default, skip_serializing_if = "is_default_updates_mode")]
671    pub updates: UpdatesMode,
672
673    /// How telemetry (logs, metrics, traces) is handled.
674    /// - off: No telemetry permissions
675    /// - auto: Telemetry flows automatically (default)
676    /// - approval-required: Telemetry waits for explicit approval
677    #[serde(default, skip_serializing_if = "is_default_telemetry_mode")]
678    pub telemetry: TelemetryMode,
679
680    /// How heartbeat health checks are handled.
681    /// - off: No heartbeat permissions
682    /// - on: Heartbeat enabled (default)
683    #[serde(default, skip_serializing_if = "is_default_heartbeats_mode")]
684    pub heartbeats: HeartbeatsMode,
685
686    /// External bindings for pre-existing infrastructure.
687    /// Allows using existing resources (MinIO, Redis, shared Container Apps
688    /// Environment, etc.) instead of having Alien provision them.
689    /// Required for Kubernetes platform, optional for cloud platforms.
690    #[serde(default, skip_serializing_if = "Option::is_none")]
691    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
692    pub external_bindings: Option<crate::ExternalBindings>,
693}
694
695fn is_default_deployment_model(model: &DeploymentModel) -> bool {
696    *model == DeploymentModel::default()
697}
698
699fn is_default_updates_mode(mode: &UpdatesMode) -> bool {
700    *mode == UpdatesMode::default()
701}
702
703fn is_default_telemetry_mode(mode: &TelemetryMode) -> bool {
704    *mode == TelemetryMode::default()
705}
706
707fn is_default_heartbeats_mode(mode: &HeartbeatsMode) -> bool {
708    *mode == HeartbeatsMode::default()
709}