alien_bindings/providers/artifact_registry/
acr.rs1use crate::{
2 error::{map_cloud_client_error, ErrorData, Result},
3 traits::{
4 ArtifactRegistry, ArtifactRegistryCredentials, ArtifactRegistryPermissions, Binding,
5 CrossAccountAccess, CrossAccountPermissions, RegistryAuthMethod, RepositoryResponse,
6 },
7};
8use alien_azure_clients::{AzureClientConfig, AzureTokenCache};
9use alien_core::bindings::ArtifactRegistryBinding;
10use alien_error::{AlienError, Context, IntoAlienError};
11use async_trait::async_trait;
12use tracing::info;
13
14#[derive(Debug)]
16pub struct AcrArtifactRegistry {
17 registry_name: String,
18 registry_endpoint: String,
19 repository_prefix: String,
20 azure_token_cache: AzureTokenCache,
22 http_client: reqwest::Client,
23}
24
25impl AcrArtifactRegistry {
26 pub async fn new(
32 binding_name: String,
33 binding: ArtifactRegistryBinding,
34 azure_config: &AzureClientConfig,
35 ) -> Result<Self> {
36 info!(
37 binding_name = %binding_name,
38 "Initializing Azure Container Registry"
39 );
40
41 let config = match binding {
43 ArtifactRegistryBinding::Acr(config) => config,
44 _ => {
45 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
46 binding_name: binding_name.clone(),
47 reason: "Expected ACR binding, got different service type".to_string(),
48 }));
49 }
50 };
51
52 let registry_name = config
53 .registry_name
54 .into_value(&binding_name, "registry_name")
55 .context(ErrorData::BindingConfigInvalid {
56 binding_name: binding_name.clone(),
57 reason: "Failed to extract registry_name from binding".to_string(),
58 })?;
59
60 config
61 .resource_group_name
62 .into_value(&binding_name, "resource_group_name")
63 .context(ErrorData::BindingConfigInvalid {
64 binding_name: binding_name.clone(),
65 reason: "Failed to extract resource_group_name from binding".to_string(),
66 })?;
67
68 let registry_endpoint = format!("{}.azurecr.io", registry_name);
70 let client = crate::http_client::create_http_client();
71 let azure_token_cache = AzureTokenCache::new(azure_config.clone());
72
73 let repository_prefix = match config.repository_prefix {
74 Some(bv) => bv
75 .into_value(&binding_name, "repository_prefix")
76 .unwrap_or_default(),
77 None => String::new(),
78 };
79
80 Ok(Self {
81 registry_name,
82 registry_endpoint,
83 repository_prefix,
84 azure_token_cache,
85 http_client: client,
86 })
87 }
88}
89
90impl Binding for AcrArtifactRegistry {}
91
92#[async_trait]
93impl ArtifactRegistry for AcrArtifactRegistry {
94 fn registry_endpoint(&self) -> String {
95 format!("https://{}", self.registry_endpoint)
96 }
97
98 fn upstream_repository_prefix(&self) -> String {
99 self.repository_prefix.clone()
100 }
101
102 async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse> {
103 let repository_uri = format!("{}/{}", self.registry_endpoint, repo_name);
106
107 Ok(RepositoryResponse {
108 name: repo_name.to_string(),
109 uri: Some(repository_uri),
110 created_at: None,
111 })
112 }
113
114 async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse> {
115 let repository_uri = format!("{}/{}", self.registry_endpoint, repo_id);
117
118 Ok(RepositoryResponse {
119 name: repo_id.to_string(),
120 uri: Some(repository_uri),
121 created_at: None,
122 })
123 }
124
125 async fn add_cross_account_access(
126 &self,
127 repo_id: &str,
128 _access: CrossAccountAccess,
129 ) -> Result<()> {
130 let repo_name = repo_id;
131
132 info!(
133 repo_name = %repo_name,
134 registry_name = %self.registry_name,
135 "Azure Container Registry cross-account access not supported"
136 );
137
138 Err(AlienError::new(ErrorData::OperationNotSupported {
139 operation: "add_cross_account_access".to_string(),
140 reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
141 }))
142 }
143
144 async fn remove_cross_account_access(
145 &self,
146 repo_id: &str,
147 _access: CrossAccountAccess,
148 ) -> Result<()> {
149 let repo_name = repo_id;
150
151 info!(
152 repo_name = %repo_name,
153 registry_name = %self.registry_name,
154 "Azure Container Registry cross-account access not supported"
155 );
156
157 Err(AlienError::new(ErrorData::OperationNotSupported {
158 operation: "remove_cross_account_access".to_string(),
159 reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
160 }))
161 }
162
163 async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions> {
164 let repo_name = repo_id;
165
166 info!(
167 repo_name = %repo_name,
168 registry_name = %self.registry_name,
169 "Azure Container Registry cross-account access not supported"
170 );
171
172 Err(AlienError::new(ErrorData::OperationNotSupported {
173 operation: "get_cross_account_access".to_string(),
174 reason: "Azure Container Registry uses token-based access via generate_credentials - cross-account permissions are not supported".to_string(),
175 }))
176 }
177
178 async fn generate_credentials(
179 &self,
180 repo_id: &str,
181 permissions: ArtifactRegistryPermissions,
182 _ttl_seconds: Option<u32>,
183 ) -> Result<ArtifactRegistryCredentials> {
184 info!(
185 registry = %self.registry_endpoint,
186 repo_id = %repo_id,
187 permissions = ?permissions,
188 "Generating ACR credentials via AAD → refresh → access token flow"
189 );
190
191 let aad_token = self
193 .azure_token_cache
194 .get_bearer_token_with_scope("https://management.azure.com/.default")
195 .await
196 .map_err(|e| {
197 map_cloud_client_error(e, "Failed to get AAD token for ACR".to_string(), None)
198 })?;
199
200 let exchange_url = format!("https://{}/oauth2/exchange", self.registry_endpoint);
203 let exchange_resp = self
204 .http_client
205 .post(&exchange_url)
206 .form(&[
207 ("grant_type", "access_token"),
208 ("service", &self.registry_endpoint),
209 ("access_token", &aad_token),
210 ])
211 .send()
212 .await
213 .into_alien_error()
214 .context(ErrorData::Other {
215 message: "ACR OAuth2 exchange request failed".to_string(),
216 })?;
217
218 if !exchange_resp.status().is_success() {
219 let status = exchange_resp.status();
220 let body = exchange_resp.text().await.unwrap_or_default();
221 return Err(AlienError::new(ErrorData::Other {
222 message: format!("ACR OAuth2 exchange failed with {}: {}", status, body),
223 }));
224 }
225
226 #[derive(serde::Deserialize)]
227 struct ExchangeResponse {
228 refresh_token: String,
229 }
230 let refresh_token = exchange_resp
231 .json::<ExchangeResponse>()
232 .await
233 .into_alien_error()
234 .context(ErrorData::Other {
235 message: "Failed to parse ACR exchange response".to_string(),
236 })?
237 .refresh_token;
238
239 let scope = if repo_id.is_empty() {
243 "registry:catalog:*".to_string()
245 } else {
246 let actions = match permissions {
247 ArtifactRegistryPermissions::Pull => "pull",
248 ArtifactRegistryPermissions::PushPull => "pull,push",
249 };
250 format!("repository:{}:{}", repo_id, actions)
251 };
252
253 let token_url = format!("https://{}/oauth2/token", self.registry_endpoint);
254 let token_resp = self
255 .http_client
256 .post(&token_url)
257 .form(&[
258 ("grant_type", "refresh_token"),
259 ("service", &self.registry_endpoint),
260 ("scope", &scope),
261 ("refresh_token", &refresh_token),
262 ])
263 .send()
264 .await
265 .into_alien_error()
266 .context(ErrorData::Other {
267 message: "ACR OAuth2 token request failed".to_string(),
268 })?;
269
270 if !token_resp.status().is_success() {
271 let status = token_resp.status();
272 let body = token_resp.text().await.unwrap_or_default();
273 return Err(AlienError::new(ErrorData::Other {
274 message: format!("ACR OAuth2 token failed with {}: {}", status, body),
275 }));
276 }
277
278 #[derive(serde::Deserialize)]
279 struct TokenResponse {
280 access_token: String,
281 }
282 let access_token = token_resp
283 .json::<TokenResponse>()
284 .await
285 .into_alien_error()
286 .context(ErrorData::Other {
287 message: "Failed to parse ACR token response".to_string(),
288 })?
289 .access_token;
290
291 info!(
292 registry = %self.registry_endpoint,
293 scope = %scope,
294 "ACR access token generated"
295 );
296
297 let expires_at = Some((chrono::Utc::now() + chrono::Duration::seconds(300)).to_rfc3339());
299
300 Ok(ArtifactRegistryCredentials {
301 auth_method: RegistryAuthMethod::Bearer,
302 username: String::new(),
303 password: access_token,
304 expires_at,
305 })
306 }
307
308 async fn delete_repository(&self, _repo_id: &str) -> Result<()> {
313 Ok(())
315 }
316}