1use crate::config::AwsConfig;
2use anyhow::Result;
3
4#[derive(Clone, Debug)]
5pub struct EcrRepository {
6 pub name: String,
7 pub uri: String,
8 pub created_at: String,
9 pub tag_immutability: String,
10 pub encryption_type: String,
11}
12
13#[derive(Clone, Debug)]
14pub struct EcrImage {
15 pub tag: String,
16 pub artifact_type: String,
17 pub pushed_at: String,
18 pub size_bytes: i64,
19 pub uri: String,
20 pub digest: String,
21 pub last_pull_time: String,
22}
23
24pub struct EcrClient {
25 config: AwsConfig,
26}
27
28impl EcrClient {
29 pub fn new(config: AwsConfig) -> Self {
30 Self { config }
31 }
32
33 pub async fn list_private_repositories(&self) -> Result<Vec<EcrRepository>> {
34 let client = self.config.ecr_client().await;
35
36 let mut repositories = Vec::new();
37 let mut next_token: Option<String> = None;
38
39 loop {
40 let mut request = client.describe_repositories();
41 if let Some(token) = next_token {
42 request = request.next_token(token);
43 }
44
45 let response = request.send().await?;
46
47 if let Some(repos) = response.repositories {
48 for repo in repos {
49 repositories.push(EcrRepository {
50 name: repo.repository_name.unwrap_or_default(),
51 uri: repo.repository_uri.unwrap_or_default(),
52 created_at: repo
53 .created_at
54 .map(|dt| {
55 dt.fmt(aws_smithy_types::date_time::Format::DateTime)
56 .unwrap_or_default()
57 })
58 .unwrap_or_default(),
59 tag_immutability: repo
60 .image_tag_mutability
61 .map(|m| format!("{:?}", m))
62 .unwrap_or_default(),
63 encryption_type: repo
64 .encryption_configuration
65 .map(|e| {
66 let enc_type = format!("{:?}", e.encryption_type);
67 match enc_type.as_str() {
68 "Aes256" => "AES256".to_string(),
69 _ => enc_type,
70 }
71 })
72 .unwrap_or_default(),
73 });
74 }
75 }
76
77 next_token = response.next_token;
78 if next_token.is_none() {
79 break;
80 }
81 }
82
83 Ok(repositories)
84 }
85
86 pub async fn list_public_repositories(&self) -> Result<Vec<EcrRepository>> {
87 let client = self.config.ecr_public_client().await;
88
89 let mut repositories = Vec::new();
90 let mut next_token: Option<String> = None;
91
92 loop {
93 let mut request = client.describe_repositories();
94 if let Some(token) = next_token {
95 request = request.next_token(token);
96 }
97
98 let response = request.send().await?;
99
100 if let Some(repos) = response.repositories {
101 for repo in repos {
102 repositories.push(EcrRepository {
103 name: repo.repository_name.unwrap_or_default(),
104 uri: repo.repository_uri.unwrap_or_default(),
105 created_at: repo
106 .created_at
107 .map(|dt| {
108 dt.fmt(aws_smithy_types::date_time::Format::DateTime)
109 .unwrap_or_default()
110 })
111 .unwrap_or_default(),
112 tag_immutability: String::new(),
113 encryption_type: String::new(),
114 });
115 }
116 }
117
118 next_token = response.next_token;
119 if next_token.is_none() {
120 break;
121 }
122 }
123
124 Ok(repositories)
125 }
126
127 pub async fn list_images(
128 &self,
129 repository_name: &str,
130 repository_uri: &str,
131 ) -> Result<Vec<EcrImage>> {
132 let client = self.config.ecr_client().await;
133
134 let mut images = Vec::new();
135 let mut next_token: Option<String> = None;
136
137 loop {
138 let mut request = client.describe_images().repository_name(repository_name);
139 if let Some(token) = next_token {
140 request = request.next_token(token);
141 }
142
143 let response = request.send().await?;
144
145 if let Some(image_details) = response.image_details {
146 for image in image_details {
147 let tags = image.image_tags.unwrap_or_default();
148 let tag = tags
149 .first()
150 .cloned()
151 .unwrap_or_else(|| "<untagged>".to_string());
152
153 let size_bytes = image.image_size_in_bytes.unwrap_or(0);
154
155 let uri = format!("{}:{}", repository_uri, tag);
156
157 images.push(EcrImage {
158 tag,
159 artifact_type: image.artifact_media_type.unwrap_or_default(),
160 pushed_at: image
161 .image_pushed_at
162 .map(|dt| {
163 dt.fmt(aws_smithy_types::date_time::Format::DateTime)
164 .unwrap_or_default()
165 })
166 .unwrap_or_default(),
167 size_bytes,
168 uri,
169 digest: image.image_digest.unwrap_or_default(),
170 last_pull_time: image
171 .last_recorded_pull_time
172 .map(|dt| {
173 dt.fmt(aws_smithy_types::date_time::Format::DateTime)
174 .unwrap_or_default()
175 })
176 .unwrap_or_default(),
177 });
178 }
179 }
180
181 next_token = response.next_token;
182 if next_token.is_none() {
183 break;
184 }
185 }
186
187 Ok(images)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 #[test]
194 fn test_ecr_image_uri_format() {
195 let repository_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo";
196 let tag = "v1.0.0";
197 let expected_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo:v1.0.0";
198
199 let uri = format!("{}:{}", repository_uri, tag);
200 assert_eq!(uri, expected_uri);
201 }
202
203 #[test]
204 fn test_ecr_image_uri_with_untagged() {
205 let repository_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo";
206 let tag = "<untagged>";
207 let expected_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo:<untagged>";
208
209 let uri = format!("{}:{}", repository_uri, tag);
210 assert_eq!(uri, expected_uri);
211 }
212
213 #[test]
214 fn test_encryption_type_aes256_formatting() {
215 let enc_type = "Aes256";
216 let formatted = match enc_type {
217 "Aes256" => "AES256".to_string(),
218 _ => enc_type.to_string(),
219 };
220 assert_eq!(formatted, "AES256");
221 }
222
223 #[test]
224 fn test_encryption_type_kms_unchanged() {
225 let enc_type = "Kms";
226 let formatted = match enc_type {
227 "Aes256" => "AES256".to_string(),
228 _ => enc_type.to_string(),
229 };
230 assert_eq!(formatted, "Kms");
231 }
232}