1use crate::Platform;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[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 pub endpoints: HashMap<String, String>,
20}
21
22#[derive(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 pub role_arn: String,
29 pub session_name: Option<String>,
31 pub duration_seconds: Option<i32>,
33 pub external_id: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
39 pub target_region: Option<String>,
40}
41
42impl std::fmt::Debug for AwsImpersonationConfig {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("AwsImpersonationConfig")
45 .field("role_arn", &self.role_arn)
46 .field("session_name", &self.session_name)
47 .field("duration_seconds", &self.duration_seconds)
48 .field(
50 "external_id",
51 &self.external_id.as_ref().map(|_| "[REDACTED]"),
52 )
53 .field("target_region", &self.target_region)
54 .finish()
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
61#[serde(rename_all = "camelCase", deny_unknown_fields)]
62pub struct AwsWebIdentityConfig {
63 pub role_arn: String,
65 pub session_name: Option<String>,
67 pub web_identity_token_file: String,
69 pub duration_seconds: Option<i32>,
71}
72
73#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
76#[serde(rename_all = "camelCase", tag = "type")]
77pub enum AwsCredentials {
78 AccessKeys {
80 access_key_id: String,
82 secret_access_key: String,
84 session_token: Option<String>,
86 },
87 SessionCredentials {
89 access_key_id: String,
91 secret_access_key: String,
93 session_token: String,
95 expires_at: String,
97 },
98 Imds {
100 endpoint: Option<String>,
102 },
103 Container {
105 endpoint: String,
107 authorization_token: Option<String>,
109 authorization_token_file: Option<String>,
112 },
113 Profile {
115 name: String,
117 },
118 WebIdentity {
120 config: AwsWebIdentityConfig,
122 },
123}
124
125impl std::fmt::Debug for AwsCredentials {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 AwsCredentials::AccessKeys {
129 access_key_id,
130 session_token,
131 ..
132 } => f
133 .debug_struct("AwsCredentials::AccessKeys")
134 .field("access_key_id", access_key_id)
135 .field("secret_access_key", &"[REDACTED]")
136 .field(
137 "session_token",
138 &session_token.as_ref().map(|_| "[REDACTED]"),
139 )
140 .finish(),
141 AwsCredentials::SessionCredentials {
142 access_key_id,
143 expires_at,
144 ..
145 } => f
146 .debug_struct("AwsCredentials::SessionCredentials")
147 .field("access_key_id", access_key_id)
148 .field("secret_access_key", &"[REDACTED]")
149 .field("session_token", &"[REDACTED]")
150 .field("expires_at", expires_at)
151 .finish(),
152 AwsCredentials::Imds { endpoint } => f
153 .debug_struct("AwsCredentials::Imds")
154 .field("endpoint", endpoint)
155 .finish(),
156 AwsCredentials::Container {
157 endpoint,
158 authorization_token,
159 authorization_token_file,
160 } => f
161 .debug_struct("AwsCredentials::Container")
162 .field("endpoint", endpoint)
163 .field(
164 "authorization_token",
165 &authorization_token.as_ref().map(|_| "[REDACTED]"),
166 )
167 .field("authorization_token_file", authorization_token_file)
168 .finish(),
169 AwsCredentials::Profile { name } => f
170 .debug_struct("AwsCredentials::Profile")
171 .field("name", name)
172 .finish(),
173 AwsCredentials::WebIdentity { config } => f
174 .debug_struct("AwsCredentials::WebIdentity")
175 .field("config", config)
176 .finish(),
177 }
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct AwsClientConfig {
186 pub account_id: String,
188 pub region: String,
190 pub credentials: AwsCredentials,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub service_overrides: Option<AwsServiceOverrides>,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
200#[serde(rename_all = "camelCase", deny_unknown_fields)]
201pub struct GcpServiceOverrides {
202 pub endpoints: HashMap<String, String>,
205}
206
207#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
210#[serde(rename_all = "camelCase", tag = "type")]
211pub enum GcpCredentials {
212 AccessToken { token: String },
214
215 ImpersonatedServiceAccount {
217 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
219 source: Box<GcpClientConfig>,
220 config: GcpImpersonationConfig,
222 },
223
224 Federated {
227 access: Box<crate::GcpFederationAccess>,
230 source: Box<crate::GcpFederationSource>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 lifetime_seconds: Option<u32>,
234 },
235
236 ServiceAccountKey { json: String },
239
240 ServiceMetadata,
242
243 ProjectedServiceAccount {
245 token_file: String,
247 service_account_email: String,
249 },
250
251 ExternalAccount {
253 audience: String,
255 subject_token_type: String,
257 token_url: String,
259 credential_source_file: String,
261 service_account_impersonation_url: Option<String>,
263 },
264
265 AuthorizedUser {
268 client_id: String,
270 client_secret: String,
272 refresh_token: String,
274 },
275}
276
277impl std::fmt::Debug for GcpCredentials {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 match self {
280 GcpCredentials::AccessToken { .. } => f
281 .debug_struct("GcpCredentials::AccessToken")
282 .field("token", &"[REDACTED]")
283 .finish(),
284 GcpCredentials::ImpersonatedServiceAccount { source, config } => f
285 .debug_struct("GcpCredentials::ImpersonatedServiceAccount")
286 .field("source_project_id", &source.project_id)
287 .field("source_region", &source.region)
288 .field("service_account_email", &config.service_account_email)
289 .finish(),
290 GcpCredentials::Federated {
291 access,
292 source,
293 lifetime_seconds,
294 } => f
295 .debug_struct("GcpCredentials::Federated")
296 .field("access", access)
297 .field("source", source)
298 .field("lifetime_seconds", lifetime_seconds)
299 .finish(),
300 GcpCredentials::ServiceAccountKey { .. } => f
301 .debug_struct("GcpCredentials::ServiceAccountKey")
302 .field("json", &"[REDACTED]")
303 .finish(),
304 GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
305 GcpCredentials::ProjectedServiceAccount {
306 token_file,
307 service_account_email,
308 } => f
309 .debug_struct("GcpCredentials::ProjectedServiceAccount")
310 .field("token_file", token_file)
311 .field("service_account_email", service_account_email)
312 .finish(),
313 GcpCredentials::ExternalAccount {
314 audience,
315 subject_token_type,
316 token_url,
317 credential_source_file,
318 service_account_impersonation_url,
319 } => f
320 .debug_struct("GcpCredentials::ExternalAccount")
321 .field("audience", audience)
322 .field("subject_token_type", subject_token_type)
323 .field("token_url", token_url)
324 .field("credential_source_file", credential_source_file)
325 .field(
326 "service_account_impersonation_url",
327 service_account_impersonation_url,
328 )
329 .finish(),
330 GcpCredentials::AuthorizedUser { client_id, .. } => f
331 .debug_struct("GcpCredentials::AuthorizedUser")
332 .field("client_id", client_id)
333 .field("client_secret", &"[REDACTED]")
334 .field("refresh_token", &"[REDACTED]")
335 .finish(),
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
343#[serde(rename_all = "camelCase", deny_unknown_fields)]
344pub struct GcpImpersonationConfig {
345 pub service_account_email: String,
347 pub scopes: Vec<String>,
349 pub delegates: Option<Vec<String>>,
351 pub lifetime: Option<String>,
353 #[serde(skip_serializing_if = "Option::is_none")]
356 pub target_project_id: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
360 pub target_region: Option<String>,
361}
362
363impl Default for GcpImpersonationConfig {
364 fn default() -> Self {
365 Self {
366 service_account_email: String::new(),
367 scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
368 delegates: None,
369 lifetime: Some("3600s".to_string()),
370 target_project_id: None,
371 target_region: None,
372 }
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
378#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
379#[serde(rename_all = "camelCase", deny_unknown_fields)]
380pub struct GcpClientConfig {
381 pub project_id: String,
383 pub region: String,
385 pub credentials: GcpCredentials,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub service_overrides: Option<GcpServiceOverrides>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub project_number: Option<String>,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
399#[serde(rename_all = "camelCase", deny_unknown_fields)]
400pub struct AzureServiceOverrides {
401 pub endpoints: HashMap<String, String>,
404}
405
406#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
408#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
409#[serde(rename_all = "camelCase", tag = "type")]
410pub enum AzureCredentials {
411 ServicePrincipal {
413 client_id: String,
415 client_secret: String,
417 },
418 AccessToken {
420 token: String,
422 },
423 ScopedAccessTokens {
429 tokens: HashMap<String, String>,
433 },
434 VmManagedIdentity {
436 client_id: String,
438 identity_endpoint: Option<String>,
440 },
441 WorkloadIdentity {
443 client_id: String,
445 tenant_id: String,
447 federated_token_file: String,
449 authority_host: String,
451 },
452 ManagedIdentity {
455 client_id: String,
457 identity_endpoint: String,
459 identity_header: String,
461 },
462}
463
464impl std::fmt::Debug for AzureCredentials {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 match self {
467 AzureCredentials::ServicePrincipal { client_id, .. } => f
468 .debug_struct("AzureCredentials::ServicePrincipal")
469 .field("client_id", client_id)
470 .field("client_secret", &"[REDACTED]")
471 .finish(),
472 AzureCredentials::AccessToken { .. } => f
473 .debug_struct("AzureCredentials::AccessToken")
474 .field("token", &"[REDACTED]")
475 .finish(),
476 AzureCredentials::ScopedAccessTokens { tokens } => f
477 .debug_struct("AzureCredentials::ScopedAccessTokens")
478 .field("scopes", &tokens.keys().collect::<Vec<_>>())
479 .field("tokens", &"[REDACTED]")
480 .finish(),
481 AzureCredentials::VmManagedIdentity {
482 client_id,
483 identity_endpoint,
484 } => f
485 .debug_struct("AzureCredentials::VmManagedIdentity")
486 .field("client_id", client_id)
487 .field("identity_endpoint", identity_endpoint)
488 .finish(),
489 AzureCredentials::WorkloadIdentity {
490 client_id,
491 tenant_id,
492 federated_token_file,
493 authority_host,
494 } => f
495 .debug_struct("AzureCredentials::WorkloadIdentity")
496 .field("client_id", client_id)
497 .field("tenant_id", tenant_id)
498 .field("federated_token_file", federated_token_file)
499 .field("authority_host", authority_host)
500 .finish(),
501 AzureCredentials::ManagedIdentity {
502 client_id,
503 identity_endpoint,
504 ..
505 } => f
506 .debug_struct("AzureCredentials::ManagedIdentity")
507 .field("client_id", client_id)
508 .field("identity_endpoint", identity_endpoint)
509 .field("identity_header", &"[REDACTED]")
510 .finish(),
511 }
512 }
513}
514
515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
517#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
518#[serde(rename_all = "camelCase", deny_unknown_fields)]
519pub struct AzureImpersonationConfig {
520 pub client_id: String,
522 pub scope: String,
524 pub tenant_id: Option<String>,
526 #[serde(skip_serializing_if = "Option::is_none")]
529 pub target_subscription_id: Option<String>,
530 #[serde(skip_serializing_if = "Option::is_none")]
533 pub target_region: Option<String>,
534}
535
536impl Default for AzureImpersonationConfig {
537 fn default() -> Self {
538 Self {
539 client_id: String::new(),
540 scope: "https://management.azure.com/.default".to_string(),
541 tenant_id: None,
542 target_subscription_id: None,
543 target_region: None,
544 }
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
551#[serde(rename_all = "camelCase", deny_unknown_fields)]
552pub struct AzureClientConfig {
553 pub subscription_id: String,
555 pub tenant_id: String,
557 pub region: Option<String>,
559 pub credentials: AzureCredentials,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub service_overrides: Option<AzureServiceOverrides>,
564}
565
566#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
568#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
569#[serde(rename_all = "camelCase", tag = "mode")]
570pub enum KubernetesClientConfig {
571 InCluster {
573 #[serde(skip_serializing_if = "Option::is_none")]
575 namespace: Option<String>,
576 #[serde(skip_serializing_if = "Option::is_none")]
578 additional_headers: Option<HashMap<String, String>>,
579 },
580 Kubeconfig {
582 #[serde(skip_serializing_if = "Option::is_none")]
584 kubeconfig_path: Option<String>,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 context: Option<String>,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 cluster: Option<String>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 user: Option<String>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 namespace: Option<String>,
597 #[serde(skip_serializing_if = "Option::is_none")]
599 additional_headers: Option<HashMap<String, String>>,
600 },
601 Manual {
603 server_url: String,
605 certificate_authority_data: Option<String>,
607 insecure_skip_tls_verify: Option<bool>,
609 client_certificate_data: Option<String>,
611 client_key_data: Option<String>,
613 token: Option<String>,
615 username: Option<String>,
617 password: Option<String>,
619 namespace: Option<String>,
621 additional_headers: HashMap<String, String>,
623 },
624}
625
626impl std::fmt::Debug for KubernetesClientConfig {
627 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628 match self {
629 KubernetesClientConfig::InCluster {
630 namespace,
631 additional_headers,
632 } => f
633 .debug_struct("KubernetesClientConfig::InCluster")
634 .field("namespace", namespace)
635 .field("additional_headers", additional_headers)
636 .finish(),
637 KubernetesClientConfig::Kubeconfig {
638 kubeconfig_path,
639 context,
640 cluster,
641 user,
642 namespace,
643 additional_headers,
644 } => f
645 .debug_struct("KubernetesClientConfig::Kubeconfig")
646 .field("kubeconfig_path", kubeconfig_path)
647 .field("context", context)
648 .field("cluster", cluster)
649 .field("user", user)
650 .field("namespace", namespace)
651 .field("additional_headers", additional_headers)
652 .finish(),
653 KubernetesClientConfig::Manual {
654 server_url,
655 certificate_authority_data,
656 insecure_skip_tls_verify,
657 client_certificate_data,
658 client_key_data,
659 token,
660 username,
661 password,
662 namespace,
663 additional_headers,
664 } => f
665 .debug_struct("KubernetesClientConfig::Manual")
666 .field("server_url", server_url)
667 .field("certificate_authority_data", certificate_authority_data)
668 .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
669 .field("client_certificate_data", client_certificate_data)
670 .field(
671 "client_key_data",
672 &client_key_data.as_ref().map(|_| "[REDACTED]"),
673 )
674 .field("token", &token.as_ref().map(|_| "[REDACTED]"))
675 .field("username", username)
676 .field("password", &password.as_ref().map(|_| "[REDACTED]"))
677 .field("namespace", namespace)
678 .field("additional_headers", additional_headers)
679 .finish(),
680 }
681 }
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
686#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
687#[serde(rename_all = "camelCase", tag = "platform")]
688pub enum ImpersonationConfig {
689 Aws(AwsImpersonationConfig),
690 Gcp(GcpImpersonationConfig),
691 Azure(AzureImpersonationConfig),
692 }
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
697#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
698#[serde(rename_all = "camelCase", tag = "platform")]
699pub enum ClientConfig {
700 Aws(Box<AwsClientConfig>),
701 Gcp(Box<GcpClientConfig>),
702 Azure(Box<AzureClientConfig>),
703 Kubernetes(Box<KubernetesClientConfig>),
704 KubernetesCloud {
705 kubernetes: Box<KubernetesClientConfig>,
706 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
707 cloud: Box<ClientConfig>,
708 },
709 Local {
710 state_directory: String,
712 },
713 #[serde(skip)]
715 Machines,
716 #[serde(skip)]
718 Test,
719}
720
721impl ClientConfig {
722 pub fn platform(&self) -> Platform {
724 match self {
725 ClientConfig::Aws(_) => Platform::Aws,
726 ClientConfig::Gcp(_) => Platform::Gcp,
727 ClientConfig::Azure(_) => Platform::Azure,
728 ClientConfig::Kubernetes(_) => Platform::Kubernetes,
729 ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
730 ClientConfig::Local { .. } => Platform::Local,
731 ClientConfig::Machines => Platform::Machines,
732 ClientConfig::Test => Platform::Test,
733 }
734 }
735
736 pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
737 match self {
738 ClientConfig::KubernetesCloud { cloud, .. } => {
739 if platform == Platform::Kubernetes {
740 Some(self.clone())
741 } else if cloud.platform() == platform {
742 Some((**cloud).clone())
743 } else {
744 None
745 }
746 }
747 config if config.platform() == platform => Some(config.clone()),
748 _ => None,
749 }
750 }
751
752 pub fn aws_config(&self) -> Option<&AwsClientConfig> {
754 match self {
755 ClientConfig::Aws(config) => Some(config),
756 ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
757 _ => None,
758 }
759 }
760
761 pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
763 match self {
764 ClientConfig::Gcp(config) => Some(config),
765 ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
766 _ => None,
767 }
768 }
769
770 pub fn azure_config(&self) -> Option<&AzureClientConfig> {
772 match self {
773 ClientConfig::Azure(config) => Some(config),
774 ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
775 _ => None,
776 }
777 }
778
779 pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
781 match self {
782 ClientConfig::Kubernetes(config) => Some(config),
783 ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
784 _ => None,
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::{
792 AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
793 GcpClientConfig, GcpCredentials, KubernetesClientConfig,
794 };
795
796 #[test]
797 fn aws_credentials_wire_format_matches_manager_api_contract() {
798 let cases = [
799 (
800 AwsCredentials::AccessKeys {
801 access_key_id: "access-key".to_string(),
802 secret_access_key: "secret-key".to_string(),
803 session_token: Some("session-token".to_string()),
804 },
805 serde_json::json!({
806 "type": "accessKeys",
807 "access_key_id": "access-key",
808 "secret_access_key": "secret-key",
809 "session_token": "session-token",
810 }),
811 ),
812 (
813 AwsCredentials::SessionCredentials {
814 access_key_id: "access-key".to_string(),
815 secret_access_key: "secret-key".to_string(),
816 session_token: "session-token".to_string(),
817 expires_at: "2099-01-01T00:00:00Z".to_string(),
818 },
819 serde_json::json!({
820 "type": "sessionCredentials",
821 "access_key_id": "access-key",
822 "secret_access_key": "secret-key",
823 "session_token": "session-token",
824 "expires_at": "2099-01-01T00:00:00Z",
825 }),
826 ),
827 ];
828
829 for (credentials, expected) in cases {
830 let serialized = serde_json::to_value(&credentials).unwrap();
831 assert_eq!(serialized, expected);
832 assert_eq!(
833 serde_json::from_value::<AwsCredentials>(serialized).unwrap(),
834 credentials
835 );
836 }
837 }
838
839 #[test]
840 fn kubernetes_cloud_exposes_nested_aws_config() {
841 let config = ClientConfig::KubernetesCloud {
842 kubernetes: Box::new(KubernetesClientConfig::InCluster {
843 namespace: Some("test".to_string()),
844 additional_headers: None,
845 }),
846 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
847 account_id: "123456789012".to_string(),
848 region: "us-east-2".to_string(),
849 credentials: AwsCredentials::AccessKeys {
850 access_key_id: "access".to_string(),
851 secret_access_key: "secret".to_string(),
852 session_token: None,
853 },
854 service_overrides: None,
855 }))),
856 };
857
858 assert_eq!(config.platform(), crate::Platform::Kubernetes);
859 assert!(config.kubernetes_config().is_some());
860 assert_eq!(config.aws_config().unwrap().region, "us-east-2");
861 assert!(config.gcp_config().is_none());
862 assert!(config.azure_config().is_none());
863 }
864
865 #[test]
866 fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
867 let config = ClientConfig::KubernetesCloud {
868 kubernetes: Box::new(KubernetesClientConfig::InCluster {
869 namespace: Some("test".to_string()),
870 additional_headers: None,
871 }),
872 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
873 account_id: "123456789012".to_string(),
874 region: "us-east-2".to_string(),
875 credentials: AwsCredentials::AccessKeys {
876 access_key_id: "access".to_string(),
877 secret_access_key: "secret".to_string(),
878 session_token: None,
879 },
880 service_overrides: None,
881 }))),
882 };
883
884 let kubernetes_config = config
885 .config_for_platform(crate::Platform::Kubernetes)
886 .unwrap();
887
888 assert!(matches!(
889 kubernetes_config,
890 ClientConfig::KubernetesCloud { .. }
891 ));
892 assert!(kubernetes_config.kubernetes_config().is_some());
893 assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
894 }
895
896 #[test]
897 fn kubernetes_cloud_exposes_nested_gcp_config() {
898 let config = ClientConfig::KubernetesCloud {
899 kubernetes: Box::new(KubernetesClientConfig::InCluster {
900 namespace: Some("test".to_string()),
901 additional_headers: None,
902 }),
903 cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
904 project_id: "project".to_string(),
905 region: "us-central1".to_string(),
906 credentials: GcpCredentials::AccessToken {
907 token: "token".to_string(),
908 },
909 service_overrides: None,
910 project_number: None,
911 }))),
912 };
913
914 assert_eq!(config.gcp_config().unwrap().project_id, "project");
915 assert!(config.aws_config().is_none());
916 assert!(config.azure_config().is_none());
917 }
918
919 #[test]
920 fn kubernetes_cloud_exposes_nested_azure_config() {
921 let config = ClientConfig::KubernetesCloud {
922 kubernetes: Box::new(KubernetesClientConfig::InCluster {
923 namespace: Some("test".to_string()),
924 additional_headers: None,
925 }),
926 cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
927 subscription_id: "sub".to_string(),
928 tenant_id: "tenant".to_string(),
929 region: Some("eastus".to_string()),
930 credentials: AzureCredentials::AccessToken {
931 token: "token".to_string(),
932 },
933 service_overrides: None,
934 }))),
935 };
936
937 assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
938 assert!(config.aws_config().is_none());
939 assert!(config.gcp_config().is_none());
940 }
941
942 #[test]
943 fn scoped_azure_tokens_debug_redacts_every_token() {
944 let credentials = AzureCredentials::ScopedAccessTokens {
945 tokens: std::collections::HashMap::from([
946 (
947 "https://management.azure.com/.default".to_string(),
948 "management-secret".to_string(),
949 ),
950 (
951 "https://storage.azure.com/.default".to_string(),
952 "storage-secret".to_string(),
953 ),
954 ]),
955 };
956
957 let debug = format!("{credentials:?}");
958 assert!(debug.contains("https://management.azure.com/.default"));
959 assert!(debug.contains("https://storage.azure.com/.default"));
960 assert!(!debug.contains("management-secret"));
961 assert!(!debug.contains("storage-secret"));
962 assert!(debug.contains("[REDACTED]"));
963 }
964}