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    KubernetesCloud {
527        kubernetes: Box<KubernetesClientConfig>,
528        #[cfg_attr(feature = "openapi", schema(value_type = Object))]
529        cloud: Box<ClientConfig>,
530    },
531    Local {
532        /// State directory for local resources and deployment state
533        state_directory: String,
534    },
535    /// Test platform - uses mock controllers without real cloud APIs
536    #[serde(skip)]
537    Test,
538}
539
540impl ClientConfig {
541    /// Returns the platform enum for this configuration.
542    pub fn platform(&self) -> Platform {
543        match self {
544            ClientConfig::Aws(_) => Platform::Aws,
545            ClientConfig::Gcp(_) => Platform::Gcp,
546            ClientConfig::Azure(_) => Platform::Azure,
547            ClientConfig::Kubernetes(_) => Platform::Kubernetes,
548            ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
549            ClientConfig::Local { .. } => Platform::Local,
550            ClientConfig::Test => Platform::Test,
551        }
552    }
553
554    pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
555        match self {
556            ClientConfig::KubernetesCloud { cloud, .. } => {
557                if platform == Platform::Kubernetes {
558                    Some(self.clone())
559                } else if cloud.platform() == platform {
560                    Some((**cloud).clone())
561                } else {
562                    None
563                }
564            }
565            config if config.platform() == platform => Some(config.clone()),
566            _ => None,
567        }
568    }
569
570    /// Returns the AWS configuration if this is an AWS client config.
571    pub fn aws_config(&self) -> Option<&AwsClientConfig> {
572        match self {
573            ClientConfig::Aws(config) => Some(config),
574            ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
575            _ => None,
576        }
577    }
578
579    /// Returns the GCP configuration if this is a GCP client config.
580    pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
581        match self {
582            ClientConfig::Gcp(config) => Some(config),
583            ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
584            _ => None,
585        }
586    }
587
588    /// Returns the Azure configuration if this is an Azure client config.
589    pub fn azure_config(&self) -> Option<&AzureClientConfig> {
590        match self {
591            ClientConfig::Azure(config) => Some(config),
592            ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
593            _ => None,
594        }
595    }
596
597    /// Returns the Kubernetes configuration if this is a Kubernetes client config.
598    pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
599        match self {
600            ClientConfig::Kubernetes(config) => Some(config),
601            ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
602            _ => None,
603        }
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::{
610        AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
611        GcpClientConfig, GcpCredentials, KubernetesClientConfig,
612    };
613
614    #[test]
615    fn kubernetes_cloud_exposes_nested_aws_config() {
616        let config = ClientConfig::KubernetesCloud {
617            kubernetes: Box::new(KubernetesClientConfig::InCluster {
618                namespace: Some("test".to_string()),
619                additional_headers: None,
620            }),
621            cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
622                account_id: "123456789012".to_string(),
623                region: "us-east-2".to_string(),
624                credentials: AwsCredentials::AccessKeys {
625                    access_key_id: "access".to_string(),
626                    secret_access_key: "secret".to_string(),
627                    session_token: None,
628                },
629                service_overrides: None,
630            }))),
631        };
632
633        assert_eq!(config.platform(), crate::Platform::Kubernetes);
634        assert!(config.kubernetes_config().is_some());
635        assert_eq!(config.aws_config().unwrap().region, "us-east-2");
636        assert!(config.gcp_config().is_none());
637        assert!(config.azure_config().is_none());
638    }
639
640    #[test]
641    fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
642        let config = ClientConfig::KubernetesCloud {
643            kubernetes: Box::new(KubernetesClientConfig::InCluster {
644                namespace: Some("test".to_string()),
645                additional_headers: None,
646            }),
647            cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
648                account_id: "123456789012".to_string(),
649                region: "us-east-2".to_string(),
650                credentials: AwsCredentials::AccessKeys {
651                    access_key_id: "access".to_string(),
652                    secret_access_key: "secret".to_string(),
653                    session_token: None,
654                },
655                service_overrides: None,
656            }))),
657        };
658
659        let kubernetes_config = config
660            .config_for_platform(crate::Platform::Kubernetes)
661            .unwrap();
662
663        assert!(matches!(
664            kubernetes_config,
665            ClientConfig::KubernetesCloud { .. }
666        ));
667        assert!(kubernetes_config.kubernetes_config().is_some());
668        assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
669    }
670
671    #[test]
672    fn kubernetes_cloud_exposes_nested_gcp_config() {
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::Gcp(Box::new(GcpClientConfig {
679                project_id: "project".to_string(),
680                region: "us-central1".to_string(),
681                credentials: GcpCredentials::AccessToken {
682                    token: "token".to_string(),
683                },
684                service_overrides: None,
685                project_number: None,
686            }))),
687        };
688
689        assert_eq!(config.gcp_config().unwrap().project_id, "project");
690        assert!(config.aws_config().is_none());
691        assert!(config.azure_config().is_none());
692    }
693
694    #[test]
695    fn kubernetes_cloud_exposes_nested_azure_config() {
696        let config = ClientConfig::KubernetesCloud {
697            kubernetes: Box::new(KubernetesClientConfig::InCluster {
698                namespace: Some("test".to_string()),
699                additional_headers: None,
700            }),
701            cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
702                subscription_id: "sub".to_string(),
703                tenant_id: "tenant".to_string(),
704                region: Some("eastus".to_string()),
705                credentials: AzureCredentials::AccessToken {
706                    token: "token".to_string(),
707                },
708                service_overrides: None,
709            }))),
710        };
711
712        assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
713        assert!(config.aws_config().is_none());
714        assert!(config.gcp_config().is_none());
715    }
716}