use anyhow::{Context, Result};
use aws_config::BehaviorVersion;
use aws_sdk_ecr::Client as EcrClient;
use crate::server::registry::models::EcrConfig;
fn format_sdk_error<E: std::fmt::Debug>(err: &E) -> String {
let debug_str = format!("{:?}", err);
if let Some(start) = debug_str.find("message: Some(\"") {
let start = start + 15; if let Some(end) = debug_str[start..].find("\")") {
return debug_str[start..start + end].to_string();
}
}
if let Some(start) = debug_str.find("\"Message\":\"") {
let start = start + 11; if let Some(end) = debug_str[start..].find("\"") {
return debug_str[start..start + end].to_string();
}
}
if debug_str.len() > 200 {
format!("{}...", &debug_str[..200])
} else {
debug_str
}
}
pub struct EcrRepoManager {
config: EcrConfig,
ecr_client: EcrClient,
}
impl EcrRepoManager {
pub async fn new(config: EcrConfig) -> Result<Self> {
let aws_config = if let (Some(access_key), Some(secret_key)) =
(&config.access_key_id, &config.secret_access_key)
{
let creds =
aws_sdk_ecr::config::Credentials::new(access_key, secret_key, None, None, "static");
aws_config::defaults(BehaviorVersion::latest())
.credentials_provider(creds)
.region(aws_config::Region::new(config.region.clone()))
.load()
.await
} else {
aws_config::defaults(BehaviorVersion::latest())
.region(aws_config::Region::new(config.region.clone()))
.load()
.await
};
let ecr_client = EcrClient::new(&aws_config);
Ok(Self { config, ecr_client })
}
pub fn repo_name(&self, project: &str) -> String {
format!("{}{}", self.config.repo_prefix, project)
}
pub fn repo_arn(&self, project: &str) -> String {
format!(
"arn:aws:ecr:{}:{}:repository/{}",
self.config.region,
self.config.account_id,
self.repo_name(project)
)
}
pub async fn repository_exists(&self, project: &str) -> Result<bool> {
let repo_name = self.repo_name(project);
match self
.ecr_client
.describe_repositories()
.repository_names(&repo_name)
.send()
.await
{
Ok(response) => Ok(!response.repositories().is_empty()),
Err(err) => {
if let Some(service_err) = err.as_service_error() {
if service_err.is_repository_not_found_exception() {
return Ok(false);
}
}
Err(anyhow::anyhow!(
"Failed to check ECR repository existence for '{}': {}",
repo_name,
format_sdk_error(&err)
))
}
}
}
pub async fn create_repository(&self, project: &str) -> Result<bool> {
let repo_name = self.repo_name(project);
if self.repository_exists(project).await? {
tracing::debug!("ECR repository {} already exists", repo_name);
return Ok(false);
}
tracing::info!("Creating ECR repository: {}", repo_name);
let managed_tag = aws_sdk_ecr::types::Tag::builder()
.key("rise:managed")
.value("true")
.build()
.context("Failed to build managed tag")?;
let project_tag = aws_sdk_ecr::types::Tag::builder()
.key("rise:project")
.value(project)
.build()
.context("Failed to build project tag")?;
self.ecr_client
.create_repository()
.repository_name(&repo_name)
.tags(managed_tag)
.tags(project_tag)
.image_scanning_configuration(
aws_sdk_ecr::types::ImageScanningConfiguration::builder()
.scan_on_push(true)
.build(),
)
.send()
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to create ECR repository '{}': {}",
repo_name,
format_sdk_error(&e)
)
})?;
tracing::info!("Created ECR repository: {}", repo_name);
Ok(true)
}
pub async fn delete_repository(&self, project: &str) -> Result<bool> {
let repo_name = self.repo_name(project);
if !self.repository_exists(project).await? {
tracing::debug!(
"ECR repository {} does not exist, nothing to delete",
repo_name
);
return Ok(false);
}
tracing::info!("Deleting ECR repository: {}", repo_name);
self.ecr_client
.delete_repository()
.repository_name(&repo_name)
.force(true)
.send()
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to delete ECR repository '{}': {}",
repo_name,
format_sdk_error(&e)
)
})?;
tracing::info!("Deleted ECR repository: {}", repo_name);
Ok(true)
}
pub async fn tag_as_orphaned(&self, project: &str) -> Result<bool> {
if !self.repository_exists(project).await? {
tracing::debug!(
"ECR repository {} does not exist, nothing to tag",
self.repo_name(project)
);
return Ok(false);
}
let repo_arn = self.repo_arn(project);
tracing::info!(
"Tagging ECR repository as orphaned: {}",
self.repo_name(project)
);
let orphaned_tag = aws_sdk_ecr::types::Tag::builder()
.key("rise:orphaned")
.value("true")
.build()
.context("Failed to build orphaned tag")?;
self.ecr_client
.tag_resource()
.resource_arn(&repo_arn)
.tags(orphaned_tag)
.send()
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to tag ECR repository '{}' as orphaned: {}",
self.repo_name(project),
format_sdk_error(&e)
)
})?;
Ok(true)
}
pub fn auto_remove(&self) -> bool {
self.config.auto_remove
}
}