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(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 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
42#[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 pub role_arn: String,
49 pub session_name: Option<String>,
51 pub web_identity_token_file: String,
53 pub duration_seconds: Option<i32>,
55}
56
57#[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 AccessKeys {
64 access_key_id: String,
66 secret_access_key: String,
68 session_token: Option<String>,
70 },
71 SessionCredentials {
73 access_key_id: String,
75 secret_access_key: String,
77 session_token: String,
79 expires_at: String,
81 },
82 Imds {
84 endpoint: Option<String>,
86 },
87 Container {
89 endpoint: String,
91 authorization_token: Option<String>,
93 authorization_token_file: Option<String>,
96 },
97 Profile {
99 name: String,
101 },
102 WebIdentity {
104 config: AwsWebIdentityConfig,
106 },
107}
108
109impl std::fmt::Debug for AwsCredentials {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 AwsCredentials::AccessKeys {
113 access_key_id,
114 session_token,
115 ..
116 } => f
117 .debug_struct("AwsCredentials::AccessKeys")
118 .field("access_key_id", access_key_id)
119 .field("secret_access_key", &"[REDACTED]")
120 .field(
121 "session_token",
122 &session_token.as_ref().map(|_| "[REDACTED]"),
123 )
124 .finish(),
125 AwsCredentials::SessionCredentials {
126 access_key_id,
127 expires_at,
128 ..
129 } => f
130 .debug_struct("AwsCredentials::SessionCredentials")
131 .field("access_key_id", access_key_id)
132 .field("secret_access_key", &"[REDACTED]")
133 .field("session_token", &"[REDACTED]")
134 .field("expires_at", expires_at)
135 .finish(),
136 AwsCredentials::Imds { endpoint } => f
137 .debug_struct("AwsCredentials::Imds")
138 .field("endpoint", endpoint)
139 .finish(),
140 AwsCredentials::Container {
141 endpoint,
142 authorization_token,
143 authorization_token_file,
144 } => f
145 .debug_struct("AwsCredentials::Container")
146 .field("endpoint", endpoint)
147 .field(
148 "authorization_token",
149 &authorization_token.as_ref().map(|_| "[REDACTED]"),
150 )
151 .field("authorization_token_file", authorization_token_file)
152 .finish(),
153 AwsCredentials::Profile { name } => f
154 .debug_struct("AwsCredentials::Profile")
155 .field("name", name)
156 .finish(),
157 AwsCredentials::WebIdentity { config } => f
158 .debug_struct("AwsCredentials::WebIdentity")
159 .field("config", config)
160 .finish(),
161 }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct AwsClientConfig {
170 pub account_id: String,
172 pub region: String,
174 pub credentials: AwsCredentials,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub service_overrides: Option<AwsServiceOverrides>,
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 GcpServiceOverrides {
186 pub endpoints: HashMap<String, String>,
189}
190
191#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
194#[serde(rename_all = "camelCase", tag = "type")]
195pub enum GcpCredentials {
196 AccessToken { token: String },
198
199 ImpersonatedServiceAccount {
201 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
203 source: Box<GcpClientConfig>,
204 config: GcpImpersonationConfig,
206 },
207
208 ServiceAccountKey { json: String },
211
212 ServiceMetadata,
214
215 ProjectedServiceAccount {
217 token_file: String,
219 service_account_email: String,
221 },
222
223 ExternalAccount {
225 audience: String,
227 subject_token_type: String,
229 token_url: String,
231 credential_source_file: String,
233 service_account_impersonation_url: Option<String>,
235 },
236
237 AuthorizedUser {
240 client_id: String,
242 client_secret: String,
244 refresh_token: String,
246 },
247}
248
249impl std::fmt::Debug for GcpCredentials {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 match self {
252 GcpCredentials::AccessToken { .. } => f
253 .debug_struct("GcpCredentials::AccessToken")
254 .field("token", &"[REDACTED]")
255 .finish(),
256 GcpCredentials::ImpersonatedServiceAccount { source, config } => f
257 .debug_struct("GcpCredentials::ImpersonatedServiceAccount")
258 .field("source_project_id", &source.project_id)
259 .field("source_region", &source.region)
260 .field("service_account_email", &config.service_account_email)
261 .finish(),
262 GcpCredentials::ServiceAccountKey { .. } => f
263 .debug_struct("GcpCredentials::ServiceAccountKey")
264 .field("json", &"[REDACTED]")
265 .finish(),
266 GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
267 GcpCredentials::ProjectedServiceAccount {
268 token_file,
269 service_account_email,
270 } => f
271 .debug_struct("GcpCredentials::ProjectedServiceAccount")
272 .field("token_file", token_file)
273 .field("service_account_email", service_account_email)
274 .finish(),
275 GcpCredentials::ExternalAccount {
276 audience,
277 subject_token_type,
278 token_url,
279 credential_source_file,
280 service_account_impersonation_url,
281 } => f
282 .debug_struct("GcpCredentials::ExternalAccount")
283 .field("audience", audience)
284 .field("subject_token_type", subject_token_type)
285 .field("token_url", token_url)
286 .field("credential_source_file", credential_source_file)
287 .field(
288 "service_account_impersonation_url",
289 service_account_impersonation_url,
290 )
291 .finish(),
292 GcpCredentials::AuthorizedUser { client_id, .. } => f
293 .debug_struct("GcpCredentials::AuthorizedUser")
294 .field("client_id", client_id)
295 .field("client_secret", &"[REDACTED]")
296 .field("refresh_token", &"[REDACTED]")
297 .finish(),
298 }
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
305#[serde(rename_all = "camelCase", deny_unknown_fields)]
306pub struct GcpImpersonationConfig {
307 pub service_account_email: String,
309 pub scopes: Vec<String>,
311 pub delegates: Option<Vec<String>>,
313 pub lifetime: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
318 pub target_project_id: Option<String>,
319 #[serde(skip_serializing_if = "Option::is_none")]
322 pub target_region: Option<String>,
323}
324
325impl Default for GcpImpersonationConfig {
326 fn default() -> Self {
327 Self {
328 service_account_email: String::new(),
329 scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
330 delegates: None,
331 lifetime: Some("3600s".to_string()),
332 target_project_id: None,
333 target_region: None,
334 }
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
341#[serde(rename_all = "camelCase", deny_unknown_fields)]
342pub struct GcpClientConfig {
343 pub project_id: String,
345 pub region: String,
347 pub credentials: GcpCredentials,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub service_overrides: Option<GcpServiceOverrides>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub project_number: Option<String>,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
361#[serde(rename_all = "camelCase", deny_unknown_fields)]
362pub struct AzureServiceOverrides {
363 pub endpoints: HashMap<String, String>,
366}
367
368#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
371#[serde(rename_all = "camelCase", tag = "type")]
372pub enum AzureCredentials {
373 ServicePrincipal {
375 client_id: String,
377 client_secret: String,
379 },
380 AccessToken {
382 token: String,
384 },
385 ScopedAccessTokens {
391 tokens: HashMap<String, String>,
395 },
396 VmManagedIdentity {
398 client_id: String,
400 identity_endpoint: Option<String>,
402 },
403 WorkloadIdentity {
405 client_id: String,
407 tenant_id: String,
409 federated_token_file: String,
411 authority_host: String,
413 },
414 ManagedIdentity {
417 client_id: String,
419 identity_endpoint: String,
421 identity_header: String,
423 },
424}
425
426impl std::fmt::Debug for AzureCredentials {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 AzureCredentials::ServicePrincipal { client_id, .. } => f
430 .debug_struct("AzureCredentials::ServicePrincipal")
431 .field("client_id", client_id)
432 .field("client_secret", &"[REDACTED]")
433 .finish(),
434 AzureCredentials::AccessToken { .. } => f
435 .debug_struct("AzureCredentials::AccessToken")
436 .field("token", &"[REDACTED]")
437 .finish(),
438 AzureCredentials::ScopedAccessTokens { tokens } => f
439 .debug_struct("AzureCredentials::ScopedAccessTokens")
440 .field("scopes", &tokens.keys().collect::<Vec<_>>())
441 .field("tokens", &"[REDACTED]")
442 .finish(),
443 AzureCredentials::VmManagedIdentity {
444 client_id,
445 identity_endpoint,
446 } => f
447 .debug_struct("AzureCredentials::VmManagedIdentity")
448 .field("client_id", client_id)
449 .field("identity_endpoint", identity_endpoint)
450 .finish(),
451 AzureCredentials::WorkloadIdentity {
452 client_id,
453 tenant_id,
454 federated_token_file,
455 authority_host,
456 } => f
457 .debug_struct("AzureCredentials::WorkloadIdentity")
458 .field("client_id", client_id)
459 .field("tenant_id", tenant_id)
460 .field("federated_token_file", federated_token_file)
461 .field("authority_host", authority_host)
462 .finish(),
463 AzureCredentials::ManagedIdentity {
464 client_id,
465 identity_endpoint,
466 ..
467 } => f
468 .debug_struct("AzureCredentials::ManagedIdentity")
469 .field("client_id", client_id)
470 .field("identity_endpoint", identity_endpoint)
471 .field("identity_header", &"[REDACTED]")
472 .finish(),
473 }
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
480#[serde(rename_all = "camelCase", deny_unknown_fields)]
481pub struct AzureImpersonationConfig {
482 pub client_id: String,
484 pub scope: String,
486 pub tenant_id: Option<String>,
488 #[serde(skip_serializing_if = "Option::is_none")]
491 pub target_subscription_id: Option<String>,
492 #[serde(skip_serializing_if = "Option::is_none")]
495 pub target_region: Option<String>,
496}
497
498impl Default for AzureImpersonationConfig {
499 fn default() -> Self {
500 Self {
501 client_id: String::new(),
502 scope: "https://management.azure.com/.default".to_string(),
503 tenant_id: None,
504 target_subscription_id: None,
505 target_region: None,
506 }
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
513#[serde(rename_all = "camelCase", deny_unknown_fields)]
514pub struct AzureClientConfig {
515 pub subscription_id: String,
517 pub tenant_id: String,
519 pub region: Option<String>,
521 pub credentials: AzureCredentials,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub service_overrides: Option<AzureServiceOverrides>,
526}
527
528#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
530#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
531#[serde(rename_all = "camelCase", tag = "mode")]
532pub enum KubernetesClientConfig {
533 InCluster {
535 #[serde(skip_serializing_if = "Option::is_none")]
537 namespace: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 additional_headers: Option<HashMap<String, String>>,
541 },
542 Kubeconfig {
544 #[serde(skip_serializing_if = "Option::is_none")]
546 kubeconfig_path: Option<String>,
547 #[serde(skip_serializing_if = "Option::is_none")]
549 context: Option<String>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 cluster: Option<String>,
553 #[serde(skip_serializing_if = "Option::is_none")]
555 user: Option<String>,
556 #[serde(skip_serializing_if = "Option::is_none")]
558 namespace: Option<String>,
559 #[serde(skip_serializing_if = "Option::is_none")]
561 additional_headers: Option<HashMap<String, String>>,
562 },
563 Manual {
565 server_url: String,
567 certificate_authority_data: Option<String>,
569 insecure_skip_tls_verify: Option<bool>,
571 client_certificate_data: Option<String>,
573 client_key_data: Option<String>,
575 token: Option<String>,
577 username: Option<String>,
579 password: Option<String>,
581 namespace: Option<String>,
583 additional_headers: HashMap<String, String>,
585 },
586}
587
588impl std::fmt::Debug for KubernetesClientConfig {
589 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590 match self {
591 KubernetesClientConfig::InCluster {
592 namespace,
593 additional_headers,
594 } => f
595 .debug_struct("KubernetesClientConfig::InCluster")
596 .field("namespace", namespace)
597 .field("additional_headers", additional_headers)
598 .finish(),
599 KubernetesClientConfig::Kubeconfig {
600 kubeconfig_path,
601 context,
602 cluster,
603 user,
604 namespace,
605 additional_headers,
606 } => f
607 .debug_struct("KubernetesClientConfig::Kubeconfig")
608 .field("kubeconfig_path", kubeconfig_path)
609 .field("context", context)
610 .field("cluster", cluster)
611 .field("user", user)
612 .field("namespace", namespace)
613 .field("additional_headers", additional_headers)
614 .finish(),
615 KubernetesClientConfig::Manual {
616 server_url,
617 certificate_authority_data,
618 insecure_skip_tls_verify,
619 client_certificate_data,
620 client_key_data,
621 token,
622 username,
623 password,
624 namespace,
625 additional_headers,
626 } => f
627 .debug_struct("KubernetesClientConfig::Manual")
628 .field("server_url", server_url)
629 .field("certificate_authority_data", certificate_authority_data)
630 .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
631 .field("client_certificate_data", client_certificate_data)
632 .field(
633 "client_key_data",
634 &client_key_data.as_ref().map(|_| "[REDACTED]"),
635 )
636 .field("token", &token.as_ref().map(|_| "[REDACTED]"))
637 .field("username", username)
638 .field("password", &password.as_ref().map(|_| "[REDACTED]"))
639 .field("namespace", namespace)
640 .field("additional_headers", additional_headers)
641 .finish(),
642 }
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
649#[serde(rename_all = "camelCase", tag = "platform")]
650pub enum ImpersonationConfig {
651 Aws(AwsImpersonationConfig),
652 Gcp(GcpImpersonationConfig),
653 Azure(AzureImpersonationConfig),
654 }
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
660#[serde(rename_all = "camelCase", tag = "platform")]
661pub enum ClientConfig {
662 Aws(Box<AwsClientConfig>),
663 Gcp(Box<GcpClientConfig>),
664 Azure(Box<AzureClientConfig>),
665 Kubernetes(Box<KubernetesClientConfig>),
666 KubernetesCloud {
667 kubernetes: Box<KubernetesClientConfig>,
668 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
669 cloud: Box<ClientConfig>,
670 },
671 Local {
672 state_directory: String,
674 },
675 #[serde(skip)]
677 Machines,
678 #[serde(skip)]
680 Test,
681}
682
683impl ClientConfig {
684 pub fn platform(&self) -> Platform {
686 match self {
687 ClientConfig::Aws(_) => Platform::Aws,
688 ClientConfig::Gcp(_) => Platform::Gcp,
689 ClientConfig::Azure(_) => Platform::Azure,
690 ClientConfig::Kubernetes(_) => Platform::Kubernetes,
691 ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
692 ClientConfig::Local { .. } => Platform::Local,
693 ClientConfig::Machines => Platform::Machines,
694 ClientConfig::Test => Platform::Test,
695 }
696 }
697
698 pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
699 match self {
700 ClientConfig::KubernetesCloud { cloud, .. } => {
701 if platform == Platform::Kubernetes {
702 Some(self.clone())
703 } else if cloud.platform() == platform {
704 Some((**cloud).clone())
705 } else {
706 None
707 }
708 }
709 config if config.platform() == platform => Some(config.clone()),
710 _ => None,
711 }
712 }
713
714 pub fn aws_config(&self) -> Option<&AwsClientConfig> {
716 match self {
717 ClientConfig::Aws(config) => Some(config),
718 ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
719 _ => None,
720 }
721 }
722
723 pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
725 match self {
726 ClientConfig::Gcp(config) => Some(config),
727 ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
728 _ => None,
729 }
730 }
731
732 pub fn azure_config(&self) -> Option<&AzureClientConfig> {
734 match self {
735 ClientConfig::Azure(config) => Some(config),
736 ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
737 _ => None,
738 }
739 }
740
741 pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
743 match self {
744 ClientConfig::Kubernetes(config) => Some(config),
745 ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
746 _ => None,
747 }
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::{
754 AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
755 GcpClientConfig, GcpCredentials, KubernetesClientConfig,
756 };
757
758 #[test]
759 fn aws_credentials_wire_format_matches_manager_api_contract() {
760 let cases = [
761 (
762 AwsCredentials::AccessKeys {
763 access_key_id: "access-key".to_string(),
764 secret_access_key: "secret-key".to_string(),
765 session_token: Some("session-token".to_string()),
766 },
767 serde_json::json!({
768 "type": "accessKeys",
769 "access_key_id": "access-key",
770 "secret_access_key": "secret-key",
771 "session_token": "session-token",
772 }),
773 ),
774 (
775 AwsCredentials::SessionCredentials {
776 access_key_id: "access-key".to_string(),
777 secret_access_key: "secret-key".to_string(),
778 session_token: "session-token".to_string(),
779 expires_at: "2099-01-01T00:00:00Z".to_string(),
780 },
781 serde_json::json!({
782 "type": "sessionCredentials",
783 "access_key_id": "access-key",
784 "secret_access_key": "secret-key",
785 "session_token": "session-token",
786 "expires_at": "2099-01-01T00:00:00Z",
787 }),
788 ),
789 ];
790
791 for (credentials, expected) in cases {
792 let serialized = serde_json::to_value(&credentials).unwrap();
793 assert_eq!(serialized, expected);
794 assert_eq!(
795 serde_json::from_value::<AwsCredentials>(serialized).unwrap(),
796 credentials
797 );
798 }
799 }
800
801 #[test]
802 fn kubernetes_cloud_exposes_nested_aws_config() {
803 let config = ClientConfig::KubernetesCloud {
804 kubernetes: Box::new(KubernetesClientConfig::InCluster {
805 namespace: Some("test".to_string()),
806 additional_headers: None,
807 }),
808 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
809 account_id: "123456789012".to_string(),
810 region: "us-east-2".to_string(),
811 credentials: AwsCredentials::AccessKeys {
812 access_key_id: "access".to_string(),
813 secret_access_key: "secret".to_string(),
814 session_token: None,
815 },
816 service_overrides: None,
817 }))),
818 };
819
820 assert_eq!(config.platform(), crate::Platform::Kubernetes);
821 assert!(config.kubernetes_config().is_some());
822 assert_eq!(config.aws_config().unwrap().region, "us-east-2");
823 assert!(config.gcp_config().is_none());
824 assert!(config.azure_config().is_none());
825 }
826
827 #[test]
828 fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
829 let config = ClientConfig::KubernetesCloud {
830 kubernetes: Box::new(KubernetesClientConfig::InCluster {
831 namespace: Some("test".to_string()),
832 additional_headers: None,
833 }),
834 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
835 account_id: "123456789012".to_string(),
836 region: "us-east-2".to_string(),
837 credentials: AwsCredentials::AccessKeys {
838 access_key_id: "access".to_string(),
839 secret_access_key: "secret".to_string(),
840 session_token: None,
841 },
842 service_overrides: None,
843 }))),
844 };
845
846 let kubernetes_config = config
847 .config_for_platform(crate::Platform::Kubernetes)
848 .unwrap();
849
850 assert!(matches!(
851 kubernetes_config,
852 ClientConfig::KubernetesCloud { .. }
853 ));
854 assert!(kubernetes_config.kubernetes_config().is_some());
855 assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
856 }
857
858 #[test]
859 fn kubernetes_cloud_exposes_nested_gcp_config() {
860 let config = ClientConfig::KubernetesCloud {
861 kubernetes: Box::new(KubernetesClientConfig::InCluster {
862 namespace: Some("test".to_string()),
863 additional_headers: None,
864 }),
865 cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
866 project_id: "project".to_string(),
867 region: "us-central1".to_string(),
868 credentials: GcpCredentials::AccessToken {
869 token: "token".to_string(),
870 },
871 service_overrides: None,
872 project_number: None,
873 }))),
874 };
875
876 assert_eq!(config.gcp_config().unwrap().project_id, "project");
877 assert!(config.aws_config().is_none());
878 assert!(config.azure_config().is_none());
879 }
880
881 #[test]
882 fn kubernetes_cloud_exposes_nested_azure_config() {
883 let config = ClientConfig::KubernetesCloud {
884 kubernetes: Box::new(KubernetesClientConfig::InCluster {
885 namespace: Some("test".to_string()),
886 additional_headers: None,
887 }),
888 cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
889 subscription_id: "sub".to_string(),
890 tenant_id: "tenant".to_string(),
891 region: Some("eastus".to_string()),
892 credentials: AzureCredentials::AccessToken {
893 token: "token".to_string(),
894 },
895 service_overrides: None,
896 }))),
897 };
898
899 assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
900 assert!(config.aws_config().is_none());
901 assert!(config.gcp_config().is_none());
902 }
903
904 #[test]
905 fn scoped_azure_tokens_debug_redacts_every_token() {
906 let credentials = AzureCredentials::ScopedAccessTokens {
907 tokens: std::collections::HashMap::from([
908 (
909 "https://management.azure.com/.default".to_string(),
910 "management-secret".to_string(),
911 ),
912 (
913 "https://storage.azure.com/.default".to_string(),
914 "storage-secret".to_string(),
915 ),
916 ]),
917 };
918
919 let debug = format!("{credentials:?}");
920 assert!(debug.contains("https://management.azure.com/.default"));
921 assert!(debug.contains("https://storage.azure.com/.default"));
922 assert!(!debug.contains("management-secret"));
923 assert!(!debug.contains("storage-secret"));
924 assert!(debug.contains("[REDACTED]"));
925 }
926}