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