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 Profile {
89 name: String,
91 },
92 WebIdentity {
94 config: AwsWebIdentityConfig,
96 },
97}
98
99impl std::fmt::Debug for AwsCredentials {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 AwsCredentials::AccessKeys {
103 access_key_id,
104 session_token,
105 ..
106 } => f
107 .debug_struct("AwsCredentials::AccessKeys")
108 .field("access_key_id", access_key_id)
109 .field("secret_access_key", &"[REDACTED]")
110 .field(
111 "session_token",
112 &session_token.as_ref().map(|_| "[REDACTED]"),
113 )
114 .finish(),
115 AwsCredentials::SessionCredentials {
116 access_key_id,
117 expires_at,
118 ..
119 } => f
120 .debug_struct("AwsCredentials::SessionCredentials")
121 .field("access_key_id", access_key_id)
122 .field("secret_access_key", &"[REDACTED]")
123 .field("session_token", &"[REDACTED]")
124 .field("expires_at", expires_at)
125 .finish(),
126 AwsCredentials::Imds { endpoint } => f
127 .debug_struct("AwsCredentials::Imds")
128 .field("endpoint", endpoint)
129 .finish(),
130 AwsCredentials::Profile { name } => f
131 .debug_struct("AwsCredentials::Profile")
132 .field("name", name)
133 .finish(),
134 AwsCredentials::WebIdentity { config } => f
135 .debug_struct("AwsCredentials::WebIdentity")
136 .field("config", config)
137 .finish(),
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145#[serde(rename_all = "camelCase", deny_unknown_fields)]
146pub struct AwsClientConfig {
147 pub account_id: String,
149 pub region: String,
151 pub credentials: AwsCredentials,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub service_overrides: Option<AwsServiceOverrides>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct GcpServiceOverrides {
163 pub endpoints: HashMap<String, String>,
166}
167
168#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
171#[serde(rename_all = "camelCase", tag = "type")]
172pub enum GcpCredentials {
173 AccessToken { token: String },
175
176 ImpersonatedServiceAccount {
178 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
180 source: Box<GcpClientConfig>,
181 config: GcpImpersonationConfig,
183 },
184
185 ServiceAccountKey { json: String },
188
189 ServiceMetadata,
191
192 ProjectedServiceAccount {
194 token_file: String,
196 service_account_email: String,
198 },
199
200 ExternalAccount {
202 audience: String,
204 subject_token_type: String,
206 token_url: String,
208 credential_source_file: String,
210 service_account_impersonation_url: Option<String>,
212 },
213
214 AuthorizedUser {
217 client_id: String,
219 client_secret: String,
221 refresh_token: String,
223 },
224}
225
226impl std::fmt::Debug for GcpCredentials {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 match self {
229 GcpCredentials::AccessToken { .. } => f
230 .debug_struct("GcpCredentials::AccessToken")
231 .field("token", &"[REDACTED]")
232 .finish(),
233 GcpCredentials::ImpersonatedServiceAccount { source, config } => f
234 .debug_struct("GcpCredentials::ImpersonatedServiceAccount")
235 .field("source_project_id", &source.project_id)
236 .field("source_region", &source.region)
237 .field("service_account_email", &config.service_account_email)
238 .finish(),
239 GcpCredentials::ServiceAccountKey { .. } => f
240 .debug_struct("GcpCredentials::ServiceAccountKey")
241 .field("json", &"[REDACTED]")
242 .finish(),
243 GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
244 GcpCredentials::ProjectedServiceAccount {
245 token_file,
246 service_account_email,
247 } => f
248 .debug_struct("GcpCredentials::ProjectedServiceAccount")
249 .field("token_file", token_file)
250 .field("service_account_email", service_account_email)
251 .finish(),
252 GcpCredentials::ExternalAccount {
253 audience,
254 subject_token_type,
255 token_url,
256 credential_source_file,
257 service_account_impersonation_url,
258 } => f
259 .debug_struct("GcpCredentials::ExternalAccount")
260 .field("audience", audience)
261 .field("subject_token_type", subject_token_type)
262 .field("token_url", token_url)
263 .field("credential_source_file", credential_source_file)
264 .field(
265 "service_account_impersonation_url",
266 service_account_impersonation_url,
267 )
268 .finish(),
269 GcpCredentials::AuthorizedUser { client_id, .. } => f
270 .debug_struct("GcpCredentials::AuthorizedUser")
271 .field("client_id", client_id)
272 .field("client_secret", &"[REDACTED]")
273 .field("refresh_token", &"[REDACTED]")
274 .finish(),
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
282#[serde(rename_all = "camelCase", deny_unknown_fields)]
283pub struct GcpImpersonationConfig {
284 pub service_account_email: String,
286 pub scopes: Vec<String>,
288 pub delegates: Option<Vec<String>>,
290 pub lifetime: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
295 pub target_project_id: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
299 pub target_region: Option<String>,
300}
301
302impl Default for GcpImpersonationConfig {
303 fn default() -> Self {
304 Self {
305 service_account_email: String::new(),
306 scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
307 delegates: None,
308 lifetime: Some("3600s".to_string()),
309 target_project_id: None,
310 target_region: None,
311 }
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
318#[serde(rename_all = "camelCase", deny_unknown_fields)]
319pub struct GcpClientConfig {
320 pub project_id: String,
322 pub region: String,
324 pub credentials: GcpCredentials,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub service_overrides: Option<GcpServiceOverrides>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub project_number: Option<String>,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
337#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
338#[serde(rename_all = "camelCase", deny_unknown_fields)]
339pub struct AzureServiceOverrides {
340 pub endpoints: HashMap<String, String>,
343}
344
345#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
347#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
348#[serde(rename_all = "camelCase", tag = "type")]
349pub enum AzureCredentials {
350 ServicePrincipal {
352 client_id: String,
354 client_secret: String,
356 },
357 AccessToken {
359 token: String,
361 },
362 ScopedAccessTokens {
368 tokens: HashMap<String, String>,
372 },
373 VmManagedIdentity {
375 client_id: String,
377 identity_endpoint: Option<String>,
379 },
380 WorkloadIdentity {
382 client_id: String,
384 tenant_id: String,
386 federated_token_file: String,
388 authority_host: String,
390 },
391 ManagedIdentity {
394 client_id: String,
396 identity_endpoint: String,
398 identity_header: String,
400 },
401}
402
403impl std::fmt::Debug for AzureCredentials {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 match self {
406 AzureCredentials::ServicePrincipal { client_id, .. } => f
407 .debug_struct("AzureCredentials::ServicePrincipal")
408 .field("client_id", client_id)
409 .field("client_secret", &"[REDACTED]")
410 .finish(),
411 AzureCredentials::AccessToken { .. } => f
412 .debug_struct("AzureCredentials::AccessToken")
413 .field("token", &"[REDACTED]")
414 .finish(),
415 AzureCredentials::ScopedAccessTokens { tokens } => f
416 .debug_struct("AzureCredentials::ScopedAccessTokens")
417 .field("scopes", &tokens.keys().collect::<Vec<_>>())
418 .field("tokens", &"[REDACTED]")
419 .finish(),
420 AzureCredentials::VmManagedIdentity {
421 client_id,
422 identity_endpoint,
423 } => f
424 .debug_struct("AzureCredentials::VmManagedIdentity")
425 .field("client_id", client_id)
426 .field("identity_endpoint", identity_endpoint)
427 .finish(),
428 AzureCredentials::WorkloadIdentity {
429 client_id,
430 tenant_id,
431 federated_token_file,
432 authority_host,
433 } => f
434 .debug_struct("AzureCredentials::WorkloadIdentity")
435 .field("client_id", client_id)
436 .field("tenant_id", tenant_id)
437 .field("federated_token_file", federated_token_file)
438 .field("authority_host", authority_host)
439 .finish(),
440 AzureCredentials::ManagedIdentity {
441 client_id,
442 identity_endpoint,
443 ..
444 } => f
445 .debug_struct("AzureCredentials::ManagedIdentity")
446 .field("client_id", client_id)
447 .field("identity_endpoint", identity_endpoint)
448 .field("identity_header", &"[REDACTED]")
449 .finish(),
450 }
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
457#[serde(rename_all = "camelCase", deny_unknown_fields)]
458pub struct AzureImpersonationConfig {
459 pub client_id: String,
461 pub scope: String,
463 pub tenant_id: Option<String>,
465 #[serde(skip_serializing_if = "Option::is_none")]
468 pub target_subscription_id: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
472 pub target_region: Option<String>,
473}
474
475impl Default for AzureImpersonationConfig {
476 fn default() -> Self {
477 Self {
478 client_id: String::new(),
479 scope: "https://management.azure.com/.default".to_string(),
480 tenant_id: None,
481 target_subscription_id: None,
482 target_region: None,
483 }
484 }
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
490#[serde(rename_all = "camelCase", deny_unknown_fields)]
491pub struct AzureClientConfig {
492 pub subscription_id: String,
494 pub tenant_id: String,
496 pub region: Option<String>,
498 pub credentials: AzureCredentials,
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub service_overrides: Option<AzureServiceOverrides>,
503}
504
505#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
507#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
508#[serde(rename_all = "camelCase", tag = "mode")]
509pub enum KubernetesClientConfig {
510 InCluster {
512 #[serde(skip_serializing_if = "Option::is_none")]
514 namespace: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 additional_headers: Option<HashMap<String, String>>,
518 },
519 Kubeconfig {
521 #[serde(skip_serializing_if = "Option::is_none")]
523 kubeconfig_path: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 context: Option<String>,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 cluster: Option<String>,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 user: Option<String>,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 namespace: Option<String>,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 additional_headers: Option<HashMap<String, String>>,
539 },
540 Manual {
542 server_url: String,
544 certificate_authority_data: Option<String>,
546 insecure_skip_tls_verify: Option<bool>,
548 client_certificate_data: Option<String>,
550 client_key_data: Option<String>,
552 token: Option<String>,
554 username: Option<String>,
556 password: Option<String>,
558 namespace: Option<String>,
560 additional_headers: HashMap<String, String>,
562 },
563}
564
565impl std::fmt::Debug for KubernetesClientConfig {
566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 match self {
568 KubernetesClientConfig::InCluster {
569 namespace,
570 additional_headers,
571 } => f
572 .debug_struct("KubernetesClientConfig::InCluster")
573 .field("namespace", namespace)
574 .field("additional_headers", additional_headers)
575 .finish(),
576 KubernetesClientConfig::Kubeconfig {
577 kubeconfig_path,
578 context,
579 cluster,
580 user,
581 namespace,
582 additional_headers,
583 } => f
584 .debug_struct("KubernetesClientConfig::Kubeconfig")
585 .field("kubeconfig_path", kubeconfig_path)
586 .field("context", context)
587 .field("cluster", cluster)
588 .field("user", user)
589 .field("namespace", namespace)
590 .field("additional_headers", additional_headers)
591 .finish(),
592 KubernetesClientConfig::Manual {
593 server_url,
594 certificate_authority_data,
595 insecure_skip_tls_verify,
596 client_certificate_data,
597 client_key_data,
598 token,
599 username,
600 password,
601 namespace,
602 additional_headers,
603 } => f
604 .debug_struct("KubernetesClientConfig::Manual")
605 .field("server_url", server_url)
606 .field("certificate_authority_data", certificate_authority_data)
607 .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
608 .field("client_certificate_data", client_certificate_data)
609 .field(
610 "client_key_data",
611 &client_key_data.as_ref().map(|_| "[REDACTED]"),
612 )
613 .field("token", &token.as_ref().map(|_| "[REDACTED]"))
614 .field("username", username)
615 .field("password", &password.as_ref().map(|_| "[REDACTED]"))
616 .field("namespace", namespace)
617 .field("additional_headers", additional_headers)
618 .finish(),
619 }
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
626#[serde(rename_all = "camelCase", tag = "platform")]
627pub enum ImpersonationConfig {
628 Aws(AwsImpersonationConfig),
629 Gcp(GcpImpersonationConfig),
630 Azure(AzureImpersonationConfig),
631 }
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
637#[serde(rename_all = "camelCase", tag = "platform")]
638pub enum ClientConfig {
639 Aws(Box<AwsClientConfig>),
640 Gcp(Box<GcpClientConfig>),
641 Azure(Box<AzureClientConfig>),
642 Kubernetes(Box<KubernetesClientConfig>),
643 KubernetesCloud {
644 kubernetes: Box<KubernetesClientConfig>,
645 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
646 cloud: Box<ClientConfig>,
647 },
648 Local {
649 state_directory: String,
651 },
652 #[serde(skip)]
654 Machines,
655 #[serde(skip)]
657 Test,
658}
659
660impl ClientConfig {
661 pub fn platform(&self) -> Platform {
663 match self {
664 ClientConfig::Aws(_) => Platform::Aws,
665 ClientConfig::Gcp(_) => Platform::Gcp,
666 ClientConfig::Azure(_) => Platform::Azure,
667 ClientConfig::Kubernetes(_) => Platform::Kubernetes,
668 ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
669 ClientConfig::Local { .. } => Platform::Local,
670 ClientConfig::Machines => Platform::Machines,
671 ClientConfig::Test => Platform::Test,
672 }
673 }
674
675 pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
676 match self {
677 ClientConfig::KubernetesCloud { cloud, .. } => {
678 if platform == Platform::Kubernetes {
679 Some(self.clone())
680 } else if cloud.platform() == platform {
681 Some((**cloud).clone())
682 } else {
683 None
684 }
685 }
686 config if config.platform() == platform => Some(config.clone()),
687 _ => None,
688 }
689 }
690
691 pub fn aws_config(&self) -> Option<&AwsClientConfig> {
693 match self {
694 ClientConfig::Aws(config) => Some(config),
695 ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
696 _ => None,
697 }
698 }
699
700 pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
702 match self {
703 ClientConfig::Gcp(config) => Some(config),
704 ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
705 _ => None,
706 }
707 }
708
709 pub fn azure_config(&self) -> Option<&AzureClientConfig> {
711 match self {
712 ClientConfig::Azure(config) => Some(config),
713 ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
714 _ => None,
715 }
716 }
717
718 pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
720 match self {
721 ClientConfig::Kubernetes(config) => Some(config),
722 ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
723 _ => None,
724 }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::{
731 AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
732 GcpClientConfig, GcpCredentials, KubernetesClientConfig,
733 };
734
735 #[test]
736 fn aws_credentials_wire_format_matches_manager_api_contract() {
737 let cases = [
738 (
739 AwsCredentials::AccessKeys {
740 access_key_id: "access-key".to_string(),
741 secret_access_key: "secret-key".to_string(),
742 session_token: Some("session-token".to_string()),
743 },
744 serde_json::json!({
745 "type": "accessKeys",
746 "access_key_id": "access-key",
747 "secret_access_key": "secret-key",
748 "session_token": "session-token",
749 }),
750 ),
751 (
752 AwsCredentials::SessionCredentials {
753 access_key_id: "access-key".to_string(),
754 secret_access_key: "secret-key".to_string(),
755 session_token: "session-token".to_string(),
756 expires_at: "2099-01-01T00:00:00Z".to_string(),
757 },
758 serde_json::json!({
759 "type": "sessionCredentials",
760 "access_key_id": "access-key",
761 "secret_access_key": "secret-key",
762 "session_token": "session-token",
763 "expires_at": "2099-01-01T00:00:00Z",
764 }),
765 ),
766 ];
767
768 for (credentials, expected) in cases {
769 let serialized = serde_json::to_value(&credentials).unwrap();
770 assert_eq!(serialized, expected);
771 assert_eq!(
772 serde_json::from_value::<AwsCredentials>(serialized).unwrap(),
773 credentials
774 );
775 }
776 }
777
778 #[test]
779 fn kubernetes_cloud_exposes_nested_aws_config() {
780 let config = ClientConfig::KubernetesCloud {
781 kubernetes: Box::new(KubernetesClientConfig::InCluster {
782 namespace: Some("test".to_string()),
783 additional_headers: None,
784 }),
785 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
786 account_id: "123456789012".to_string(),
787 region: "us-east-2".to_string(),
788 credentials: AwsCredentials::AccessKeys {
789 access_key_id: "access".to_string(),
790 secret_access_key: "secret".to_string(),
791 session_token: None,
792 },
793 service_overrides: None,
794 }))),
795 };
796
797 assert_eq!(config.platform(), crate::Platform::Kubernetes);
798 assert!(config.kubernetes_config().is_some());
799 assert_eq!(config.aws_config().unwrap().region, "us-east-2");
800 assert!(config.gcp_config().is_none());
801 assert!(config.azure_config().is_none());
802 }
803
804 #[test]
805 fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
806 let config = ClientConfig::KubernetesCloud {
807 kubernetes: Box::new(KubernetesClientConfig::InCluster {
808 namespace: Some("test".to_string()),
809 additional_headers: None,
810 }),
811 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
812 account_id: "123456789012".to_string(),
813 region: "us-east-2".to_string(),
814 credentials: AwsCredentials::AccessKeys {
815 access_key_id: "access".to_string(),
816 secret_access_key: "secret".to_string(),
817 session_token: None,
818 },
819 service_overrides: None,
820 }))),
821 };
822
823 let kubernetes_config = config
824 .config_for_platform(crate::Platform::Kubernetes)
825 .unwrap();
826
827 assert!(matches!(
828 kubernetes_config,
829 ClientConfig::KubernetesCloud { .. }
830 ));
831 assert!(kubernetes_config.kubernetes_config().is_some());
832 assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
833 }
834
835 #[test]
836 fn kubernetes_cloud_exposes_nested_gcp_config() {
837 let config = ClientConfig::KubernetesCloud {
838 kubernetes: Box::new(KubernetesClientConfig::InCluster {
839 namespace: Some("test".to_string()),
840 additional_headers: None,
841 }),
842 cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
843 project_id: "project".to_string(),
844 region: "us-central1".to_string(),
845 credentials: GcpCredentials::AccessToken {
846 token: "token".to_string(),
847 },
848 service_overrides: None,
849 project_number: None,
850 }))),
851 };
852
853 assert_eq!(config.gcp_config().unwrap().project_id, "project");
854 assert!(config.aws_config().is_none());
855 assert!(config.azure_config().is_none());
856 }
857
858 #[test]
859 fn kubernetes_cloud_exposes_nested_azure_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::Azure(Box::new(AzureClientConfig {
866 subscription_id: "sub".to_string(),
867 tenant_id: "tenant".to_string(),
868 region: Some("eastus".to_string()),
869 credentials: AzureCredentials::AccessToken {
870 token: "token".to_string(),
871 },
872 service_overrides: None,
873 }))),
874 };
875
876 assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
877 assert!(config.aws_config().is_none());
878 assert!(config.gcp_config().is_none());
879 }
880
881 #[test]
882 fn scoped_azure_tokens_debug_redacts_every_token() {
883 let credentials = AzureCredentials::ScopedAccessTokens {
884 tokens: std::collections::HashMap::from([
885 (
886 "https://management.azure.com/.default".to_string(),
887 "management-secret".to_string(),
888 ),
889 (
890 "https://storage.azure.com/.default".to_string(),
891 "storage-secret".to_string(),
892 ),
893 ]),
894 };
895
896 let debug = format!("{credentials:?}");
897 assert!(debug.contains("https://management.azure.com/.default"));
898 assert!(debug.contains("https://storage.azure.com/.default"));
899 assert!(!debug.contains("management-secret"));
900 assert!(!debug.contains("storage-secret"));
901 assert!(debug.contains("[REDACTED]"));
902 }
903}