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 an external account credential configuration.
152    ExternalAccount {
153        /// Workload identity audience.
154        audience: String,
155        /// Subject token type for STS token exchange.
156        subject_token_type: String,
157        /// STS token exchange URL.
158        token_url: String,
159        /// Path to the subject token file.
160        credential_source_file: String,
161        /// Optional service account impersonation URL.
162        service_account_impersonation_url: Option<String>,
163    },
164
165    /// Use gcloud Application Default Credentials (authorized_user).
166    /// Exchanges refresh_token for an access_token via Google's OAuth2 endpoint.
167    AuthorizedUser {
168        /// OAuth2 client ID
169        client_id: String,
170        /// OAuth2 client secret
171        client_secret: String,
172        /// OAuth2 refresh token
173        refresh_token: String,
174    },
175}
176
177impl std::fmt::Debug for GcpCredentials {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match self {
180            GcpCredentials::AccessToken { .. } => f
181                .debug_struct("GcpCredentials::AccessToken")
182                .field("token", &"[REDACTED]")
183                .finish(),
184            GcpCredentials::ServiceAccountKey { .. } => f
185                .debug_struct("GcpCredentials::ServiceAccountKey")
186                .field("json", &"[REDACTED]")
187                .finish(),
188            GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
189            GcpCredentials::ProjectedServiceAccount {
190                token_file,
191                service_account_email,
192            } => f
193                .debug_struct("GcpCredentials::ProjectedServiceAccount")
194                .field("token_file", token_file)
195                .field("service_account_email", service_account_email)
196                .finish(),
197            GcpCredentials::ExternalAccount {
198                audience,
199                subject_token_type,
200                token_url,
201                credential_source_file,
202                service_account_impersonation_url,
203            } => f
204                .debug_struct("GcpCredentials::ExternalAccount")
205                .field("audience", audience)
206                .field("subject_token_type", subject_token_type)
207                .field("token_url", token_url)
208                .field("credential_source_file", credential_source_file)
209                .field(
210                    "service_account_impersonation_url",
211                    service_account_impersonation_url,
212                )
213                .finish(),
214            GcpCredentials::AuthorizedUser { client_id, .. } => f
215                .debug_struct("GcpCredentials::AuthorizedUser")
216                .field("client_id", client_id)
217                .field("client_secret", &"[REDACTED]")
218                .field("refresh_token", &"[REDACTED]")
219                .finish(),
220        }
221    }
222}
223
224/// Configuration for GCP service account impersonation
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
227#[serde(rename_all = "camelCase", deny_unknown_fields)]
228pub struct GcpImpersonationConfig {
229    /// The email of the service account to impersonate
230    pub service_account_email: String,
231    /// The OAuth 2.0 scopes that define the access token's permissions
232    pub scopes: Vec<String>,
233    /// Optional sequence of service accounts in a delegation chain
234    pub delegates: Option<Vec<String>>,
235    /// Optional desired lifetime duration of the access token (max 3600s)
236    pub lifetime: Option<String>,
237    /// Optional target project ID override. When provided, the impersonated config
238    /// uses this project ID instead of inheriting the caller's project.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub target_project_id: Option<String>,
241    /// Optional target region override. When provided, the impersonated config
242    /// uses this region instead of inheriting the caller's region.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub target_region: Option<String>,
245}
246
247impl Default for GcpImpersonationConfig {
248    fn default() -> Self {
249        Self {
250            service_account_email: String::new(),
251            scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
252            delegates: None,
253            lifetime: Some("3600s".to_string()),
254            target_project_id: None,
255            target_region: None,
256        }
257    }
258}
259
260/// GCP client configuration
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
263#[serde(rename_all = "camelCase", deny_unknown_fields)]
264pub struct GcpClientConfig {
265    /// The GCP Project ID.
266    pub project_id: String,
267    /// The GCP region for resources.
268    pub region: String,
269    /// GCP authentication credentials.
270    pub credentials: GcpCredentials,
271    /// Service endpoint overrides for testing
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub service_overrides: Option<GcpServiceOverrides>,
274    /// The GCP project number (numeric). Resolved at runtime via Resource Manager API.
275    /// Used in IAM condition expressions where resource.name uses project number.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub project_number: Option<String>,
278}
279
280/// Service endpoint overrides for testing Azure services
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
283#[serde(rename_all = "camelCase", deny_unknown_fields)]
284pub struct AzureServiceOverrides {
285    /// Override endpoints for specific Azure services
286    /// Key is the service name (e.g., "management", "storage", "containerApps"), value is the base URL
287    pub endpoints: HashMap<String, String>,
288}
289
290/// Represents Azure authentication credentials
291#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
293#[serde(rename_all = "camelCase", tag = "type")]
294pub enum AzureCredentials {
295    /// Service principal with client secret
296    ServicePrincipal {
297        /// The client ID (application ID)
298        client_id: String,
299        /// The client secret
300        client_secret: String,
301    },
302    /// Direct access token
303    AccessToken {
304        /// The bearer token to use for authentication
305        token: String,
306    },
307    /// Azure AD Workload Identity (federated identity)
308    WorkloadIdentity {
309        /// The client ID of the managed identity or application
310        client_id: String,
311        /// The tenant ID for authentication
312        tenant_id: String,
313        /// Path to the federated token file
314        federated_token_file: String,
315        /// The authority host URL
316        authority_host: String,
317    },
318    /// Azure Managed Identity (Container Apps / App Service)
319    /// Uses IDENTITY_ENDPOINT + IDENTITY_HEADER injected by the platform
320    ManagedIdentity {
321        /// The client ID of the user-assigned managed identity
322        client_id: String,
323        /// The identity endpoint URL (from IDENTITY_ENDPOINT env var)
324        identity_endpoint: String,
325        /// The identity header secret (from IDENTITY_HEADER env var)
326        identity_header: String,
327    },
328}
329
330impl std::fmt::Debug for AzureCredentials {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        match self {
333            AzureCredentials::ServicePrincipal { client_id, .. } => f
334                .debug_struct("AzureCredentials::ServicePrincipal")
335                .field("client_id", client_id)
336                .field("client_secret", &"[REDACTED]")
337                .finish(),
338            AzureCredentials::AccessToken { .. } => f
339                .debug_struct("AzureCredentials::AccessToken")
340                .field("token", &"[REDACTED]")
341                .finish(),
342            AzureCredentials::WorkloadIdentity {
343                client_id,
344                tenant_id,
345                federated_token_file,
346                authority_host,
347            } => f
348                .debug_struct("AzureCredentials::WorkloadIdentity")
349                .field("client_id", client_id)
350                .field("tenant_id", tenant_id)
351                .field("federated_token_file", federated_token_file)
352                .field("authority_host", authority_host)
353                .finish(),
354            AzureCredentials::ManagedIdentity {
355                client_id,
356                identity_endpoint,
357                ..
358            } => f
359                .debug_struct("AzureCredentials::ManagedIdentity")
360                .field("client_id", client_id)
361                .field("identity_endpoint", identity_endpoint)
362                .field("identity_header", &"[REDACTED]")
363                .finish(),
364        }
365    }
366}
367
368/// Configuration for Azure managed identity impersonation
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
371#[serde(rename_all = "camelCase", deny_unknown_fields)]
372pub struct AzureImpersonationConfig {
373    /// The client ID of the managed identity or service principal to impersonate
374    pub client_id: String,
375    /// The scope for the access token (e.g., "https://management.azure.com/.default")
376    pub scope: String,
377    /// Optional tenant ID for cross-tenant impersonation
378    pub tenant_id: Option<String>,
379    /// Optional target subscription ID override. When provided, the impersonated config
380    /// uses this subscription instead of inheriting the caller's subscription.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub target_subscription_id: Option<String>,
383    /// Optional target region override. When provided, the impersonated config
384    /// uses this region instead of inheriting the caller's region.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub target_region: Option<String>,
387}
388
389impl Default for AzureImpersonationConfig {
390    fn default() -> Self {
391        Self {
392            client_id: String::new(),
393            scope: "https://management.azure.com/.default".to_string(),
394            tenant_id: None,
395            target_subscription_id: None,
396            target_region: None,
397        }
398    }
399}
400
401/// Azure client configuration
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
404#[serde(rename_all = "camelCase", deny_unknown_fields)]
405pub struct AzureClientConfig {
406    /// The Azure Subscription ID where resources will be deployed.
407    pub subscription_id: String,
408    /// The customer's Azure Tenant ID.
409    pub tenant_id: String,
410    /// Azure region for resources.
411    pub region: Option<String>,
412    /// Azure authentication credentials.
413    pub credentials: AzureCredentials,
414    /// Service endpoint overrides for testing
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub service_overrides: Option<AzureServiceOverrides>,
417}
418
419/// Configuration mode for Kubernetes access
420#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
421#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
422#[serde(rename_all = "camelCase", tag = "mode")]
423pub enum KubernetesClientConfig {
424    /// Use in-cluster configuration (service account tokens, etc.)
425    InCluster {
426        /// The namespace to operate in
427        #[serde(skip_serializing_if = "Option::is_none")]
428        namespace: Option<String>,
429        /// Additional headers to include in requests
430        #[serde(skip_serializing_if = "Option::is_none")]
431        additional_headers: Option<HashMap<String, String>>,
432    },
433    /// Use kubeconfig file for configuration
434    Kubeconfig {
435        /// Path to kubeconfig file (optional, defaults to standard locations)
436        #[serde(skip_serializing_if = "Option::is_none")]
437        kubeconfig_path: Option<String>,
438        /// Context name to use (optional, defaults to current-context)
439        #[serde(skip_serializing_if = "Option::is_none")]
440        context: Option<String>,
441        /// Cluster name to use (optional, defaults to context's cluster)
442        #[serde(skip_serializing_if = "Option::is_none")]
443        cluster: Option<String>,
444        /// User name to use (optional, defaults to context's user)
445        #[serde(skip_serializing_if = "Option::is_none")]
446        user: Option<String>,
447        /// The namespace to operate in
448        #[serde(skip_serializing_if = "Option::is_none")]
449        namespace: Option<String>,
450        /// Additional headers to include in requests
451        #[serde(skip_serializing_if = "Option::is_none")]
452        additional_headers: Option<HashMap<String, String>>,
453    },
454    /// Manual configuration with explicit values
455    Manual {
456        /// The Kubernetes cluster server URL
457        server_url: String,
458        /// The cluster certificate authority data (base64 encoded)
459        certificate_authority_data: Option<String>,
460        /// Skip TLS verification (insecure)
461        insecure_skip_tls_verify: Option<bool>,
462        /// Client certificate data (base64 encoded) for mutual TLS
463        client_certificate_data: Option<String>,
464        /// Client key data (base64 encoded) for mutual TLS
465        client_key_data: Option<String>,
466        /// Bearer token for authentication
467        token: Option<String>,
468        /// Username for basic authentication
469        username: Option<String>,
470        /// Password for basic authentication
471        password: Option<String>,
472        /// The namespace to operate in
473        namespace: Option<String>,
474        /// Additional headers to include in requests
475        additional_headers: HashMap<String, String>,
476    },
477}
478
479impl std::fmt::Debug for KubernetesClientConfig {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        match self {
482            KubernetesClientConfig::InCluster {
483                namespace,
484                additional_headers,
485            } => f
486                .debug_struct("KubernetesClientConfig::InCluster")
487                .field("namespace", namespace)
488                .field("additional_headers", additional_headers)
489                .finish(),
490            KubernetesClientConfig::Kubeconfig {
491                kubeconfig_path,
492                context,
493                cluster,
494                user,
495                namespace,
496                additional_headers,
497            } => f
498                .debug_struct("KubernetesClientConfig::Kubeconfig")
499                .field("kubeconfig_path", kubeconfig_path)
500                .field("context", context)
501                .field("cluster", cluster)
502                .field("user", user)
503                .field("namespace", namespace)
504                .field("additional_headers", additional_headers)
505                .finish(),
506            KubernetesClientConfig::Manual {
507                server_url,
508                certificate_authority_data,
509                insecure_skip_tls_verify,
510                client_certificate_data,
511                client_key_data,
512                token,
513                username,
514                password,
515                namespace,
516                additional_headers,
517            } => f
518                .debug_struct("KubernetesClientConfig::Manual")
519                .field("server_url", server_url)
520                .field("certificate_authority_data", certificate_authority_data)
521                .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
522                .field("client_certificate_data", client_certificate_data)
523                .field(
524                    "client_key_data",
525                    &client_key_data.as_ref().map(|_| "[REDACTED]"),
526                )
527                .field("token", &token.as_ref().map(|_| "[REDACTED]"))
528                .field("username", username)
529                .field("password", &password.as_ref().map(|_| "[REDACTED]"))
530                .field("namespace", namespace)
531                .field("additional_headers", additional_headers)
532                .finish(),
533        }
534    }
535}
536
537/// Cloud-agnostic impersonation configuration
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
539#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
540#[serde(rename_all = "camelCase", tag = "platform")]
541pub enum ImpersonationConfig {
542    Aws(AwsImpersonationConfig),
543    Gcp(GcpImpersonationConfig),
544    Azure(AzureImpersonationConfig),
545    // Kubernetes doesn't support impersonation, so we don't include it here
546}
547
548/// Configuration for different cloud platform clients
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
551#[serde(rename_all = "camelCase", tag = "platform")]
552pub enum ClientConfig {
553    Aws(Box<AwsClientConfig>),
554    Gcp(Box<GcpClientConfig>),
555    Azure(Box<AzureClientConfig>),
556    Kubernetes(Box<KubernetesClientConfig>),
557    KubernetesCloud {
558        kubernetes: Box<KubernetesClientConfig>,
559        #[cfg_attr(feature = "openapi", schema(value_type = Object))]
560        cloud: Box<ClientConfig>,
561    },
562    Local {
563        /// State directory for local resources and deployment state
564        state_directory: String,
565    },
566    /// Test platform - uses mock controllers without real cloud APIs
567    #[serde(skip)]
568    Test,
569}
570
571impl ClientConfig {
572    /// Returns the platform enum for this configuration.
573    pub fn platform(&self) -> Platform {
574        match self {
575            ClientConfig::Aws(_) => Platform::Aws,
576            ClientConfig::Gcp(_) => Platform::Gcp,
577            ClientConfig::Azure(_) => Platform::Azure,
578            ClientConfig::Kubernetes(_) => Platform::Kubernetes,
579            ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
580            ClientConfig::Local { .. } => Platform::Local,
581            ClientConfig::Test => Platform::Test,
582        }
583    }
584
585    pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
586        match self {
587            ClientConfig::KubernetesCloud { cloud, .. } => {
588                if platform == Platform::Kubernetes {
589                    Some(self.clone())
590                } else if cloud.platform() == platform {
591                    Some((**cloud).clone())
592                } else {
593                    None
594                }
595            }
596            config if config.platform() == platform => Some(config.clone()),
597            _ => None,
598        }
599    }
600
601    /// Returns the AWS configuration if this is an AWS client config.
602    pub fn aws_config(&self) -> Option<&AwsClientConfig> {
603        match self {
604            ClientConfig::Aws(config) => Some(config),
605            ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
606            _ => None,
607        }
608    }
609
610    /// Returns the GCP configuration if this is a GCP client config.
611    pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
612        match self {
613            ClientConfig::Gcp(config) => Some(config),
614            ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
615            _ => None,
616        }
617    }
618
619    /// Returns the Azure configuration if this is an Azure client config.
620    pub fn azure_config(&self) -> Option<&AzureClientConfig> {
621        match self {
622            ClientConfig::Azure(config) => Some(config),
623            ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
624            _ => None,
625        }
626    }
627
628    /// Returns the Kubernetes configuration if this is a Kubernetes client config.
629    pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
630        match self {
631            ClientConfig::Kubernetes(config) => Some(config),
632            ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
633            _ => None,
634        }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::{
641        AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
642        GcpClientConfig, GcpCredentials, KubernetesClientConfig,
643    };
644
645    #[test]
646    fn kubernetes_cloud_exposes_nested_aws_config() {
647        let config = ClientConfig::KubernetesCloud {
648            kubernetes: Box::new(KubernetesClientConfig::InCluster {
649                namespace: Some("test".to_string()),
650                additional_headers: None,
651            }),
652            cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
653                account_id: "123456789012".to_string(),
654                region: "us-east-2".to_string(),
655                credentials: AwsCredentials::AccessKeys {
656                    access_key_id: "access".to_string(),
657                    secret_access_key: "secret".to_string(),
658                    session_token: None,
659                },
660                service_overrides: None,
661            }))),
662        };
663
664        assert_eq!(config.platform(), crate::Platform::Kubernetes);
665        assert!(config.kubernetes_config().is_some());
666        assert_eq!(config.aws_config().unwrap().region, "us-east-2");
667        assert!(config.gcp_config().is_none());
668        assert!(config.azure_config().is_none());
669    }
670
671    #[test]
672    fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
673        let config = ClientConfig::KubernetesCloud {
674            kubernetes: Box::new(KubernetesClientConfig::InCluster {
675                namespace: Some("test".to_string()),
676                additional_headers: None,
677            }),
678            cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
679                account_id: "123456789012".to_string(),
680                region: "us-east-2".to_string(),
681                credentials: AwsCredentials::AccessKeys {
682                    access_key_id: "access".to_string(),
683                    secret_access_key: "secret".to_string(),
684                    session_token: None,
685                },
686                service_overrides: None,
687            }))),
688        };
689
690        let kubernetes_config = config
691            .config_for_platform(crate::Platform::Kubernetes)
692            .unwrap();
693
694        assert!(matches!(
695            kubernetes_config,
696            ClientConfig::KubernetesCloud { .. }
697        ));
698        assert!(kubernetes_config.kubernetes_config().is_some());
699        assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
700    }
701
702    #[test]
703    fn kubernetes_cloud_exposes_nested_gcp_config() {
704        let config = ClientConfig::KubernetesCloud {
705            kubernetes: Box::new(KubernetesClientConfig::InCluster {
706                namespace: Some("test".to_string()),
707                additional_headers: None,
708            }),
709            cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
710                project_id: "project".to_string(),
711                region: "us-central1".to_string(),
712                credentials: GcpCredentials::AccessToken {
713                    token: "token".to_string(),
714                },
715                service_overrides: None,
716                project_number: None,
717            }))),
718        };
719
720        assert_eq!(config.gcp_config().unwrap().project_id, "project");
721        assert!(config.aws_config().is_none());
722        assert!(config.azure_config().is_none());
723    }
724
725    #[test]
726    fn kubernetes_cloud_exposes_nested_azure_config() {
727        let config = ClientConfig::KubernetesCloud {
728            kubernetes: Box::new(KubernetesClientConfig::InCluster {
729                namespace: Some("test".to_string()),
730                additional_headers: None,
731            }),
732            cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
733                subscription_id: "sub".to_string(),
734                tenant_id: "tenant".to_string(),
735                region: Some("eastus".to_string()),
736                credentials: AzureCredentials::AccessToken {
737                    token: "token".to_string(),
738                },
739                service_overrides: None,
740            }))),
741        };
742
743        assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
744        assert!(config.aws_config().is_none());
745        assert!(config.gcp_config().is_none());
746    }
747}