alien_bindings/providers/artifact_registry/
local.rs1use crate::{
2 error::{binding_env_var, 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 env_var: binding_env_var(&binding_name),
50 binding_name,
51 reason: "Expected Local artifact registry binding variant".to_string(),
52 }));
53 }
54 };
55
56 let registry_endpoint = config
57 .registry_url
58 .into_value(&binding_name, "registry_url")
59 .context(ErrorData::BindingConfigInvalid {
60 env_var: binding_env_var(&binding_name),
61 binding_name: binding_name.clone(),
62 reason: "Failed to extract registry_url from binding".to_string(),
63 })?;
64
65 if registry_endpoint.is_empty() {
67 return Err(AlienError::new(ErrorData::BindingConfigInvalid {
68 env_var: binding_env_var(&binding_name),
69 binding_name: binding_name.clone(),
70 reason: "Registry endpoint cannot be empty".to_string(),
71 }));
72 }
73
74 info!(
75 binding_name = %binding_name,
76 endpoint = %registry_endpoint,
77 "Local artifact registry client configured"
78 );
79
80 Ok(Self {
81 binding_name,
82 registry_endpoint,
83 })
84 }
85
86 pub fn registry_endpoint(&self) -> &str {
88 &self.registry_endpoint
89 }
90
91 fn create_oci_client(&self) -> OciClient {
93 OciClient::new(OciClientConfig {
94 protocol: ClientProtocol::Http,
95 ..Default::default()
96 })
97 }
98
99 fn create_reference(&self, logical: &str) -> Result<Reference> {
102 let ref_string = format!(
108 "{}/{}/{}:latest",
109 self.registry_endpoint, self.binding_name, logical
110 );
111 Reference::try_from(ref_string.as_str())
112 .into_alien_error()
113 .context(ErrorData::Other {
114 message: format!("Invalid repository reference: {}", ref_string),
115 })
116 }
117
118 fn routable_name(&self, logical: &str) -> String {
122 if logical.is_empty() {
123 self.binding_name.clone()
124 } else {
125 format!("{}/{}", self.binding_name, logical)
126 }
127 }
128
129 fn logical_from_routable<'a>(&self, repo_id: &'a str) -> &'a str {
134 let prefix = format!("{}/", self.binding_name);
135 repo_id.strip_prefix(prefix.as_str()).unwrap_or(repo_id)
136 }
137}
138
139impl Binding for LocalArtifactRegistry {}
140
141#[async_trait]
142impl ArtifactRegistry for LocalArtifactRegistry {
143 fn registry_endpoint(&self) -> String {
144 let host = &self.registry_endpoint;
145 if host.starts_with("http://") || host.starts_with("https://") {
146 host.clone()
147 } else {
148 format!("http://{}", host)
149 }
150 }
151
152 fn upstream_repository_prefix(&self) -> String {
153 "artifacts/default".to_string()
158 }
159
160 async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse> {
161 info!(
162 binding_name = %self.binding_name,
163 repo_name = %repo_name,
164 "Creating local Docker repository"
165 );
166
167 let client = self.create_oci_client();
173 let reference = self.create_reference(repo_name)?;
174
175 use oci_client::manifest::{OciDescriptor, OciImageManifest, OciManifest};
177
178 let config_json = serde_json::json!({
180 "architecture": "amd64",
181 "os": "linux",
182 "rootfs": {
183 "type": "layers",
184 "diff_ids": []
185 },
186 "config": {}
187 });
188
189 let config_bytes = serde_json::to_vec(&config_json)
190 .into_alien_error()
191 .context(ErrorData::Other {
192 message: "Failed to serialize config".to_string(),
193 })?;
194
195 use sha2::{Digest as Sha2Digest, Sha256};
197 let config_digest = format!("sha256:{:x}", Sha256::digest(&config_bytes));
198
199 let config_descriptor = OciDescriptor {
201 media_type: "application/vnd.oci.image.config.v1+json".to_string(),
202 size: config_bytes.len() as i64,
203 digest: config_digest.clone(),
204 urls: None,
205 annotations: None,
206 };
207
208 let manifest = OciImageManifest {
210 schema_version: 2,
211 media_type: Some("application/vnd.oci.image.manifest.v1+json".to_string()),
212 config: config_descriptor,
213 layers: vec![], annotations: Some({
215 let mut map = std::collections::BTreeMap::new();
216 map.insert(
217 "dev.alien.marker".to_string(),
218 "empty-repository-created-by-alien".to_string(),
219 );
220 map
221 }),
222 subject: None,
223 artifact_type: None,
224 };
225
226 let auth = RegistryAuth::Anonymous;
229 client
230 .store_auth_if_needed(&self.registry_endpoint, &auth)
231 .await;
232
233 client
234 .push_blob(&reference, &config_bytes, &config_digest)
235 .await
236 .into_alien_error()
237 .context(ErrorData::Other {
238 message: format!("Failed to push config blob for repository '{}'", repo_name),
239 })?;
240
241 client
242 .push_manifest(&reference, &OciManifest::Image(manifest))
243 .await
244 .into_alien_error()
245 .context(ErrorData::Other {
246 message: format!(
247 "Failed to push marker manifest for repository '{}'",
248 repo_name
249 ),
250 })?;
251
252 let repository_uri = format!(
256 "{}/{}/{}",
257 self.registry_endpoint, self.binding_name, repo_name
258 );
259
260 info!(
261 binding_name = %self.binding_name,
262 repo_name = %repo_name,
263 uri = %repository_uri,
264 "Local Docker repository created successfully"
265 );
266
267 Ok(RepositoryResponse {
273 name: self.routable_name(repo_name),
274 uri: Some(repository_uri),
275 created_at: None,
276 })
277 }
278
279 async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse> {
280 debug!(
281 binding_name = %self.binding_name,
282 repo_id = %repo_id,
283 "Checking local repository existence via OCI API"
284 );
285
286 let logical = self.logical_from_routable(repo_id);
290
291 let client = self.create_oci_client();
293 let reference = self.create_reference(logical)?;
294
295 let auth = RegistryAuth::Anonymous;
297 client
298 .store_auth_if_needed(&self.registry_endpoint, &auth)
299 .await;
300
301 match client.pull_manifest(&reference, &auth).await {
304 Ok(_) => {
305 let repository_uri = format!(
308 "{}/{}/{}",
309 self.registry_endpoint, self.binding_name, logical
310 );
311
312 debug!(
313 binding_name = %self.binding_name,
314 repo_id = %repo_id,
315 repo_uri = %repository_uri,
316 "Local repository exists"
317 );
318
319 Ok(RepositoryResponse {
320 name: self.routable_name(logical),
321 uri: Some(repository_uri),
322 created_at: None,
323 })
324 }
325 Err(OciDistributionError::ServerError { code: 404, .. }) => {
326 debug!(
328 binding_name = %self.binding_name,
329 repo_id = %repo_id,
330 "Local repository not found (404)"
331 );
332
333 Err(AlienError::new(ErrorData::ResourceNotFound {
334 resource_id: repo_id.to_string(),
335 }))
336 }
337 Err(OciDistributionError::ImageManifestNotFoundError(_)) => {
338 debug!(
340 binding_name = %self.binding_name,
341 repo_id = %repo_id,
342 "Local repository not found (manifest not found)"
343 );
344
345 Err(AlienError::new(ErrorData::ResourceNotFound {
346 resource_id: repo_id.to_string(),
347 }))
348 }
349 Err(OciDistributionError::RegistryError { envelope, .. })
350 if envelope.errors.iter().any(|e| {
351 matches!(
352 e.code,
353 oci_client::errors::OciErrorCode::BlobUnknown
354 | oci_client::errors::OciErrorCode::ManifestUnknown
355 | oci_client::errors::OciErrorCode::NameUnknown
356 )
357 }) =>
358 {
359 debug!(
361 binding_name = %self.binding_name,
362 repo_id = %repo_id,
363 "Local repository not found (OCI error: blob/manifest/name unknown)"
364 );
365
366 Err(AlienError::new(ErrorData::ResourceNotFound {
367 resource_id: repo_id.to_string(),
368 }))
369 }
370 Err(e) => {
371 Err(e.into_alien_error().context(ErrorData::Other {
374 message: "Failed to check repository existence".to_string(),
375 }))
376 }
377 }
378 }
379
380 async fn add_cross_account_access(
381 &self,
382 repo_id: &str,
383 _access: CrossAccountAccess,
384 ) -> Result<()> {
385 info!(
386 binding_name = %self.binding_name,
387 repo_id = %repo_id,
388 "Local artifact registry cross-account access not supported"
389 );
390
391 Err(AlienError::new(ErrorData::OperationNotSupported {
392 operation: "add_cross_account_access".to_string(),
393 reason: "Local artifact registry does not support cross-account access".to_string(),
394 }))
395 }
396
397 async fn remove_cross_account_access(
398 &self,
399 repo_id: &str,
400 _access: CrossAccountAccess,
401 ) -> Result<()> {
402 info!(
403 binding_name = %self.binding_name,
404 repo_id = %repo_id,
405 "Local artifact registry cross-account access not supported"
406 );
407
408 Err(AlienError::new(ErrorData::OperationNotSupported {
409 operation: "remove_cross_account_access".to_string(),
410 reason: "Local artifact registry does not support cross-account access".to_string(),
411 }))
412 }
413
414 async fn get_cross_account_access(&self, repo_id: &str) -> Result<CrossAccountPermissions> {
415 info!(
416 binding_name = %self.binding_name,
417 repo_id = %repo_id,
418 "Local artifact registry cross-account access not supported"
419 );
420
421 Err(AlienError::new(ErrorData::OperationNotSupported {
422 operation: "get_cross_account_access".to_string(),
423 reason: "Local artifact registry does not support cross-account access".to_string(),
424 }))
425 }
426
427 async fn generate_credentials(
428 &self,
429 repo_id: &str,
430 permissions: ArtifactRegistryPermissions,
431 ttl_seconds: Option<u32>,
432 ) -> Result<ArtifactRegistryCredentials> {
433 info!(
434 repo_id = %repo_id,
435 permissions = ?permissions,
436 ttl_seconds = ?ttl_seconds,
437 "Generating local artifact registry credentials"
438 );
439
440 Ok(ArtifactRegistryCredentials {
443 auth_method: RegistryAuthMethod::Basic,
444 username: String::new(),
445 password: String::new(),
446 expires_at: None,
447 })
448 }
449
450 async fn delete_repository(&self, repo_id: &str) -> Result<()> {
451 info!(
452 binding_name = %self.binding_name,
453 repo_id = %repo_id,
454 "Deleting local repository (stateless - no-op)"
455 );
456
457 info!(
460 binding_name = %self.binding_name,
461 repo_id = %repo_id,
462 "Local repository deletion acknowledged (no-op for stateless client)"
463 );
464
465 Ok(())
466 }
467}