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 WebIdentity {
73 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#[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 pub account_id: String,
109 pub region: String,
111 pub credentials: AwsCredentials,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub service_overrides: Option<AwsServiceOverrides>,
116}
117
118#[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 pub endpoints: HashMap<String, String>,
126}
127
128#[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 AccessToken { token: String },
135
136 ServiceAccountKey { json: String },
139
140 ServiceMetadata,
142
143 ProjectedServiceAccount {
145 token_file: String,
147 service_account_email: String,
149 },
150
151 ExternalAccount {
153 audience: String,
155 subject_token_type: String,
157 token_url: String,
159 credential_source_file: String,
161 service_account_impersonation_url: Option<String>,
163 },
164
165 AuthorizedUser {
168 client_id: String,
170 client_secret: String,
172 refresh_token: String,
174 },
175}
176
177impl std::fmt::Debug for GcpCredentials {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 GcpCredentials::AccessToken { .. } => f
181 .debug_struct("GcpCredentials::AccessToken")
182 .field("token", &"[REDACTED]")
183 .finish(),
184 GcpCredentials::ServiceAccountKey { .. } => f
185 .debug_struct("GcpCredentials::ServiceAccountKey")
186 .field("json", &"[REDACTED]")
187 .finish(),
188 GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
189 GcpCredentials::ProjectedServiceAccount {
190 token_file,
191 service_account_email,
192 } => f
193 .debug_struct("GcpCredentials::ProjectedServiceAccount")
194 .field("token_file", token_file)
195 .field("service_account_email", service_account_email)
196 .finish(),
197 GcpCredentials::ExternalAccount {
198 audience,
199 subject_token_type,
200 token_url,
201 credential_source_file,
202 service_account_impersonation_url,
203 } => f
204 .debug_struct("GcpCredentials::ExternalAccount")
205 .field("audience", audience)
206 .field("subject_token_type", subject_token_type)
207 .field("token_url", token_url)
208 .field("credential_source_file", credential_source_file)
209 .field(
210 "service_account_impersonation_url",
211 service_account_impersonation_url,
212 )
213 .finish(),
214 GcpCredentials::AuthorizedUser { client_id, .. } => f
215 .debug_struct("GcpCredentials::AuthorizedUser")
216 .field("client_id", client_id)
217 .field("client_secret", &"[REDACTED]")
218 .field("refresh_token", &"[REDACTED]")
219 .finish(),
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
227#[serde(rename_all = "camelCase", deny_unknown_fields)]
228pub struct GcpImpersonationConfig {
229 pub service_account_email: String,
231 pub scopes: Vec<String>,
233 pub delegates: Option<Vec<String>>,
235 pub lifetime: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
240 pub target_project_id: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
244 pub target_region: Option<String>,
245}
246
247impl Default for GcpImpersonationConfig {
248 fn default() -> Self {
249 Self {
250 service_account_email: String::new(),
251 scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
252 delegates: None,
253 lifetime: Some("3600s".to_string()),
254 target_project_id: None,
255 target_region: None,
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
263#[serde(rename_all = "camelCase", deny_unknown_fields)]
264pub struct GcpClientConfig {
265 pub project_id: String,
267 pub region: String,
269 pub credentials: GcpCredentials,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub service_overrides: Option<GcpServiceOverrides>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub project_number: Option<String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
283#[serde(rename_all = "camelCase", deny_unknown_fields)]
284pub struct AzureServiceOverrides {
285 pub endpoints: HashMap<String, String>,
288}
289
290#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
293#[serde(rename_all = "camelCase", tag = "type")]
294pub enum AzureCredentials {
295 ServicePrincipal {
297 client_id: String,
299 client_secret: String,
301 },
302 AccessToken {
304 token: String,
306 },
307 WorkloadIdentity {
309 client_id: String,
311 tenant_id: String,
313 federated_token_file: String,
315 authority_host: String,
317 },
318 ManagedIdentity {
321 client_id: String,
323 identity_endpoint: String,
325 identity_header: String,
327 },
328}
329
330impl std::fmt::Debug for AzureCredentials {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332 match self {
333 AzureCredentials::ServicePrincipal { client_id, .. } => f
334 .debug_struct("AzureCredentials::ServicePrincipal")
335 .field("client_id", client_id)
336 .field("client_secret", &"[REDACTED]")
337 .finish(),
338 AzureCredentials::AccessToken { .. } => f
339 .debug_struct("AzureCredentials::AccessToken")
340 .field("token", &"[REDACTED]")
341 .finish(),
342 AzureCredentials::WorkloadIdentity {
343 client_id,
344 tenant_id,
345 federated_token_file,
346 authority_host,
347 } => f
348 .debug_struct("AzureCredentials::WorkloadIdentity")
349 .field("client_id", client_id)
350 .field("tenant_id", tenant_id)
351 .field("federated_token_file", federated_token_file)
352 .field("authority_host", authority_host)
353 .finish(),
354 AzureCredentials::ManagedIdentity {
355 client_id,
356 identity_endpoint,
357 ..
358 } => f
359 .debug_struct("AzureCredentials::ManagedIdentity")
360 .field("client_id", client_id)
361 .field("identity_endpoint", identity_endpoint)
362 .field("identity_header", &"[REDACTED]")
363 .finish(),
364 }
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
371#[serde(rename_all = "camelCase", deny_unknown_fields)]
372pub struct AzureImpersonationConfig {
373 pub client_id: String,
375 pub scope: String,
377 pub tenant_id: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
382 pub target_subscription_id: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
386 pub target_region: Option<String>,
387}
388
389impl Default for AzureImpersonationConfig {
390 fn default() -> Self {
391 Self {
392 client_id: String::new(),
393 scope: "https://management.azure.com/.default".to_string(),
394 tenant_id: None,
395 target_subscription_id: None,
396 target_region: None,
397 }
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
404#[serde(rename_all = "camelCase", deny_unknown_fields)]
405pub struct AzureClientConfig {
406 pub subscription_id: String,
408 pub tenant_id: String,
410 pub region: Option<String>,
412 pub credentials: AzureCredentials,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub service_overrides: Option<AzureServiceOverrides>,
417}
418
419#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
421#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
422#[serde(rename_all = "camelCase", tag = "mode")]
423pub enum KubernetesClientConfig {
424 InCluster {
426 #[serde(skip_serializing_if = "Option::is_none")]
428 namespace: Option<String>,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 additional_headers: Option<HashMap<String, String>>,
432 },
433 Kubeconfig {
435 #[serde(skip_serializing_if = "Option::is_none")]
437 kubeconfig_path: Option<String>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 context: Option<String>,
441 #[serde(skip_serializing_if = "Option::is_none")]
443 cluster: Option<String>,
444 #[serde(skip_serializing_if = "Option::is_none")]
446 user: Option<String>,
447 #[serde(skip_serializing_if = "Option::is_none")]
449 namespace: Option<String>,
450 #[serde(skip_serializing_if = "Option::is_none")]
452 additional_headers: Option<HashMap<String, String>>,
453 },
454 Manual {
456 server_url: String,
458 certificate_authority_data: Option<String>,
460 insecure_skip_tls_verify: Option<bool>,
462 client_certificate_data: Option<String>,
464 client_key_data: Option<String>,
466 token: Option<String>,
468 username: Option<String>,
470 password: Option<String>,
472 namespace: Option<String>,
474 additional_headers: HashMap<String, String>,
476 },
477}
478
479impl std::fmt::Debug for KubernetesClientConfig {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 match self {
482 KubernetesClientConfig::InCluster {
483 namespace,
484 additional_headers,
485 } => f
486 .debug_struct("KubernetesClientConfig::InCluster")
487 .field("namespace", namespace)
488 .field("additional_headers", additional_headers)
489 .finish(),
490 KubernetesClientConfig::Kubeconfig {
491 kubeconfig_path,
492 context,
493 cluster,
494 user,
495 namespace,
496 additional_headers,
497 } => f
498 .debug_struct("KubernetesClientConfig::Kubeconfig")
499 .field("kubeconfig_path", kubeconfig_path)
500 .field("context", context)
501 .field("cluster", cluster)
502 .field("user", user)
503 .field("namespace", namespace)
504 .field("additional_headers", additional_headers)
505 .finish(),
506 KubernetesClientConfig::Manual {
507 server_url,
508 certificate_authority_data,
509 insecure_skip_tls_verify,
510 client_certificate_data,
511 client_key_data,
512 token,
513 username,
514 password,
515 namespace,
516 additional_headers,
517 } => f
518 .debug_struct("KubernetesClientConfig::Manual")
519 .field("server_url", server_url)
520 .field("certificate_authority_data", certificate_authority_data)
521 .field("insecure_skip_tls_verify", insecure_skip_tls_verify)
522 .field("client_certificate_data", client_certificate_data)
523 .field(
524 "client_key_data",
525 &client_key_data.as_ref().map(|_| "[REDACTED]"),
526 )
527 .field("token", &token.as_ref().map(|_| "[REDACTED]"))
528 .field("username", username)
529 .field("password", &password.as_ref().map(|_| "[REDACTED]"))
530 .field("namespace", namespace)
531 .field("additional_headers", additional_headers)
532 .finish(),
533 }
534 }
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
539#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
540#[serde(rename_all = "camelCase", tag = "platform")]
541pub enum ImpersonationConfig {
542 Aws(AwsImpersonationConfig),
543 Gcp(GcpImpersonationConfig),
544 Azure(AzureImpersonationConfig),
545 }
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
551#[serde(rename_all = "camelCase", tag = "platform")]
552pub enum ClientConfig {
553 Aws(Box<AwsClientConfig>),
554 Gcp(Box<GcpClientConfig>),
555 Azure(Box<AzureClientConfig>),
556 Kubernetes(Box<KubernetesClientConfig>),
557 KubernetesCloud {
558 kubernetes: Box<KubernetesClientConfig>,
559 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
560 cloud: Box<ClientConfig>,
561 },
562 Local {
563 state_directory: String,
565 },
566 #[serde(skip)]
568 Test,
569}
570
571impl ClientConfig {
572 pub fn platform(&self) -> Platform {
574 match self {
575 ClientConfig::Aws(_) => Platform::Aws,
576 ClientConfig::Gcp(_) => Platform::Gcp,
577 ClientConfig::Azure(_) => Platform::Azure,
578 ClientConfig::Kubernetes(_) => Platform::Kubernetes,
579 ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
580 ClientConfig::Local { .. } => Platform::Local,
581 ClientConfig::Test => Platform::Test,
582 }
583 }
584
585 pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
586 match self {
587 ClientConfig::KubernetesCloud { cloud, .. } => {
588 if platform == Platform::Kubernetes {
589 Some(self.clone())
590 } else if cloud.platform() == platform {
591 Some((**cloud).clone())
592 } else {
593 None
594 }
595 }
596 config if config.platform() == platform => Some(config.clone()),
597 _ => None,
598 }
599 }
600
601 pub fn aws_config(&self) -> Option<&AwsClientConfig> {
603 match self {
604 ClientConfig::Aws(config) => Some(config),
605 ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
606 _ => None,
607 }
608 }
609
610 pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
612 match self {
613 ClientConfig::Gcp(config) => Some(config),
614 ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
615 _ => None,
616 }
617 }
618
619 pub fn azure_config(&self) -> Option<&AzureClientConfig> {
621 match self {
622 ClientConfig::Azure(config) => Some(config),
623 ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
624 _ => None,
625 }
626 }
627
628 pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
630 match self {
631 ClientConfig::Kubernetes(config) => Some(config),
632 ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
633 _ => None,
634 }
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::{
641 AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
642 GcpClientConfig, GcpCredentials, KubernetesClientConfig,
643 };
644
645 #[test]
646 fn kubernetes_cloud_exposes_nested_aws_config() {
647 let config = ClientConfig::KubernetesCloud {
648 kubernetes: Box::new(KubernetesClientConfig::InCluster {
649 namespace: Some("test".to_string()),
650 additional_headers: None,
651 }),
652 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
653 account_id: "123456789012".to_string(),
654 region: "us-east-2".to_string(),
655 credentials: AwsCredentials::AccessKeys {
656 access_key_id: "access".to_string(),
657 secret_access_key: "secret".to_string(),
658 session_token: None,
659 },
660 service_overrides: None,
661 }))),
662 };
663
664 assert_eq!(config.platform(), crate::Platform::Kubernetes);
665 assert!(config.kubernetes_config().is_some());
666 assert_eq!(config.aws_config().unwrap().region, "us-east-2");
667 assert!(config.gcp_config().is_none());
668 assert!(config.azure_config().is_none());
669 }
670
671 #[test]
672 fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
673 let config = ClientConfig::KubernetesCloud {
674 kubernetes: Box::new(KubernetesClientConfig::InCluster {
675 namespace: Some("test".to_string()),
676 additional_headers: None,
677 }),
678 cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
679 account_id: "123456789012".to_string(),
680 region: "us-east-2".to_string(),
681 credentials: AwsCredentials::AccessKeys {
682 access_key_id: "access".to_string(),
683 secret_access_key: "secret".to_string(),
684 session_token: None,
685 },
686 service_overrides: None,
687 }))),
688 };
689
690 let kubernetes_config = config
691 .config_for_platform(crate::Platform::Kubernetes)
692 .unwrap();
693
694 assert!(matches!(
695 kubernetes_config,
696 ClientConfig::KubernetesCloud { .. }
697 ));
698 assert!(kubernetes_config.kubernetes_config().is_some());
699 assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
700 }
701
702 #[test]
703 fn kubernetes_cloud_exposes_nested_gcp_config() {
704 let config = ClientConfig::KubernetesCloud {
705 kubernetes: Box::new(KubernetesClientConfig::InCluster {
706 namespace: Some("test".to_string()),
707 additional_headers: None,
708 }),
709 cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
710 project_id: "project".to_string(),
711 region: "us-central1".to_string(),
712 credentials: GcpCredentials::AccessToken {
713 token: "token".to_string(),
714 },
715 service_overrides: None,
716 project_number: None,
717 }))),
718 };
719
720 assert_eq!(config.gcp_config().unwrap().project_id, "project");
721 assert!(config.aws_config().is_none());
722 assert!(config.azure_config().is_none());
723 }
724
725 #[test]
726 fn kubernetes_cloud_exposes_nested_azure_config() {
727 let config = ClientConfig::KubernetesCloud {
728 kubernetes: Box::new(KubernetesClientConfig::InCluster {
729 namespace: Some("test".to_string()),
730 additional_headers: None,
731 }),
732 cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
733 subscription_id: "sub".to_string(),
734 tenant_id: "tenant".to_string(),
735 region: Some("eastus".to_string()),
736 credentials: AzureCredentials::AccessToken {
737 token: "token".to_string(),
738 },
739 service_overrides: None,
740 }))),
741 };
742
743 assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
744 assert!(config.aws_config().is_none());
745 assert!(config.gcp_config().is_none());
746 }
747}