alien_bindings/providers/artifact_registry/
local.rs1use crate::{
2 error::{ErrorData, Result},
3 traits::{
4 ArtifactRegistry, ArtifactRegistryCredentials, ArtifactRegistryPermissions, Binding,
5 CrossAccountAccess, CrossAccountPermissions, RegistryAuthMethod, RepositoryResponse,
6 },
7};
8use alien_core::bindings::ArtifactRegistryBinding;
9use alien_error::{AlienError, Context, ContextError, IntoAlienError, IntoAlienErrorDirect};
10use async_trait::async_trait;
11use oci_client::{
12 client::{Client as OciClient, ClientConfig as OciClientConfig, ClientProtocol},
13 errors::OciDistributionError,
14 secrets::RegistryAuth,
15 Reference,
16};
17use tracing::{debug, info};
18
19#[derive(Debug)]
29pub struct LocalArtifactRegistry {
30 binding_name: String,
31 registry_endpoint: String,
32}
33
34impl LocalArtifactRegistry {
35 pub async fn new(
41 binding_name: String,
42 binding: alien_core::bindings::ArtifactRegistryBinding,
43 ) -> Result<Self> {
44 let config = match binding {
46 ArtifactRegistryBinding::Local(config) => config,
47 _ => {
48 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
49 binding_name,
50 reason: "Expected Local artifact registry binding variant".to_string(),
51 }));
52 }
53 };
54
55 let registry_endpoint = config
56 .registry_url
57 .into_value(&binding_name, "registry_url")
58 .context(ErrorData::BindingConfigInvalid {
59 binding_name: binding_name.clone(),
60 reason: "Failed to extract registry_url from binding".to_string(),
61 })?;
62
63 if registry_endpoint.is_empty() {
65 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
66 binding_name: binding_name.clone(),
67 reason: "Registry endpoint cannot be empty".to_string(),
68 }));
69 }
70
71 info!(
72 binding_name = %binding_name,
73 endpoint = %registry_endpoint,
74 "Local artifact registry client configured"
75 );
76
77 Ok(Self {
78 binding_name,
79 registry_endpoint,
80 })
81 }
82
83 pub fn registry_endpoint(&self) -> &str {
85 &self.registry_endpoint
86 }
87
88 fn create_oci_client(&self) -> OciClient {
90 OciClient::new(OciClientConfig {
91 protocol: ClientProtocol::Http,
92 ..Default::default()
93 })
94 }
95
96 fn create_reference(&self, logical: &str) -> Result<Reference> {
99 let ref_string = format!(
105 "{}/{}/{}:latest",
106 self.registry_endpoint, self.binding_name, logical
107 );
108 Reference::try_from(ref_string.as_str())
109 .into_alien_error()
110 .context(ErrorData::Other {
111 message: format!("Invalid repository reference: {}", ref_string),
112 })
113 }
114
115 fn routable_name(&self, logical: &str) -> String {
119 if logical.is_empty() {
120 self.binding_name.clone()
121 } else {
122 format!("{}/{}", self.binding_name, logical)
123 }
124 }
125
126 fn logical_from_routable<'a>(&self, repo_id: &'a str) -> &'a str {
131 let prefix = format!("{}/", self.binding_name);
132 repo_id.strip_prefix(prefix.as_str()).unwrap_or(repo_id)
133 }
134}
135
136impl Binding for LocalArtifactRegistry {}
137
138#[async_trait]
139impl ArtifactRegistry for LocalArtifactRegistry {
140 fn registry_endpoint(&self) -> String {
141 let host = &self.registry_endpoint;
142 if host.starts_with("http://") || host.starts_with("https://") {
143 host.clone()
144 } else {
145 format!("http://{}", host)
146 }
147 }
148
149 fn upstream_repository_prefix(&self) -> String {
150 "artifacts/default".to_string()
155 }
156
157 async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse> {
158 info!(
159 binding_name = %self.binding_name,
160 repo_name = %repo_name,
161 "Creating local Docker repository"
162 );
163
164 let client = self.create_oci_client();
170 let reference = self.create_reference(repo_name)?;
171
172 use oci_client::manifest::{OciDescriptor, OciImageManifest, OciManifest};
174
175 let config_json = serde_json::json!({
177 "architecture": "amd64",
178 "os": "linux",
179 "rootfs": {
180 "type": "layers",
181 "diff_ids": []
182 },
183 "config": {}
184 });
185
186 let config_bytes = serde_json::to_vec(&config_json)
187 .into_alien_error()
188 .context(ErrorData::Other {
189 message: "Failed to serialize config".to_string(),
190 })?;
191
192 use sha2::{Digest as Sha2Digest, Sha256};
194 let config_digest = format!("sha256:{:x}", Sha256::digest(&config_bytes));
195
196 let config_descriptor = OciDescriptor {
198 media_type: "application/vnd.oci.image.config.v1+json".to_string(),
199 size: config_bytes.len() as i64,
200 digest: config_digest.clone(),
201 urls: None,
202 annotations: None,
203 };
204
205 let manifest = OciImageManifest {
207 schema_version: 2,
208 media_type: Some("application/vnd.oci.image.manifest.v1+json".to_string()),
209 config: config_descriptor,
210 layers: vec![], annotations: Some({
212 let mut map = std::collections::BTreeMap::new();
213 map.insert(
214 "dev.alien.marker".to_string(),
215 "empty-repository-created-by-alien".to_string(),
216 );
217 map
218 }),
219 subject: None,
220 artifact_type: None,
221 };
222
223 let auth = RegistryAuth::Anonymous;
226 client
227 .store_auth_if_needed(&self.registry_endpoint, &auth)
228 .await;
229
230 client
231 .push_blob(&reference, &config_bytes, &config_digest)
232 .await
233 .into_alien_error()
234 .context(ErrorData::Other {
235 message: format!("Failed to push config blob for repository '{}'", repo_name),
236 })?;
237
238 client
239 .push_manifest(&reference, &OciManifest::Image(manifest))
240 .await
241 .into_alien_error()
242 .context(ErrorData::Other {
243 message: format!(
244 "Failed to push marker manifest for repository '{}'",
245 repo_name
246 ),
247 })?;
248
249 let repository_uri = format!(
253 "{}/{}/{}",
254 self.registry_endpoint, self.binding_name, repo_name
255 );
256
257 info!(
258 binding_name = %self.binding_name,
259 repo_name = %repo_name,
260 uri = %repository_uri,
261 "Local Docker repository created successfully"
262 );
263
264 Ok(RepositoryResponse {
270 name: self.routable_name(repo_name),
271 uri: Some(repository_uri),
272 created_at: None,
273 })
274 }
275
276 async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse> {
277 debug!(
278 binding_name = %self.binding_name,
279 repo_id = %repo_id,
280 "Checking local repository existence via OCI API"
281 );
282
283 let logical = self.logical_from_routable(repo_id);
287
288 let client = self.create_oci_client();
290 let reference = self.create_reference(logical)?;
291
292 let auth = RegistryAuth::Anonymous;
294 client
295 .store_auth_if_needed(&self.registry_endpoint, &auth)
296 .await;
297
298 match client.pull_manifest(&reference, &auth).await {
301 Ok(_) => {
302 let repository_uri = format!(
305 "{}/{}/{}",
306 self.registry_endpoint, self.binding_name, logical
307 );
308
309 debug!(
310 binding_name = %self.binding_name,
311 repo_id = %repo_id,
312 repo_uri = %repository_uri,
313 "Local repository exists"
314 );
315
316 Ok(RepositoryResponse {
317 name: self.routable_name(logical),
318 uri: Some(repository_uri),
319 created_at: None,
320 })
321 }
322 Err(OciDistributionError::ServerError { code: 404, .. }) => {
323 debug!(
325 binding_name = %self.binding_name,
326 repo_id = %repo_id,
327 "Local repository not found (404)"
328 );
329
330 Err(AlienError::new(ErrorData::ResourceNotFound {
331 resource_id: repo_id.to_string(),
332 }))
333 }
334 Err(OciDistributionError::ImageManifestNotFoundError(_)) => {
335 debug!(
337 binding_name = %self.binding_name,
338 repo_id = %repo_id,
339 "Local repository not found (manifest not found)"
340 );
341
342 Err(AlienError::new(ErrorData::ResourceNotFound {
343 resource_id: repo_id.to_string(),
344 }))
345 }
346 Err(OciDistributionError::RegistryError { envelope, .. })
347 if envelope.errors.iter().any(|e| {
348 matches!(
349 e.code,
350 oci_client::errors::OciErrorCode::BlobUnknown
351 | oci_client::errors::OciErrorCode::ManifestUnknown
352 | oci_client::errors::OciErrorCode::NameUnknown
353 )
354 }) =>
355 {
356 debug!(
358 binding_name = %self.binding_name,
359 repo_id = %repo_id,
360 "Local repository not found (OCI error: blob/manifest/name unknown)"
361 );
362
363 Err(AlienError::new(ErrorData::ResourceNotFound {
364 resource_id: repo_id.to_string(),
365 }))
366 }
367 Err(e) => {
368 Err(e.into_alien_error().context(ErrorData::Other {
371 message: "Failed to check repository existence".to_string(),
372 }))
373 }
374 }
375 }
376
377 async fn add_cross_account_access(
378 &self,
379 repo_id: &str,
380 _access: CrossAccountAccess,
381 ) -> Result<()> {
382 info!(
383 binding_name = %self.binding_name,
384 repo_id = %repo_id,
385 "Local artifact registry cross-account access not supported"
386 );
387
388 Err(AlienError::new(ErrorData::OperationNotSupported {
389 operation: "add_cross_account_access".to_string(),
390 reason: "Local artifact registry does not support cross-account access".to_string(),
391 }))
392 }
393
394 async fn remove_cross_account_access(
395 &self,
396 repo_id: &str,
397 _access: CrossAccountAccess,
398 ) -> Result<()> {
399 info!(
400 binding_name = %self.binding_name,
401 repo_id = %repo_id,
402 "Local artifact registry cross-account access not supported"
403 );
404
405 Err(AlienError::new(ErrorData::OperationNotSupported {
406 operation: "remove_cross_account_access".to_string(),
407 reason: "Local artifact registry does not support cross-account access".to_string(),
408 }))
409 }
410
411 async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions> {
412 info!(
413 binding_name = %self.binding_name,
414 repo_id = %repo_id,
415 "Local artifact registry cross-account access not supported"
416 );
417
418 Err(AlienError::new(ErrorData::OperationNotSupported {
419 operation: "get_cross_account_access".to_string(),
420 reason: "Local artifact registry does not support cross-account access".to_string(),
421 }))
422 }
423
424 async fn generate_credentials(
425 &self,
426 repo_id: &str,
427 permissions: ArtifactRegistryPermissions,
428 ttl_seconds: Option<u32>,
429 ) -> Result<ArtifactRegistryCredentials> {
430 info!(
431 repo_id = %repo_id,
432 permissions = ?permissions,
433 ttl_seconds = ?ttl_seconds,
434 "Generating local artifact registry credentials"
435 );
436
437 Ok(ArtifactRegistryCredentials {
440 auth_method: RegistryAuthMethod::Basic,
441 username: String::new(),
442 password: String::new(),
443 expires_at: None,
444 })
445 }
446
447 async fn delete_repository(&self, repo_id: &str) -> Result<()> {
448 info!(
449 binding_name = %self.binding_name,
450 repo_id = %repo_id,
451 "Deleting local repository (stateless - no-op)"
452 );
453
454 info!(
457 binding_name = %self.binding_name,
458 repo_id = %repo_id,
459 "Local repository deletion acknowledged (no-op for stateless client)"
460 );
461
462 Ok(())
463 }
464}