use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const DEFAULT_PORT: u16 = 5439;
fn default_port() -> u16 {
DEFAULT_PORT
}
fn default_tls() -> bool {
true
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum RedshiftCredentials {
Password {
password: String,
},
Iam {
#[serde(default, skip_serializing_if = "Option::is_none")]
region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cluster_identifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
db_user: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workgroup_name: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
auto_create: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
db_groups: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
duration_secs: Option<u32>,
#[serde(default, skip_serializing_if = "RedshiftAwsCredentials::is_default")]
aws_credentials: RedshiftAwsCredentials,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-faucet-resource" = "url"))]
endpoint_url: Option<String>,
},
RedshiftDataApi {
#[serde(default, skip_serializing_if = "Option::is_none")]
region: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cluster_identifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workgroup_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
secret_arn: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
db_user: Option<String>,
#[serde(default, skip_serializing_if = "RedshiftAwsCredentials::is_default")]
aws_credentials: RedshiftAwsCredentials,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-faucet-resource" = "url"))]
endpoint_url: Option<String>,
},
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum RedshiftAwsCredentials {
#[default]
Default,
Profile {
name: String,
},
AccessKey {
access_key_id: String,
secret_access_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_token: Option<String>,
},
AssumeRole {
role_arn: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
external_id: Option<String>,
},
WebIdentity,
}
impl RedshiftAwsCredentials {
pub fn is_default(&self) -> bool {
matches!(self, Self::Default)
}
}
impl std::fmt::Debug for RedshiftAwsCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Default => write!(f, "Default"),
Self::Profile { name } => f.debug_struct("Profile").field("name", name).finish(),
Self::AccessKey { access_key_id, .. } => f
.debug_struct("AccessKey")
.field("access_key_id", access_key_id)
.field("secret_access_key", &"***")
.finish(),
Self::AssumeRole { role_arn, .. } => f
.debug_struct("AssumeRole")
.field("role_arn", role_arn)
.finish(),
Self::WebIdentity => write!(f, "WebIdentity"),
}
}
}
impl std::fmt::Debug for RedshiftCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Password { .. } => write!(f, "Password(***)"),
Self::Iam {
region,
cluster_identifier,
db_user,
workgroup_name,
aws_credentials,
..
} => f
.debug_struct("Iam")
.field("region", region)
.field("cluster_identifier", cluster_identifier)
.field("db_user", db_user)
.field("workgroup_name", workgroup_name)
.field("aws_credentials", aws_credentials)
.finish_non_exhaustive(),
Self::RedshiftDataApi {
region,
cluster_identifier,
workgroup_name,
secret_arn,
db_user,
aws_credentials,
..
} => f
.debug_struct("RedshiftDataApi")
.field("region", region)
.field("cluster_identifier", cluster_identifier)
.field("workgroup_name", workgroup_name)
.field("secret_arn", secret_arn)
.field("db_user", db_user)
.field("aws_credentials", aws_credentials)
.finish_non_exhaustive(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct RedshiftConnection {
#[serde(default)]
#[schemars(extend("x-faucet-resource" = "url"))]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
pub database: String,
#[serde(default)]
pub user: String,
pub credentials: RedshiftCredentials,
#[serde(default = "default_tls")]
pub tls: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tls_mode: Option<RedshiftTlsMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("x-faucet-resource" = "path"))]
pub ssl_root_cert: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RedshiftTlsMode {
Disable,
Prefer,
Require,
VerifyCa,
VerifyFull,
}
impl RedshiftConnection {
pub fn new(
host: impl Into<String>,
database: impl Into<String>,
user: impl Into<String>,
password: impl Into<String>,
) -> Self {
Self {
host: host.into(),
port: DEFAULT_PORT,
database: database.into(),
user: user.into(),
credentials: RedshiftCredentials::Password {
password: password.into(),
},
tls: true,
tls_mode: None,
ssl_root_cert: None,
}
}
pub fn uses_data_api(&self) -> bool {
matches!(
self.credentials,
RedshiftCredentials::RedshiftDataApi { .. }
)
}
pub fn endpoint_label(&self) -> String {
match &self.credentials {
RedshiftCredentials::RedshiftDataApi {
cluster_identifier,
workgroup_name,
..
} => match (cluster_identifier, workgroup_name) {
(Some(c), _) => format!("cluster:{c}"),
(None, Some(w)) => format!("workgroup:{w}"),
(None, None) => String::new(),
},
_ => self.host.clone(),
}
}
}
#[cfg(test)]
mod tests;