use crate::{
error::{map_cloud_client_error, ErrorData, Result},
traits::{
ArtifactRegistry, ArtifactRegistryCredentials, ArtifactRegistryPermissions, Binding,
CrossAccountAccess, CrossAccountPermissions, RegistryAuthMethod, RepositoryResponse,
},
};
use alien_azure_clients::{AzureClientConfig, AzureTokenCache};
use alien_core::bindings::ArtifactRegistryBinding;
use alien_error::{AlienError, Context, IntoAlienError};
use async_trait::async_trait;
use tracing::info;
#[derive(Debug)]
pub struct AcrArtifactRegistry {
registry_name: String,
registry_endpoint: String,
repository_prefix: String,
azure_token_cache: AzureTokenCache,
http_client: reqwest::Client,
}
impl AcrArtifactRegistry {
pub async fn new(
binding_name: String,
binding: ArtifactRegistryBinding,
azure_config: &AzureClientConfig,
) -> Result<Self> {
info!(
binding_name = %binding_name,
"Initializing Azure Container Registry"
);
let config = match binding {
ArtifactRegistryBinding::Acr(config) => config,
_ => {
return Err(AlienError::new(ErrorData::BindingConfigInvalid {
binding_name: binding_name.clone(),
reason: "Expected ACR binding, got different service type".to_string(),
}));
}
};
let registry_name = config
.registry_name
.into_value(&binding_name, "registry_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: binding_name.clone(),
reason: "Failed to extract registry_name from binding".to_string(),
})?;
config
.resource_group_name
.into_value(&binding_name, "resource_group_name")
.context(ErrorData::BindingConfigInvalid {
binding_name: binding_name.clone(),
reason: "Failed to extract resource_group_name from binding".to_string(),
})?;
let registry_endpoint = format!("{}.azurecr.io", registry_name);
let client = crate::http_client::create_http_client();
let azure_token_cache = AzureTokenCache::new(azure_config.clone());
let repository_prefix = match config.repository_prefix {
Some(bv) => bv
.into_value(&binding_name, "repository_prefix")
.unwrap_or_default(),
None => String::new(),
};
Ok(Self {
registry_name,
registry_endpoint,
repository_prefix,
azure_token_cache,
http_client: client,
})
}
}
impl Binding for AcrArtifactRegistry {}
#[async_trait]
impl ArtifactRegistry for AcrArtifactRegistry {
fn registry_endpoint(&self) -> String {
format!("https://{}", self.registry_endpoint)
}
fn upstream_repository_prefix(&self) -> String {
self.repository_prefix.clone()
}
async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse> {
let repository_uri = format!("{}/{}", self.registry_endpoint, repo_name);
Ok(RepositoryResponse {
name: repo_name.to_string(),
uri: Some(repository_uri),
created_at: None,
})
}
async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse> {
let repository_uri = format!("{}/{}", self.registry_endpoint, repo_id);
Ok(RepositoryResponse {
name: repo_id.to_string(),
uri: Some(repository_uri),
created_at: None,
})
}
async fn add_cross_account_access(
&self,
repo_id: &str,
_access: CrossAccountAccess,
) -> Result<()> {
let repo_name = repo_id;
info!(
repo_name = %repo_name,
registry_name = %self.registry_name,
"Azure Container Registry cross-account access not supported"
);
Err(AlienError::new(ErrorData::OperationNotSupported {
operation: "add_cross_account_access".to_string(),
reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
}))
}
async fn remove_cross_account_access(
&self,
repo_id: &str,
_access: CrossAccountAccess,
) -> Result<()> {
let repo_name = repo_id;
info!(
repo_name = %repo_name,
registry_name = %self.registry_name,
"Azure Container Registry cross-account access not supported"
);
Err(AlienError::new(ErrorData::OperationNotSupported {
operation: "remove_cross_account_access".to_string(),
reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
}))
}
async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions> {
let repo_name = repo_id;
info!(
repo_name = %repo_name,
registry_name = %self.registry_name,
"Azure Container Registry cross-account access not supported"
);
Err(AlienError::new(ErrorData::OperationNotSupported {
operation: "get_cross_account_access".to_string(),
reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
}))
}
async fn generate_credentials(
&self,
repo_id: &str,
permissions: ArtifactRegistryPermissions,
_ttl_seconds: Option<u32>,
) -> Result<ArtifactRegistryCredentials> {
info!(
registry = %self.registry_endpoint,
repo_id = %repo_id,
permissions = ?permissions,
"Generating ACR credentials via AAD → refresh → access token flow"
);
let aad_token = self
.azure_token_cache
.get_bearer_token_with_scope("https://management.azure.com/.default")
.await
.map_err(|e| {
map_cloud_client_error(e, "Failed to get AAD token for ACR".to_string(), None)
})?;
let exchange_url = format!("https://{}/oauth2/exchange", self.registry_endpoint);
let exchange_resp = self
.http_client
.post(&exchange_url)
.form(&[
("grant_type", "access_token"),
("service", &self.registry_endpoint),
("access_token", &aad_token),
])
.send()
.await
.into_alien_error()
.context(ErrorData::Other {
message: "ACR OAuth2 exchange request failed".to_string(),
})?;
if !exchange_resp.status().is_success() {
let status = exchange_resp.status();
let body = exchange_resp.text().await.unwrap_or_default();
return Err(AlienError::new(ErrorData::Other {
message: format!("ACR OAuth2 exchange failed with {}: {}", status, body),
}));
}
#[derive(serde::Deserialize)]
struct ExchangeResponse {
refresh_token: String,
}
let refresh_token = exchange_resp
.json::<ExchangeResponse>()
.await
.into_alien_error()
.context(ErrorData::Other {
message: "Failed to parse ACR exchange response".to_string(),
})?
.refresh_token;
let scope = if repo_id.is_empty() {
"registry:catalog:*".to_string()
} else {
let actions = match permissions {
ArtifactRegistryPermissions::Pull => "pull",
ArtifactRegistryPermissions::PushPull => "pull,push",
};
format!("repository:{}:{}", repo_id, actions)
};
let token_url = format!("https://{}/oauth2/token", self.registry_endpoint);
let token_resp = self
.http_client
.post(&token_url)
.form(&[
("grant_type", "refresh_token"),
("service", &self.registry_endpoint),
("scope", &scope),
("refresh_token", &refresh_token),
])
.send()
.await
.into_alien_error()
.context(ErrorData::Other {
message: "ACR OAuth2 token request failed".to_string(),
})?;
if !token_resp.status().is_success() {
let status = token_resp.status();
let body = token_resp.text().await.unwrap_or_default();
return Err(AlienError::new(ErrorData::Other {
message: format!("ACR OAuth2 token failed with {}: {}", status, body),
}));
}
#[derive(serde::Deserialize)]
struct TokenResponse {
access_token: String,
}
let access_token = token_resp
.json::<TokenResponse>()
.await
.into_alien_error()
.context(ErrorData::Other {
message: "Failed to parse ACR token response".to_string(),
})?
.access_token;
info!(
registry = %self.registry_endpoint,
scope = %scope,
"ACR access token generated"
);
let expires_at = Some((chrono::Utc::now() + chrono::Duration::seconds(300)).to_rfc3339());
Ok(ArtifactRegistryCredentials {
auth_method: RegistryAuthMethod::Bearer,
username: String::new(),
password: access_token,
expires_at,
})
}
async fn delete_repository(&self, _repo_id: &str) -> Result<()> {
Ok(())
}
}