Skip to main content

fakecloud_ecr/
state.rs

1use std::collections::{BTreeMap, HashMap};
2use std::sync::Arc;
3
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9pub type SharedEcrState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<EcrState>>>;
10
11impl fakecloud_core::multi_account::AccountState for EcrState {
12    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
13        Self::new(account_id, region)
14    }
15}
16
17pub const ECR_SNAPSHOT_SCHEMA_VERSION: u32 = 3;
18
19/// Top-level persisted ECR snapshot. The shape mirrors the convention
20/// used by other multi-account services (Kinesis, ElastiCache) so the
21/// `main.rs` loader can use the same branching pattern.
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct EcrSnapshot {
24    pub schema_version: u32,
25    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<EcrState>>,
26}
27
28#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29pub struct EcrState {
30    pub account_id: String,
31    pub region: String,
32    /// Repository name -> repository.
33    pub repositories: BTreeMap<String, Repository>,
34    /// Registry-level policy JSON document. `None` until the caller
35    /// sets one via `PutRegistryPolicy`.
36    pub registry_policy: Option<String>,
37    /// Registry-level scanning configuration. Defaults to `BASIC` per
38    /// AWS behaviour; tracked here so `Get/PutRegistryScanningConfiguration`
39    /// round-trips correctly.
40    pub registry_scanning_configuration: RegistryScanningConfiguration,
41    /// Registry-level replication configuration.
42    pub replication_configuration: Option<ReplicationConfiguration>,
43    /// Account setting flags keyed by setting name (e.g.,
44    /// `BASIC_SCAN_TYPE_VERSION`, `REGISTRY_POLICY_SCOPE`).
45    pub account_settings: HashMap<String, String>,
46    /// Layer upload state machine keyed by `uploadId`. Each entry is
47    /// tied to a specific repository.
48    #[serde(default)]
49    pub layer_uploads: BTreeMap<String, LayerUpload>,
50    /// Pull-time update exclusions keyed by IAM principal ARN. These
51    /// are registry-level per the Smithy model.
52    #[serde(default)]
53    pub pull_time_exclusions: BTreeMap<String, PullTimeExclusion>,
54    /// Pull-through cache rules keyed by `ecrRepositoryPrefix`.
55    #[serde(default)]
56    pub pull_through_cache_rules: BTreeMap<String, PullThroughCacheRule>,
57    /// Repository creation templates keyed by prefix.
58    #[serde(default)]
59    pub repository_creation_templates: BTreeMap<String, RepositoryCreationTemplate>,
60    /// Registry-wide signing configuration.
61    #[serde(default)]
62    pub signing_configuration: Option<SigningConfiguration>,
63}
64
65impl EcrState {
66    pub fn new(account_id: &str, region: &str) -> Self {
67        Self {
68            account_id: account_id.to_string(),
69            region: region.to_string(),
70            repositories: BTreeMap::new(),
71            registry_policy: None,
72            registry_scanning_configuration: RegistryScanningConfiguration::default(),
73            replication_configuration: None,
74            account_settings: HashMap::new(),
75            layer_uploads: BTreeMap::new(),
76            pull_time_exclusions: BTreeMap::new(),
77            pull_through_cache_rules: BTreeMap::new(),
78            repository_creation_templates: BTreeMap::new(),
79            signing_configuration: None,
80        }
81    }
82
83    pub fn reset(&mut self) {
84        self.repositories.clear();
85        self.registry_policy = None;
86        self.registry_scanning_configuration = RegistryScanningConfiguration::default();
87        self.replication_configuration = None;
88        self.account_settings.clear();
89        self.layer_uploads.clear();
90        self.pull_time_exclusions.clear();
91        self.pull_through_cache_rules.clear();
92        self.repository_creation_templates.clear();
93        self.signing_configuration = None;
94    }
95
96    pub fn repository_arn(&self, repository_name: &str) -> String {
97        format!(
98            "arn:aws:ecr:{}:{}:repository/{}",
99            self.region, self.account_id, repository_name
100        )
101    }
102
103    pub fn registry_id(&self) -> &str {
104        &self.account_id
105    }
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct Repository {
110    pub repository_name: String,
111    pub repository_arn: String,
112    pub registry_id: String,
113    pub repository_uri: String,
114    pub created_at: DateTime<Utc>,
115    pub image_tag_mutability: String,
116    pub image_scanning_configuration: ImageScanningConfiguration,
117    pub encryption_configuration: EncryptionConfiguration,
118    pub tags: BTreeMap<String, String>,
119    /// Repository-level policy document JSON. `None` until the caller
120    /// sets one via `SetRepositoryPolicy`.
121    pub policy: Option<String>,
122    /// Repository-level lifecycle policy document JSON.
123    pub lifecycle_policy: Option<String>,
124    /// Per-image scan findings, keyed by manifest digest.
125    #[serde(default)]
126    pub scan_findings: BTreeMap<String, ImageScanFindings>,
127    /// Stored images keyed by manifest digest (sha256). One image can
128    /// have many tags (via `image_tags`).
129    #[serde(default)]
130    pub images: BTreeMap<String, Image>,
131    /// Tag name -> image digest. Multiple tags can point to the same
132    /// digest.
133    #[serde(default)]
134    pub image_tags: BTreeMap<String, String>,
135    /// Content-addressed layer blobs keyed by their sha256 digest
136    /// (e.g. `sha256:deadbeef…`). Stored as base64 to keep JSON
137    /// snapshots portable.
138    #[serde(default)]
139    pub layers: BTreeMap<String, Layer>,
140}
141
142impl Repository {
143    pub fn new(
144        repository_name: &str,
145        repository_arn: String,
146        registry_id: &str,
147        endpoint: &str,
148    ) -> Self {
149        // Strip scheme from endpoint for repositoryUri (docker requires host only).
150        let host = endpoint
151            .trim_start_matches("http://")
152            .trim_start_matches("https://")
153            .trim_end_matches('/')
154            .to_string();
155        Self {
156            repository_name: repository_name.to_string(),
157            repository_arn,
158            registry_id: registry_id.to_string(),
159            repository_uri: format!("{host}/{repository_name}"),
160            created_at: Utc::now(),
161            image_tag_mutability: "MUTABLE".to_string(),
162            image_scanning_configuration: ImageScanningConfiguration::default(),
163            encryption_configuration: EncryptionConfiguration::default(),
164            tags: BTreeMap::new(),
165            policy: None,
166            lifecycle_policy: None,
167            scan_findings: BTreeMap::new(),
168            images: BTreeMap::new(),
169            image_tags: BTreeMap::new(),
170            layers: BTreeMap::new(),
171        }
172    }
173}
174
175#[derive(Clone, Debug, Serialize, Deserialize)]
176pub struct PullTimeExclusion {
177    pub principal_arn: String,
178    pub registered_at: DateTime<Utc>,
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize)]
182pub struct ImageScanFindings {
183    pub image_digest: String,
184    pub scan_status: String,
185    pub scan_completed_at: Option<DateTime<Utc>>,
186    pub vulnerability_source_updated_at: Option<DateTime<Utc>>,
187    pub finding_severity_counts: BTreeMap<String, i64>,
188    pub findings: Vec<Value>,
189}
190
191#[derive(Clone, Debug, Serialize, Deserialize)]
192pub struct PullThroughCacheRule {
193    pub ecr_repository_prefix: String,
194    pub upstream_registry_url: String,
195    pub upstream_registry: Option<String>,
196    pub credential_arn: Option<String>,
197    pub created_at: DateTime<Utc>,
198    pub updated_at: DateTime<Utc>,
199    pub custom_role_arn: Option<String>,
200}
201
202#[derive(Clone, Debug, Serialize, Deserialize)]
203pub struct RepositoryCreationTemplate {
204    pub prefix: String,
205    pub description: Option<String>,
206    pub image_tag_mutability: String,
207    pub applied_for: Vec<String>,
208    pub resource_tags: Vec<Value>,
209    pub created_at: DateTime<Utc>,
210    pub updated_at: DateTime<Utc>,
211    pub custom_role_arn: Option<String>,
212    pub repository_policy: Option<String>,
213    pub lifecycle_policy: Option<String>,
214    pub encryption_configuration: Option<EncryptionConfiguration>,
215}
216
217#[derive(Clone, Debug, Default, Serialize, Deserialize)]
218pub struct SigningConfiguration {
219    /// Raw rule payload from `PutSigningConfiguration`. Round-trippable
220    /// via `GetSigningConfiguration` even when a rule specifies a
221    /// key algorithm we can't verify against.
222    pub rules: Vec<Value>,
223    /// PEM-parsed public keys that `DescribeImageSigningStatus` will
224    /// use to verify companion cosign signatures. Populated lazily
225    /// from `rules` at `PutSigningConfiguration` time; unrecognised
226    /// rule shapes just leave this empty.
227    #[serde(default)]
228    pub trusted_keys: Vec<crate::signing::TrustedKey>,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize)]
232pub struct Image {
233    pub image_digest: String,
234    pub image_manifest: String,
235    pub image_manifest_media_type: String,
236    pub artifact_media_type: Option<String>,
237    pub image_size_in_bytes: u64,
238    pub image_pushed_at: DateTime<Utc>,
239    pub last_recorded_pull_time: Option<DateTime<Utc>>,
240}
241
242#[derive(Clone, Debug, Serialize, Deserialize)]
243pub struct Layer {
244    pub digest: String,
245    pub size: u64,
246    /// Base64-encoded blob bytes. When the owning repository has
247    /// `EncryptionConfiguration.encryption_type == "KMS"`, these bytes
248    /// are the envelope produced by `fakecloud_kms::api::encrypt_blob`;
249    /// `blob_get` decrypts on the way out. For AES256 (fakecloud's
250    /// default) the bytes are the plaintext blob.
251    pub blob_b64: String,
252    pub media_type: String,
253    /// ARN of the KMS key the blob was encrypted under, when stored
254    /// encrypted. `None` means the bytes in `blob_b64` are plaintext
255    /// — either because the repo used the default AES256 encryption
256    /// (fakecloud-internal, no-op) or the layer pre-dates the KMS
257    /// wire-up.
258    #[serde(default)]
259    pub encrypted_with_kms_key: Option<String>,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
263pub struct LayerUpload {
264    pub upload_id: String,
265    pub repository_name: String,
266    pub created_at: DateTime<Utc>,
267    /// Filesystem path to the in-progress upload's spool file. Each
268    /// `UploadLayerPart` (JSON control plane) and OCI blob `PATCH`
269    /// appends raw bytes to this file; the OCI `PUT` finish step
270    /// streams the final chunk in, computes SHA-256 over the file in
271    /// constant memory, and then promotes the bytes into a `Layer`.
272    /// Storing the path means a 1 GiB push never holds the partial
273    /// upload in RAM.
274    #[serde(default)]
275    pub spool_path: String,
276    pub last_byte_received: u64,
277}
278
279#[derive(Clone, Debug, Default, Serialize, Deserialize)]
280pub struct ImageScanningConfiguration {
281    /// Whether images are scanned automatically on push. Defaults to `false`.
282    pub scan_on_push: bool,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct EncryptionConfiguration {
287    /// `AES256` or `KMS`.
288    pub encryption_type: String,
289    /// KMS key ARN when `encryption_type == "KMS"`.
290    pub kms_key: Option<String>,
291}
292
293impl Default for EncryptionConfiguration {
294    fn default() -> Self {
295        Self {
296            encryption_type: "AES256".to_string(),
297            kms_key: None,
298        }
299    }
300}
301
302#[derive(Clone, Debug, Serialize, Deserialize)]
303pub struct RegistryScanningConfiguration {
304    /// `BASIC` or `ENHANCED`.
305    pub scan_type: String,
306    pub rules: Vec<RegistryScanningRule>,
307}
308
309impl Default for RegistryScanningConfiguration {
310    fn default() -> Self {
311        Self {
312            scan_type: "BASIC".to_string(),
313            rules: Vec::new(),
314        }
315    }
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
319pub struct RegistryScanningRule {
320    pub scan_frequency: String,
321    pub repository_filters: Vec<RepositoryFilter>,
322}
323
324#[derive(Clone, Debug, Serialize, Deserialize)]
325pub struct RepositoryFilter {
326    pub filter: String,
327    pub filter_type: String,
328}
329
330#[derive(Clone, Debug, Serialize, Deserialize)]
331pub struct ReplicationConfiguration {
332    pub rules: Vec<ReplicationRule>,
333}
334
335#[derive(Clone, Debug, Serialize, Deserialize)]
336pub struct ReplicationRule {
337    pub destinations: Vec<ReplicationDestination>,
338    pub repository_filters: Vec<RepositoryFilter>,
339}
340
341#[derive(Clone, Debug, Serialize, Deserialize)]
342pub struct ReplicationDestination {
343    pub region: String,
344    pub registry_id: String,
345}