Skip to main content

alien_core/
client_config.rs

1//! Client configuration structures for different cloud platforms
2//!
3//! This module contains the configuration structs for all supported cloud platforms.
4//! These structs define the authentication and platform-specific settings needed
5//! to connect to cloud services, but do not contain implementation logic (which
6//! remains in the respective client crates).
7
8use crate::Platform;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Service endpoint overrides for testing AWS services
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct AwsServiceOverrides {
17    /// Override endpoints for specific AWS services
18    /// Key is the service name (e.g., "lambda", "s3"), value is the base URL
19    pub endpoints: HashMap<String, String>,
20}
21
22/// Configuration for AWS role impersonation
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26pub struct AwsImpersonationConfig {
27    /// The ARN of the role to assume
28    pub role_arn: String,
29    /// Optional session name for the assumed role session
30    pub session_name: Option<String>,
31    /// Optional duration for the assumed role credentials (in seconds)
32    pub duration_seconds: Option<i32>,
33    /// Optional external ID for the assume role operation
34    pub external_id: Option<String>,
35    /// Optional target region override. When provided, the impersonated config
36    /// uses this region instead of inheriting the caller's region. Required for
37    /// cross-region impersonation (e.g., management in us-east-1 targeting us-east-2).
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub target_region: Option<String>,
40}
41
42/// Configuration for AWS Web Identity Token authentication
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
45#[serde(rename_all = "camelCase", deny_unknown_fields)]
46pub struct AwsWebIdentityConfig {
47    /// The ARN of the role to assume
48    pub role_arn: String,
49    /// Optional session name for the assumed role session
50    pub session_name: Option<String>,
51    /// The path to the web identity token file
52    pub web_identity_token_file: String,
53    /// Optional duration for the assumed role credentials (in seconds)
54    pub duration_seconds: Option<i32>,
55}
56
57/// Supported AWS authentication methods
58#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
60#[serde(rename_all = "camelCase", tag = "type")]
61pub enum AwsCredentials {
62    /// Direct access keys
63    AccessKeys {
64        /// AWS Access Key ID
65        access_key_id: String,
66        /// AWS Secret Access Key
67        secret_access_key: String,
68        /// Optional AWS Session Token
69        session_token: Option<String>,
70    },
71    /// Web Identity Token for OIDC authentication
72    WebIdentity {
73        /// Web identity configuration
74        config: AwsWebIdentityConfig,
75    },
76}
77
78impl std::fmt::Debug for AwsCredentials {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            AwsCredentials::AccessKeys {
82                access_key_id,
83                session_token,
84                ..
85            } => f
86                .debug_struct("AwsCredentials::AccessKeys")
87                .field("access_key_id", access_key_id)
88                .field("secret_access_key", &"[REDACTED]")
89                .field(
90                    "session_token",
91                    &session_token.as_ref().map(|_| "[REDACTED]"),
92                )
93                .finish(),
94            AwsCredentials::WebIdentity { config } => f
95                .debug_struct("AwsCredentials::WebIdentity")
96                .field("config", config)
97                .finish(),
98        }
99    }
100}
101
102/// AWS client configuration
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
105#[serde(rename_all = "camelCase", deny_unknown_fields)]
106pub struct AwsClientConfig {
107    /// The AWS Account ID.
108    pub account_id: String,
109    /// The AWS region.
110    pub region: String,
111    /// AWS authentication credentials.
112    pub credentials: AwsCredentials,
113    /// Service endpoint overrides for testing
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub service_overrides: Option<AwsServiceOverrides>,
116}
117
118/// Service endpoint overrides for testing GCP services
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122pub struct GcpServiceOverrides {
123    /// Override endpoints for specific GCP services
124    /// Key is the service name (e.g., "cloudrun", "storage"), value is the base URL
125    pub endpoints: HashMap<String, String>,
126}
127
128/// Authentication options for talking to GCP APIs.
129#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
131#[serde(rename_all = "camelCase", tag = "type")]
132pub enum GcpCredentials {
133    /// Use an already-minted OAuth2 access token.
134    AccessToken { token: String },
135
136    /// Use a full Service Account JSON key (as string). A short-lived JWT will
137    /// be created and exchanged for a bearer token automatically.
138    ServiceAccountKey { json: String },
139
140    /// Use GCP metadata server for authentication (for instances running on GCP)
141    ServiceMetadata,
142
143    /// Use projected service account token (for Kubernetes workload identity)
144    ProjectedServiceAccount {
145        /// Path to the projected service account token
146        token_file: String,
147        /// Service account email
148        service_account_email: String,
149    },
150
151    /// Use gcloud Application Default Credentials (authorized_user).
152    /// Exchanges refresh_token for an access_token via Google's OAuth2 endpoint.
153    AuthorizedUser {
154        /// OAuth2 client ID
155        client_id: String,
156        /// OAuth2 client secret
157        client_secret: String,
158        /// OAuth2 refresh token
159        refresh_token: String,
160    },
161}
162
163impl std::fmt::Debug for GcpCredentials {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        match self {
166            GcpCredentials::AccessToken { .. } => f
167                .debug_struct("GcpCredentials::AccessToken")
168                .field("token", &"[REDACTED]")
169                .finish(),
170            GcpCredentials::ServiceAccountKey { .. } => f
171                .debug_struct("GcpCredentials::ServiceAccountKey")
172                .field("json", &"[REDACTED]")
173                .finish(),
174            GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
175            GcpCredentials::ProjectedServiceAccount {
176                token_file,
177                service_account_email,
178            } => f
179                .debug_struct("GcpCredentials::ProjectedServiceAccount")
180                .field("token_file", token_file)
181                .field("service_account_email", service_account_email)
182                .finish(),
183            GcpCredentials::AuthorizedUser { client_id, .. } => f
184                .debug_struct("GcpCredentials::AuthorizedUser")
185                .field("client_id", client_id)
186                .field("client_secret", &"[REDACTED]")
187                .field("refresh_token", &"[REDACTED]")
188                .finish(),
189        }
190    }
191}
192
193/// Configuration for GCP service account impersonation
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct GcpImpersonationConfig {
198    /// The email of the service account to impersonate
199    pub service_account_email: String,
200    /// The OAuth 2.0 scopes that define the access token's permissions
201    pub scopes: Vec<String>,
202    /// Optional sequence of service accounts in a delegation chain
203    pub delegates: Option<Vec<String>>,
204    /// Optional desired lifetime duration of the access token (max 3600s)
205    pub lifetime: Option<String>,
206    /// Optional target project ID override. When provided, the impersonated config
207    /// uses this project ID instead of inheriting the caller's project.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub target_project_id: Option<String>,
210    /// Optional target region override. When provided, the impersonated config
211    /// uses this region instead of inheriting the caller's region.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub target_region: Option<String>,
214}
215
216impl Default for GcpImpersonationConfig {
217    fn default() -> Self {
218        Self {
219            service_account_email: String::new(),
220            scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
221            delegates: None,
222            lifetime: Some("3600s".to_string()),
223            target_project_id: None,
224            target_region: None,
225        }
226    }
227}
228
229/// GCP client configuration
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
232#[serde(rename_all = "camelCase", deny_unknown_fields)]
233pub struct GcpClientConfig {
234    /// The GCP Project ID.
235    pub project_id: String,
236    /// The GCP region for resources.
237    pub region: String,
238    /// GCP authentication credentials.
239    pub credentials: GcpCredentials,
240    /// Service endpoint overrides for testing
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub service_overrides: Option<GcpServiceOverrides>,
243    /// The GCP project number (numeric). Resolved at runtime via Resource Manager API.
244    /// Used in IAM condition expressions where resource.name uses project number.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub project_number: Option<String>,
247}
248
249/// Service endpoint overrides for testing Azure services
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
252#[serde(rename_all = "camelCase", deny_unknown_fields)]
253pub struct AzureServiceOverrides {
254    /// Override endpoints for specific Azure services
255    /// Key is the service name (e.g., "management", "storage", "containerApps"), value is the base URL
256    pub endpoints: HashMap<String, String>,
257}
258
259/// Represents Azure authentication credentials
260#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
261#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
262#[serde(rename_all = "camelCase", tag = "type")]
263pub enum AzureCredentials {
264    /// Service principal with client secret
265    ServicePrincipal {
266        /// The client ID (application ID)
267        client_id: String,
268        /// The client secret
269        client_secret: String,
270    },
271    /// Direct access token
272    AccessToken {
273        /// The bearer token to use for authentication
274        token: String,
275    },
276    /// Azure AD Workload Identity (federated identity)
277    WorkloadIdentity {
278        /// The client ID of the managed identity or application
279        client_id: String,
280        /// The tenant ID for authentication
281        tenant_id: String,
282        /// Path to the federated token file
283        federated_token_file: String,
284        /// The authority host URL
285        authority_host: String,
286    },
287    /// Azure Managed Identity (Container Apps / App Service)
288    /// Uses IDENTITY_ENDPOINT + IDENTITY_HEADER injected by the platform
289    ManagedIdentity {
290        /// The client ID of the user-assigned managed identity
291        client_id: String,
292        /// The identity endpoint URL (from IDENTITY_ENDPOINT env var)
293        identity_endpoint: String,
294        /// The identity header secret (from IDENTITY_HEADER env var)
295        identity_header: String,
296    },
297}
298
299impl std::fmt::Debug for AzureCredentials {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            AzureCredentials::ServicePrincipal { client_id, .. } => f
303                .debug_struct("AzureCredentials::ServicePrincipal")
304                .field("client_id", client_id)
305                .field("client_secret", &"[REDACTED]")
306                .finish(),
307            AzureCredentials::AccessToken { .. } => f
308                .debug_struct("AzureCredentials::AccessToken")
309                .field("token", &"[REDACTED]")
310                .finish(),
311            AzureCredentials::WorkloadIdentity {
312                client_id,
313                tenant_id,
314                federated_token_file,
315                authority_host,
316            } => f
317                .debug_struct("AzureCredentials::WorkloadIdentity")
318                .field("client_id", client_id)
319                .field("tenant_id", tenant_id)
320                .field("federated_token_file", federated_token_file)
321                .field("authority_host", authority_host)
322                .finish(),
323            AzureCredentials::ManagedIdentity {
324                client_id,
325                identity_endpoint,
326                ..
327            } => f
328                .debug_struct("AzureCredentials::ManagedIdentity")
329                .field("client_id", client_id)
330                .field("identity_endpoint", identity_endpoint)
331                .field("identity_header", &"[REDACTED]")
332                .finish(),
333        }
334    }
335}
336
337/// Configuration for Azure managed identity impersonation
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
340#[serde(rename_all = "camelCase", deny_unknown_fields)]
341pub struct AzureImpersonationConfig {
342    /// The client ID of the managed identity or service principal to impersonate
343    pub client_id: String,
344    /// The scope for the access token (e.g., "https://management.azure.com/.default")
345    pub scope: String,
346    /// Optional tenant ID for cross-tenant impersonation
347    pub tenant_id: Option<String>,
348    /// Optional target subscription ID override. When provided, the impersonated config
349    /// uses this subscription instead of inheriting the caller's subscription.
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub target_subscription_id: Option<String>,
352    /// Optional target region override. When provided, the impersonated config
353    /// uses this region instead of inheriting the caller's region.
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub target_region: Option<String>,
356}
357
358impl Default for AzureImpersonationConfig {
359    fn default() -> Self {
360        Self {
361            client_id: String::new(),
362            scope: "https://management.azure.com/.default".to_string(),
363            tenant_id: None,
364            target_subscription_id: None,
365            target_region: None,
366        }
367    }
368}
369
370/// Azure client configuration
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
373#[serde(rename_all = "camelCase", deny_unknown_fields)]
374pub struct AzureClientConfig {
375    /// The Azure Subscription ID where resources will be deployed.
376    pub subscription_id: String,
377    /// The customer's Azure Tenant ID.
378    pub tenant_id: String,
379    /// Azure region for resources.
380    pub region: Option<String>,
381    /// Azure authentication credentials.
382    pub credentials: AzureCredentials,
383    /// Service endpoint overrides for testing
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub service_overrides: Option<AzureServiceOverrides>,
386}
387
388/// Configuration mode for Kubernetes access
389#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
390#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
391#[serde(rename_all = "camelCase", tag = "mode")]
392pub enum KubernetesClientConfig {
393    /// Use in-cluster configuration (service account tokens, etc.)
394    InCluster {
395        /// The namespace to operate in
396        #[serde(skip_serializing_if = "Option::is_none")]
397        namespace: Option<String>,
398        /// Additional headers to include in requests
399        #[serde(skip_serializing_if = "Option::is_none")]
400        additional_headers: Option<HashMap<String, String>>,
401    },
402    /// Use kubeconfig file for configuration
403    Kubeconfig {
404        /// Path to kubeconfig file (optional, defaults to standard locations)
405        #[serde(skip_serializing_if = "Option::is_none")]
406        kubeconfig_path: Option<String>,
407        /// Context name to use (optional, defaults to current-context)
408        #[serde(skip_serializing_if = "Option::is_none")]
409        context: Option<String>,
410        /// Cluster name to use (optional, defaults to context's cluster)
411        #[serde(skip_serializing_if = "Option::is_none")]
412        cluster: Option<String>,
413        /// User name to use (optional, defaults to context's user)
414        #[serde(skip_serializing_if = "Option::is_none")]
415        user: Option<String>,
416        /// The namespace to operate in
417        #[serde(skip_serializing_if = "Option::is_none")]
418        namespace: Option<String>,
419        /// Additional headers to include in requests
420        #[serde(skip_serializing_if = "Option::is_none")]
421        additional_headers: Option<HashMap<String, String>>,
422    },
423    /// Manual configuration with explicit values
424    Manual {
425        /// The Kubernetes cluster server URL
426        server_url: String,
427        /// The cluster certificate authority data (base64 encoded)
428        certificate_authority_data: Option<String>,
429        /// Skip TLS verification (insecure)
430        insecure_skip_tls_verify: Option<bool>,
431        /// Client certificate data (base64 encoded) for mutual TLS
432        client_certificate_data: Option<String>,
433        /// Client key data (base64 encoded) for mutual TLS
434        client_key_data: Option<String>,
435        /// Bearer token for authentication
436        token: Option<String>,
437        /// Username for basic authentication
438        username: Option<String>,
439        /// Password for basic authentication
440        password: Option<String>,
441        /// The namespace to operate in
442        namespace: Option<String>,
443        /// Additional headers to include in requests
444        additional_headers: HashMap<String, String>,
445    },
446}
447
448impl std::fmt::Debug for KubernetesClientConfig {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        match self {
451            KubernetesClientConfig::InCluster {
452                namespace,
453                additional_headers,
454            } => f
455                .debug_struct("KubernetesClientConfig::InCluster")
456                .field("namespace", namespace)
457                .field("additional_headers", additional_headers)
458                .finish(),
459            KubernetesClientConfig::Kubeconfig {
460                kubeconfig_path,
461                context,
462                cluster,
463                user,
464                namespace,
465                additional_headers,
466            } => f
467                .debug_struct("KubernetesClientConfig::Kubeconfig")
468                .field("kubeconfig_path", kubeconfig_path)
469                .field("context", context)
470                .field("cluster", cluster)
471                .field("user", user)
472                .field("namespace", namespace)
473                .field("additional_headers", additional_headers)
474                .finish(),
475            KubernetesClientConfig::Manual {
476                server_url,
477                certificate_authority_data,
478                insecure_skip_tls_verify,
479                client_certificate_data,
480                client_key_data,
481                token,
482                username,
483                password,
484                namespace,
485                additional_headers,
486            } => f
487                .debug_struct("KubernetesClientConfig::Manual")
488                .field("server_url", server_url)
489                .field("certificate_authority_data", certificate_authority_data)
490                .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
491                .field("client_certificate_data", client_certificate_data)
492                .field(
493                    "client_key_data",
494                    &client_key_data.as_ref().map(|_| "[REDACTED]"),
495                )
496                .field("token", &token.as_ref().map(|_| "[REDACTED]"))
497                .field("username", username)
498                .field("password", &password.as_ref().map(|_| "[REDACTED]"))
499                .field("namespace", namespace)
500                .field("additional_headers", additional_headers)
501                .finish(),
502        }
503    }
504}
505
506/// Cloud-agnostic impersonation configuration
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
509#[serde(rename_all = "camelCase", tag = "platform")]
510pub enum ImpersonationConfig {
511    Aws(AwsImpersonationConfig),
512    Gcp(GcpImpersonationConfig),
513    Azure(AzureImpersonationConfig),
514    // Kubernetes doesn't support impersonation, so we don't include it here
515}
516
517/// Configuration for different cloud platform clients
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
520#[serde(rename_all = "camelCase", tag = "platform")]
521pub enum ClientConfig {
522    Aws(Box<AwsClientConfig>),
523    Gcp(Box<GcpClientConfig>),
524    Azure(Box<AzureClientConfig>),
525    Kubernetes(Box<KubernetesClientConfig>),
526    Local {
527        /// State directory for local resources and deployment state
528        state_directory: String,
529    },
530    /// Test platform - uses mock controllers without real cloud APIs
531    #[serde(skip)]
532    Test,
533}
534
535impl ClientConfig {
536    /// Returns the platform enum for this configuration.
537    pub fn platform(&self) -> Platform {
538        match self {
539            ClientConfig::Aws(_) => Platform::Aws,
540            ClientConfig::Gcp(_) => Platform::Gcp,
541            ClientConfig::Azure(_) => Platform::Azure,
542            ClientConfig::Kubernetes(_) => Platform::Kubernetes,
543            ClientConfig::Local { .. } => Platform::Local,
544            ClientConfig::Test => Platform::Test,
545        }
546    }
547
548    /// Returns the AWS configuration if this is an AWS client config.
549    pub fn aws_config(&self) -> Option<&AwsClientConfig> {
550        match self {
551            ClientConfig::Aws(config) => Some(config),
552            _ => None,
553        }
554    }
555
556    /// Returns the GCP configuration if this is a GCP client config.
557    pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
558        match self {
559            ClientConfig::Gcp(config) => Some(config),
560            _ => None,
561        }
562    }
563
564    /// Returns the Azure configuration if this is an Azure client config.
565    pub fn azure_config(&self) -> Option<&AzureClientConfig> {
566        match self {
567            ClientConfig::Azure(config) => Some(config),
568            _ => None,
569        }
570    }
571
572    /// Returns the Kubernetes configuration if this is a Kubernetes client config.
573    pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
574        match self {
575            ClientConfig::Kubernetes(config) => Some(config),
576            _ => None,
577        }
578    }
579}