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 AuthorizedUser {
154 client_id: String,
156 client_secret: String,
158 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#[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 pub service_account_email: String,
200 pub scopes: Vec<String>,
202 pub delegates: Option<Vec<String>>,
204 pub lifetime: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
209 pub target_project_id: Option<String>,
210 #[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#[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 pub project_id: String,
236 pub region: String,
238 pub credentials: GcpCredentials,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub service_overrides: Option<GcpServiceOverrides>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub project_number: Option<String>,
247}
248
249#[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 pub endpoints: HashMap<String, String>,
257}
258
259#[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 ServicePrincipal {
266 client_id: String,
268 client_secret: String,
270 },
271 AccessToken {
273 token: String,
275 },
276 WorkloadIdentity {
278 client_id: String,
280 tenant_id: String,
282 federated_token_file: String,
284 authority_host: String,
286 },
287 ManagedIdentity {
290 client_id: String,
292 identity_endpoint: String,
294 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#[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 pub client_id: String,
344 pub scope: String,
346 pub tenant_id: Option<String>,
348 #[serde(skip_serializing_if = "Option::is_none")]
351 pub target_subscription_id: Option<String>,
352 #[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#[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 pub subscription_id: String,
377 pub tenant_id: String,
379 pub region: Option<String>,
381 pub credentials: AzureCredentials,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub service_overrides: Option<AzureServiceOverrides>,
386}
387
388#[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 InCluster {
395 #[serde(skip_serializing_if = "Option::is_none")]
397 namespace: Option<String>,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 additional_headers: Option<HashMap<String, String>>,
401 },
402 Kubeconfig {
404 #[serde(skip_serializing_if = "Option::is_none")]
406 kubeconfig_path: Option<String>,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 context: Option<String>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 cluster: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 user: Option<String>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 namespace: Option<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 additional_headers: Option<HashMap<String, String>>,
422 },
423 Manual {
425 server_url: String,
427 certificate_authority_data: Option<String>,
429 insecure_skip_tls_verify: Option<bool>,
431 client_certificate_data: Option<String>,
433 client_key_data: Option<String>,
435 token: Option<String>,
437 username: Option<String>,
439 password: Option<String>,
441 namespace: Option<String>,
443 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#[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 }
516
517#[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: String,
534 },
535 #[serde(skip)]
537 Test,
538}
539
540impl ClientConfig {
541 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 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 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 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 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}