use crate::Platform;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AwsServiceOverrides {
pub endpoints: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AwsImpersonationConfig {
pub role_arn: String,
pub session_name: Option<String>,
pub duration_seconds: Option<i32>,
pub external_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_region: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AwsWebIdentityConfig {
pub role_arn: String,
pub session_name: Option<String>,
pub web_identity_token_file: String,
pub duration_seconds: Option<i32>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum AwsCredentials {
AccessKeys {
access_key_id: String,
secret_access_key: String,
session_token: Option<String>,
},
WebIdentity {
config: AwsWebIdentityConfig,
},
}
impl std::fmt::Debug for AwsCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AwsCredentials::AccessKeys {
access_key_id,
session_token,
..
} => f
.debug_struct("AwsCredentials::AccessKeys")
.field("access_key_id", access_key_id)
.field("secret_access_key", &"[REDACTED]")
.field(
"session_token",
&session_token.as_ref().map(|_| "[REDACTED]"),
)
.finish(),
AwsCredentials::WebIdentity { config } => f
.debug_struct("AwsCredentials::WebIdentity")
.field("config", config)
.finish(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AwsClientConfig {
pub account_id: String,
pub region: String,
pub credentials: AwsCredentials,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_overrides: Option<AwsServiceOverrides>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GcpServiceOverrides {
pub endpoints: HashMap<String, String>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum GcpCredentials {
AccessToken { token: String },
ServiceAccountKey { json: String },
ServiceMetadata,
ProjectedServiceAccount {
token_file: String,
service_account_email: String,
},
ExternalAccount {
audience: String,
subject_token_type: String,
token_url: String,
credential_source_file: String,
service_account_impersonation_url: Option<String>,
},
AuthorizedUser {
client_id: String,
client_secret: String,
refresh_token: String,
},
}
impl std::fmt::Debug for GcpCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GcpCredentials::AccessToken { .. } => f
.debug_struct("GcpCredentials::AccessToken")
.field("token", &"[REDACTED]")
.finish(),
GcpCredentials::ServiceAccountKey { .. } => f
.debug_struct("GcpCredentials::ServiceAccountKey")
.field("json", &"[REDACTED]")
.finish(),
GcpCredentials::ServiceMetadata => write!(f, "GcpCredentials::ServiceMetadata"),
GcpCredentials::ProjectedServiceAccount {
token_file,
service_account_email,
} => f
.debug_struct("GcpCredentials::ProjectedServiceAccount")
.field("token_file", token_file)
.field("service_account_email", service_account_email)
.finish(),
GcpCredentials::ExternalAccount {
audience,
subject_token_type,
token_url,
credential_source_file,
service_account_impersonation_url,
} => f
.debug_struct("GcpCredentials::ExternalAccount")
.field("audience", audience)
.field("subject_token_type", subject_token_type)
.field("token_url", token_url)
.field("credential_source_file", credential_source_file)
.field(
"service_account_impersonation_url",
service_account_impersonation_url,
)
.finish(),
GcpCredentials::AuthorizedUser { client_id, .. } => f
.debug_struct("GcpCredentials::AuthorizedUser")
.field("client_id", client_id)
.field("client_secret", &"[REDACTED]")
.field("refresh_token", &"[REDACTED]")
.finish(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GcpImpersonationConfig {
pub service_account_email: String,
pub scopes: Vec<String>,
pub delegates: Option<Vec<String>>,
pub lifetime: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_project_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_region: Option<String>,
}
impl Default for GcpImpersonationConfig {
fn default() -> Self {
Self {
service_account_email: String::new(),
scopes: vec!["https://www.googleapis.com/auth/cloud-platform".to_string()],
delegates: None,
lifetime: Some("3600s".to_string()),
target_project_id: None,
target_region: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GcpClientConfig {
pub project_id: String,
pub region: String,
pub credentials: GcpCredentials,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_overrides: Option<GcpServiceOverrides>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_number: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AzureServiceOverrides {
pub endpoints: HashMap<String, String>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum AzureCredentials {
ServicePrincipal {
client_id: String,
client_secret: String,
},
AccessToken {
token: String,
},
WorkloadIdentity {
client_id: String,
tenant_id: String,
federated_token_file: String,
authority_host: String,
},
ManagedIdentity {
client_id: String,
identity_endpoint: String,
identity_header: String,
},
}
impl std::fmt::Debug for AzureCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AzureCredentials::ServicePrincipal { client_id, .. } => f
.debug_struct("AzureCredentials::ServicePrincipal")
.field("client_id", client_id)
.field("client_secret", &"[REDACTED]")
.finish(),
AzureCredentials::AccessToken { .. } => f
.debug_struct("AzureCredentials::AccessToken")
.field("token", &"[REDACTED]")
.finish(),
AzureCredentials::WorkloadIdentity {
client_id,
tenant_id,
federated_token_file,
authority_host,
} => f
.debug_struct("AzureCredentials::WorkloadIdentity")
.field("client_id", client_id)
.field("tenant_id", tenant_id)
.field("federated_token_file", federated_token_file)
.field("authority_host", authority_host)
.finish(),
AzureCredentials::ManagedIdentity {
client_id,
identity_endpoint,
..
} => f
.debug_struct("AzureCredentials::ManagedIdentity")
.field("client_id", client_id)
.field("identity_endpoint", identity_endpoint)
.field("identity_header", &"[REDACTED]")
.finish(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AzureImpersonationConfig {
pub client_id: String,
pub scope: String,
pub tenant_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_subscription_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_region: Option<String>,
}
impl Default for AzureImpersonationConfig {
fn default() -> Self {
Self {
client_id: String::new(),
scope: "https://management.azure.com/.default".to_string(),
tenant_id: None,
target_subscription_id: None,
target_region: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AzureClientConfig {
pub subscription_id: String,
pub tenant_id: String,
pub region: Option<String>,
pub credentials: AzureCredentials,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_overrides: Option<AzureServiceOverrides>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "mode")]
pub enum KubernetesClientConfig {
InCluster {
#[serde(skip_serializing_if = "Option::is_none")]
namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
additional_headers: Option<HashMap<String, String>>,
},
Kubeconfig {
#[serde(skip_serializing_if = "Option::is_none")]
kubeconfig_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cluster: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
additional_headers: Option<HashMap<String, String>>,
},
Manual {
server_url: String,
certificate_authority_data: Option<String>,
insecure_skip_tls_verify: Option<bool>,
client_certificate_data: Option<String>,
client_key_data: Option<String>,
token: Option<String>,
username: Option<String>,
password: Option<String>,
namespace: Option<String>,
additional_headers: HashMap<String, String>,
},
}
impl std::fmt::Debug for KubernetesClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KubernetesClientConfig::InCluster {
namespace,
additional_headers,
} => f
.debug_struct("KubernetesClientConfig::InCluster")
.field("namespace", namespace)
.field("additional_headers", additional_headers)
.finish(),
KubernetesClientConfig::Kubeconfig {
kubeconfig_path,
context,
cluster,
user,
namespace,
additional_headers,
} => f
.debug_struct("KubernetesClientConfig::Kubeconfig")
.field("kubeconfig_path", kubeconfig_path)
.field("context", context)
.field("cluster", cluster)
.field("user", user)
.field("namespace", namespace)
.field("additional_headers", additional_headers)
.finish(),
KubernetesClientConfig::Manual {
server_url,
certificate_authority_data,
insecure_skip_tls_verify,
client_certificate_data,
client_key_data,
token,
username,
password,
namespace,
additional_headers,
} => f
.debug_struct("KubernetesClientConfig::Manual")
.field("server_url", server_url)
.field("certificate_authority_data", certificate_authority_data)
.field("insecure_skip_tls_verify", insecure_skip_tls_verify)
.field("client_certificate_data", client_certificate_data)
.field(
"client_key_data",
&client_key_data.as_ref().map(|_| "[REDACTED]"),
)
.field("token", &token.as_ref().map(|_| "[REDACTED]"))
.field("username", username)
.field("password", &password.as_ref().map(|_| "[REDACTED]"))
.field("namespace", namespace)
.field("additional_headers", additional_headers)
.finish(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "platform")]
pub enum ImpersonationConfig {
Aws(AwsImpersonationConfig),
Gcp(GcpImpersonationConfig),
Azure(AzureImpersonationConfig),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase", tag = "platform")]
pub enum ClientConfig {
Aws(Box<AwsClientConfig>),
Gcp(Box<GcpClientConfig>),
Azure(Box<AzureClientConfig>),
Kubernetes(Box<KubernetesClientConfig>),
KubernetesCloud {
kubernetes: Box<KubernetesClientConfig>,
#[cfg_attr(feature = "openapi", schema(value_type = Object))]
cloud: Box<ClientConfig>,
},
Local {
state_directory: String,
},
#[serde(skip)]
Test,
}
impl ClientConfig {
pub fn platform(&self) -> Platform {
match self {
ClientConfig::Aws(_) => Platform::Aws,
ClientConfig::Gcp(_) => Platform::Gcp,
ClientConfig::Azure(_) => Platform::Azure,
ClientConfig::Kubernetes(_) => Platform::Kubernetes,
ClientConfig::KubernetesCloud { .. } => Platform::Kubernetes,
ClientConfig::Local { .. } => Platform::Local,
ClientConfig::Test => Platform::Test,
}
}
pub fn config_for_platform(&self, platform: Platform) -> Option<ClientConfig> {
match self {
ClientConfig::KubernetesCloud { cloud, .. } => {
if platform == Platform::Kubernetes {
Some(self.clone())
} else if cloud.platform() == platform {
Some((**cloud).clone())
} else {
None
}
}
config if config.platform() == platform => Some(config.clone()),
_ => None,
}
}
pub fn aws_config(&self) -> Option<&AwsClientConfig> {
match self {
ClientConfig::Aws(config) => Some(config),
ClientConfig::KubernetesCloud { cloud, .. } => cloud.aws_config(),
_ => None,
}
}
pub fn gcp_config(&self) -> Option<&GcpClientConfig> {
match self {
ClientConfig::Gcp(config) => Some(config),
ClientConfig::KubernetesCloud { cloud, .. } => cloud.gcp_config(),
_ => None,
}
}
pub fn azure_config(&self) -> Option<&AzureClientConfig> {
match self {
ClientConfig::Azure(config) => Some(config),
ClientConfig::KubernetesCloud { cloud, .. } => cloud.azure_config(),
_ => None,
}
}
pub fn kubernetes_config(&self) -> Option<&KubernetesClientConfig> {
match self {
ClientConfig::Kubernetes(config) => Some(config),
ClientConfig::KubernetesCloud { kubernetes, .. } => Some(kubernetes),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{
AwsClientConfig, AwsCredentials, AzureClientConfig, AzureCredentials, ClientConfig,
GcpClientConfig, GcpCredentials, KubernetesClientConfig,
};
#[test]
fn kubernetes_cloud_exposes_nested_aws_config() {
let config = ClientConfig::KubernetesCloud {
kubernetes: Box::new(KubernetesClientConfig::InCluster {
namespace: Some("test".to_string()),
additional_headers: None,
}),
cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
account_id: "123456789012".to_string(),
region: "us-east-2".to_string(),
credentials: AwsCredentials::AccessKeys {
access_key_id: "access".to_string(),
secret_access_key: "secret".to_string(),
session_token: None,
},
service_overrides: None,
}))),
};
assert_eq!(config.platform(), crate::Platform::Kubernetes);
assert!(config.kubernetes_config().is_some());
assert_eq!(config.aws_config().unwrap().region, "us-east-2");
assert!(config.gcp_config().is_none());
assert!(config.azure_config().is_none());
}
#[test]
fn kubernetes_cloud_preserves_cloud_config_for_kubernetes_controllers() {
let config = ClientConfig::KubernetesCloud {
kubernetes: Box::new(KubernetesClientConfig::InCluster {
namespace: Some("test".to_string()),
additional_headers: None,
}),
cloud: Box::new(ClientConfig::Aws(Box::new(AwsClientConfig {
account_id: "123456789012".to_string(),
region: "us-east-2".to_string(),
credentials: AwsCredentials::AccessKeys {
access_key_id: "access".to_string(),
secret_access_key: "secret".to_string(),
session_token: None,
},
service_overrides: None,
}))),
};
let kubernetes_config = config
.config_for_platform(crate::Platform::Kubernetes)
.unwrap();
assert!(matches!(
kubernetes_config,
ClientConfig::KubernetesCloud { .. }
));
assert!(kubernetes_config.kubernetes_config().is_some());
assert_eq!(kubernetes_config.aws_config().unwrap().region, "us-east-2");
}
#[test]
fn kubernetes_cloud_exposes_nested_gcp_config() {
let config = ClientConfig::KubernetesCloud {
kubernetes: Box::new(KubernetesClientConfig::InCluster {
namespace: Some("test".to_string()),
additional_headers: None,
}),
cloud: Box::new(ClientConfig::Gcp(Box::new(GcpClientConfig {
project_id: "project".to_string(),
region: "us-central1".to_string(),
credentials: GcpCredentials::AccessToken {
token: "token".to_string(),
},
service_overrides: None,
project_number: None,
}))),
};
assert_eq!(config.gcp_config().unwrap().project_id, "project");
assert!(config.aws_config().is_none());
assert!(config.azure_config().is_none());
}
#[test]
fn kubernetes_cloud_exposes_nested_azure_config() {
let config = ClientConfig::KubernetesCloud {
kubernetes: Box::new(KubernetesClientConfig::InCluster {
namespace: Some("test".to_string()),
additional_headers: None,
}),
cloud: Box::new(ClientConfig::Azure(Box::new(AzureClientConfig {
subscription_id: "sub".to_string(),
tenant_id: "tenant".to_string(),
region: Some("eastus".to_string()),
credentials: AzureCredentials::AccessToken {
token: "token".to_string(),
},
service_overrides: None,
}))),
};
assert_eq!(config.azure_config().unwrap().subscription_id, "sub");
assert!(config.aws_config().is_none());
assert!(config.gcp_config().is_none());
}
}