use anyhow::{anyhow, Context, Result};
use aws_config::meta::region::RegionProviderChain;
use aws_config::profile::ProfileFileCredentialsProvider;
use aws_config::retry::RetryConfig;
use aws_config::{BehaviorVersion, Region};
use aws_credential_types::provider::ProvideCredentials;
use aws_sdk_ssm::error::{ProvideErrorMetadata, SdkError};
use aws_sdk_ssm::Client;
use log::debug;
const CRED_HINT: &str = "\n\n💡 Configure AWS credentials with one of:\n \
• aws configure (static keys in ~/.aws/credentials)\n \
• aws configure sso && aws sso login (AWS SSO)\n \
• export AWS_PROFILE=<profile> (use a named profile)\n \
• export AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=… (environment variables)";
pub struct AwsClient {
ssm_client: Client,
}
impl AwsClient {
pub async fn new(region: Option<&str>, profile: Option<&str>) -> Result<Self> {
let mut config_loader = aws_config::defaults(BehaviorVersion::latest())
.retry_config(RetryConfig::adaptive().with_max_attempts(8));
if let Some(region) = region {
let region_provider = RegionProviderChain::first_try(Region::new(region.to_string()));
config_loader = config_loader.region(region_provider);
}
if let Some(profile) = profile {
let credentials_provider = ProfileFileCredentialsProvider::builder()
.profile_name(profile)
.build();
config_loader = config_loader.credentials_provider(credentials_provider);
}
let config = config_loader.load().await;
debug!("Initialized AWS config with region: {:?}", config.region());
let provider = config.credentials_provider().ok_or_else(|| {
anyhow!("No AWS credentials provider configured.{}", CRED_HINT)
})?;
provider.provide_credentials().await.map_err(|e| {
anyhow!("AWS credentials not found or invalid: {}{}", e, CRED_HINT)
})?;
let ssm_client = Client::new(&config);
Ok(AwsClient { ssm_client })
}
pub async fn get_parameter(&self, name: &str) -> Result<Option<String>> {
debug!("Getting parameter: {}", name);
let err = match self
.ssm_client
.get_parameter()
.name(name)
.with_decryption(true)
.send()
.await
{
Ok(result) => {
let value = result.parameter.and_then(|p| p.value);
if value.is_some() {
debug!("Successfully retrieved parameter: {}", name);
} else {
debug!("Parameter {} present but has no value", name);
}
return Ok(value);
}
Err(err) => err,
};
match err {
SdkError::DispatchFailure(failure) => {
let detail = if failure.is_io() {
format!("network I/O error: {:?}", failure.as_connector_error())
} else if failure.is_timeout() {
"request timed out before reaching AWS".to_string()
} else if failure.is_user() {
format!(
"client-side failure (likely expired credentials or signing): {:?}{}",
failure.as_connector_error(),
CRED_HINT
)
} else {
format!("dispatch failure: {:?}", failure.as_connector_error())
};
Err(anyhow!("AWS SSM dispatch error: {}", detail))
.with_context(|| format!("Failed to get parameter: {}", name))
}
SdkError::TimeoutError(_) => Err(anyhow!("AWS SSM request timed out"))
.with_context(|| format!("Failed to get parameter: {}", name)),
SdkError::ConstructionFailure(c) => {
Err(anyhow!("AWS SSM request construction failed: {:?}", c))
.with_context(|| format!("Failed to get parameter: {}", name))
}
SdkError::ResponseError(r) => Err(anyhow!("AWS SSM response error: {:?}", r))
.with_context(|| format!("Failed to get parameter: {}", name)),
err => {
let code = err.code().unwrap_or("Unknown").to_string();
let message = err.message().map(|m| m.to_string());
let service_err = err.into_service_error();
match service_err {
aws_sdk_ssm::operation::get_parameter::GetParameterError::ParameterNotFound(_) => {
debug!("Parameter not found: {}", name);
Ok(None)
}
_ => {
let detail = match &message {
Some(m) if !m.is_empty() => format!("{} - {}", code, m),
_ => format!("{} ({})", code, service_err),
};
Err(anyhow!("AWS SSM error: {}", detail))
.with_context(|| format!("Failed to get parameter: {}", name))
}
}
}
}
}
}