Skip to main content

alien_bindings/providers/artifact_registry/
grpc.rs

1use crate::{
2    error::{Error, ErrorData},
3    grpc::artifact_registry_service::alien_bindings::artifact_registry::{
4        artifact_registry_service_client::ArtifactRegistryServiceClient,
5        AddCrossAccountAccessRequest, ArtifactRegistryPermissions as ProtoRegistryPermissions,
6        AwsCrossAccountAccess as ProtoAwsCrossAccountAccess,
7        ComputeServiceType as ProtoComputeServiceType, CreateRepositoryRequest,
8        CrossAccountAccess as ProtoCrossAccountAccess, DeleteRepositoryRequest,
9        GcpCrossAccountAccess as ProtoGcpCrossAccountAccess, GenerateCredentialsRequest,
10        GetCrossAccountAccessRequest, GetRepositoryRequest, RemoveCrossAccountAccessRequest,
11    },
12    grpc::status_conversion::status_to_alien_error,
13    traits::{
14        ArtifactRegistry, ArtifactRegistryCredentials, ArtifactRegistryPermissions,
15        AwsCrossAccountAccess, ComputeServiceType, CrossAccountAccess, CrossAccountPermissions,
16        GcpCrossAccountAccess, RepositoryResponse,
17    },
18    Binding,
19};
20
21use alien_error::AlienError;
22use async_trait::async_trait;
23use tonic::{transport::Channel, Request};
24
25/// gRPC implementation of the `ArtifactRegistry` trait.
26///
27/// This implementation communicates with an alien-runtime gRPC server
28/// to manage artifact registry operations.
29#[derive(Debug)]
30pub struct GrpcArtifactRegistry {
31    client: ArtifactRegistryServiceClient<Channel>,
32    binding_name: String,
33}
34
35impl GrpcArtifactRegistry {
36    /// Creates a new gRPC artifact registry instance from binding parameters.
37    pub async fn new(binding_name: String, grpc_address: String) -> Result<Self, Error> {
38        let channel = crate::providers::grpc_provider::create_grpc_channel(grpc_address).await?;
39        Self::new_from_channel(channel, binding_name).await
40    }
41
42    /// Creates a new gRPC artifact registry instance from a channel.
43    pub async fn new_from_channel(channel: Channel, binding_name: String) -> Result<Self, Error> {
44        let client = ArtifactRegistryServiceClient::new(channel);
45
46        Ok(Self {
47            client,
48            binding_name,
49        })
50    }
51
52    fn client(&self) -> ArtifactRegistryServiceClient<Channel> {
53        self.client.clone()
54    }
55
56    // Helper function to convert our types to proto types
57    fn convert_cross_account_access_to_proto(
58        access: &CrossAccountAccess,
59    ) -> ProtoCrossAccountAccess {
60        match access {
61            CrossAccountAccess::Aws(aws) => {
62                let proto_service_types: Vec<i32> = aws
63                    .allowed_service_types
64                    .iter()
65                    .map(|st| match st {
66                        ComputeServiceType::Worker => ProtoComputeServiceType::Worker as i32,
67                    })
68                    .collect();
69
70                ProtoCrossAccountAccess {
71                    access: Some(crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::cross_account_access::Access::Aws(
72                        ProtoAwsCrossAccountAccess {
73                            account_ids: aws.account_ids.clone(),
74                            allowed_service_types: proto_service_types,
75                            role_arns: aws.role_arns.clone(),
76                            regions: aws.regions.clone(),
77                        }
78                    )),
79                }
80            }
81            CrossAccountAccess::Gcp(gcp) => {
82                let proto_service_types: Vec<i32> = gcp
83                    .allowed_service_types
84                    .iter()
85                    .map(|st| match st {
86                        ComputeServiceType::Worker => ProtoComputeServiceType::Worker as i32,
87                    })
88                    .collect();
89
90                ProtoCrossAccountAccess {
91                    access: Some(crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::cross_account_access::Access::Gcp(
92                        ProtoGcpCrossAccountAccess {
93                            project_numbers: gcp.project_numbers.clone(),
94                            allowed_service_types: proto_service_types,
95                            service_account_emails: gcp.service_account_emails.clone(),
96                        }
97                    )),
98                }
99            }
100        }
101    }
102
103    // Helper function to convert proto types to our types
104    fn convert_proto_to_cross_account_permissions(
105        proto: crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::CrossAccountPermissions,
106    ) -> Result<CrossAccountPermissions, Error> {
107        let access_proto = proto.access.ok_or_else(|| {
108            AlienError::new(ErrorData::UnexpectedResponseFormat {
109                provider: "grpc".to_string(),
110                binding_name: "artifact_registry".to_string(),
111                field: "access".to_string(),
112                response_json: "missing access field".to_string(),
113            })
114        })?;
115
116        let access = match access_proto.access {
117            Some(crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::cross_account_access::Access::Aws(aws)) => {
118                let service_types: Vec<ComputeServiceType> = aws.allowed_service_types.iter()
119                    .filter_map(|&st| match ProtoComputeServiceType::try_from(st) {
120                        Ok(ProtoComputeServiceType::Worker) => Some(ComputeServiceType::Worker),
121                        _ => None,
122                    })
123                    .collect();
124
125                CrossAccountAccess::Aws(AwsCrossAccountAccess {
126                    account_ids: aws.account_ids,
127                    allowed_service_types: service_types,
128                    role_arns: aws.role_arns,
129                    regions: vec![],
130                })
131            }
132            Some(crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::cross_account_access::Access::Gcp(gcp)) => {
133                let service_types: Vec<ComputeServiceType> = gcp.allowed_service_types.iter()
134                    .filter_map(|&st| match ProtoComputeServiceType::try_from(st) {
135                        Ok(ProtoComputeServiceType::Worker) => Some(ComputeServiceType::Worker),
136                        _ => None,
137                    })
138                    .collect();
139
140                CrossAccountAccess::Gcp(GcpCrossAccountAccess {
141                    project_numbers: gcp.project_numbers,
142                    allowed_service_types: service_types,
143                    service_account_emails: gcp.service_account_emails,
144                })
145            }
146            None => {
147                return Err(AlienError::new(ErrorData::UnexpectedResponseFormat {
148                    provider: "grpc".to_string(),
149                    binding_name: "artifact_registry".to_string(),
150                    field: "access".to_string(),
151                    response_json: "missing access variant".to_string(),
152                }));
153            }
154        };
155
156        Ok(CrossAccountPermissions {
157            access,
158            last_updated: proto.last_updated,
159        })
160    }
161}
162
163impl Binding for GrpcArtifactRegistry {}
164
165#[async_trait]
166impl ArtifactRegistry for GrpcArtifactRegistry {
167    async fn create_repository(&self, repo_name: &str) -> Result<RepositoryResponse, Error> {
168        let mut client = self.client();
169
170        let request = CreateRepositoryRequest {
171            binding_name: self.binding_name.clone(),
172            repo_name: repo_name.to_string(),
173        };
174
175        let response = client
176            .create_repository(Request::new(request))
177            .await
178            .map_err(|e| status_to_alien_error(e, "create_repository"))?
179            .into_inner();
180
181        let result = response.result.ok_or_else(|| {
182            AlienError::new(ErrorData::UnexpectedResponseFormat {
183                provider: "grpc".to_string(),
184                binding_name: self.binding_name.clone(),
185                field: "result".to_string(),
186                response_json: "missing result field".to_string(),
187            })
188        })?;
189
190        Ok(RepositoryResponse {
191            name: result.name,
192            uri: result.uri,
193            created_at: result.created_at,
194        })
195    }
196
197    async fn get_repository(&self, repo_id: &str) -> Result<RepositoryResponse, Error> {
198        let mut client = self.client();
199
200        let request = GetRepositoryRequest {
201            binding_name: self.binding_name.clone(),
202            repo_id: repo_id.to_string(),
203        };
204
205        let response = client
206            .get_repository(Request::new(request))
207            .await
208            .map_err(|e| status_to_alien_error(e, "get_repository"))?
209            .into_inner();
210
211        let result = response.result.ok_or_else(|| {
212            AlienError::new(ErrorData::UnexpectedResponseFormat {
213                provider: "grpc".to_string(),
214                binding_name: self.binding_name.clone(),
215                field: "result".to_string(),
216                response_json: "missing result field".to_string(),
217            })
218        })?;
219
220        Ok(RepositoryResponse {
221            name: result.name,
222            uri: result.uri,
223            created_at: result.created_at,
224        })
225    }
226
227    async fn add_cross_account_access(
228        &self,
229        repo_id: &str,
230        access: CrossAccountAccess,
231    ) -> Result<(), Error> {
232        let mut client = self.client();
233
234        let proto_access = Self::convert_cross_account_access_to_proto(&access);
235
236        let request = AddCrossAccountAccessRequest {
237            binding_name: self.binding_name.clone(),
238            repo_id: repo_id.to_string(),
239            access: Some(proto_access),
240        };
241
242        client
243            .add_cross_account_access(Request::new(request))
244            .await
245            .map_err(|e| status_to_alien_error(e, "add_cross_account_access"))?;
246
247        Ok(())
248    }
249
250    async fn remove_cross_account_access(
251        &self,
252        repo_id: &str,
253        access: CrossAccountAccess,
254    ) -> Result<(), Error> {
255        let mut client = self.client();
256
257        let proto_access = Self::convert_cross_account_access_to_proto(&access);
258
259        let request = RemoveCrossAccountAccessRequest {
260            binding_name: self.binding_name.clone(),
261            repo_id: repo_id.to_string(),
262            access: Some(proto_access),
263        };
264
265        client
266            .remove_cross_account_access(Request::new(request))
267            .await
268            .map_err(|e| status_to_alien_error(e, "remove_cross_account_access"))?;
269
270        Ok(())
271    }
272
273    async fn get_cross_account_access(
274        &self,
275        repo_id: &str,
276    ) -> Result<CrossAccountPermissions, Error> {
277        let mut client = self.client();
278
279        let request = GetCrossAccountAccessRequest {
280            binding_name: self.binding_name.clone(),
281            repo_id: repo_id.to_string(),
282        };
283
284        let response = client
285            .get_cross_account_access(Request::new(request))
286            .await
287            .map_err(|e| status_to_alien_error(e, "get_cross_account_access"))?
288            .into_inner();
289
290        let permissions = response.permissions.ok_or_else(|| {
291            AlienError::new(ErrorData::UnexpectedResponseFormat {
292                provider: "grpc".to_string(),
293                binding_name: self.binding_name.clone(),
294                field: "permissions".to_string(),
295                response_json: "missing permissions field".to_string(),
296            })
297        })?;
298
299        Self::convert_proto_to_cross_account_permissions(permissions)
300    }
301
302    async fn generate_credentials(
303        &self,
304        repo_id: &str,
305        permissions: ArtifactRegistryPermissions,
306        ttl_seconds: Option<u32>,
307    ) -> Result<ArtifactRegistryCredentials, Error> {
308        let mut client = self.client();
309
310        let proto_permissions = match permissions {
311            ArtifactRegistryPermissions::Pull => ProtoRegistryPermissions::Pull,
312            ArtifactRegistryPermissions::PushPull => ProtoRegistryPermissions::PushPull,
313        };
314
315        let request = GenerateCredentialsRequest {
316            binding_name: self.binding_name.clone(),
317            repo_id: repo_id.to_string(),
318            permissions: proto_permissions.into(),
319            ttl_seconds,
320        };
321
322        let response = client
323            .generate_credentials(Request::new(request))
324            .await
325            .map_err(|e| status_to_alien_error(e, "generate_credentials"))?
326            .into_inner();
327
328        let credentials = response.credentials.ok_or_else(|| {
329            AlienError::new(ErrorData::UnexpectedResponseFormat {
330                provider: "grpc".to_string(),
331                binding_name: self.binding_name.clone(),
332                field: "credentials".to_string(),
333                response_json: "missing credentials field".to_string(),
334            })
335        })?;
336
337        use crate::grpc::artifact_registry_service::alien_bindings::artifact_registry::AuthMethod as ProtoAuthMethod;
338        Ok(ArtifactRegistryCredentials {
339            auth_method: match ProtoAuthMethod::try_from(credentials.auth_method) {
340                Ok(ProtoAuthMethod::Bearer) => crate::traits::RegistryAuthMethod::Bearer,
341                _ => crate::traits::RegistryAuthMethod::Basic,
342            },
343            username: credentials.username,
344            password: credentials.password,
345            expires_at: credentials.expires_at,
346        })
347    }
348
349    async fn delete_repository(&self, repo_id: &str) -> Result<(), Error> {
350        let mut client = self.client();
351
352        let request = DeleteRepositoryRequest {
353            binding_name: self.binding_name.clone(),
354            repo_id: repo_id.to_string(),
355        };
356
357        client
358            .delete_repository(Request::new(request))
359            .await
360            .map_err(|e| status_to_alien_error(e, "delete_repository"))?;
361
362        Ok(())
363    }
364}