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