Skip to main content

fakecloud_ecr/
state.rs

1use std::collections::BTreeMap;
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 = 4;
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: BTreeMap<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: BTreeMap::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    /// Last time the lifecycle policy was evaluated against this
125    /// repository's images. `None` until the policy has been applied
126    /// at least once. Surfaced through `GetLifecyclePolicy`'s
127    /// `lastEvaluatedAt` field.
128    #[serde(default)]
129    pub lifecycle_policy_last_evaluated_at: Option<DateTime<Utc>>,
130    /// Per-image scan findings, keyed by manifest digest.
131    #[serde(default)]
132    pub scan_findings: BTreeMap<String, ImageScanFindings>,
133    /// Stored images keyed by manifest digest (sha256). One image can
134    /// have many tags (via `image_tags`).
135    #[serde(default)]
136    pub images: BTreeMap<String, Image>,
137    /// Tag name -> image digest. Multiple tags can point to the same
138    /// digest.
139    #[serde(default)]
140    pub image_tags: BTreeMap<String, String>,
141    /// Content-addressed layer blobs keyed by their sha256 digest
142    /// (e.g. `sha256:deadbeef…`). Stored as base64 to keep JSON
143    /// snapshots portable.
144    #[serde(default)]
145    pub layers: BTreeMap<String, Layer>,
146    /// Per-image replication status entries, keyed by image digest.
147    /// Populated as PutImage fans out to each registered destination.
148    #[serde(default)]
149    pub replication_statuses: BTreeMap<String, Vec<ImageReplicationStatus>>,
150}
151
152/// Outcome of replicating one image to one destination registry/region.
153/// Surfaced through `DescribeImageReplicationStatus`. AWS returns both
154/// `failureCode` and `failureReason`; we keep them as Options because
155/// they're only set when `status == "FAILED"`.
156#[derive(Clone, Debug, Serialize, Deserialize)]
157pub struct ImageReplicationStatus {
158    pub region: String,
159    pub registry_id: String,
160    pub status: String,
161    pub failure_code: Option<String>,
162    #[serde(default)]
163    pub failure_reason: Option<String>,
164}
165
166impl Repository {
167    pub fn new(
168        repository_name: &str,
169        repository_arn: String,
170        registry_id: &str,
171        endpoint: &str,
172    ) -> Self {
173        // Strip scheme from endpoint for repositoryUri (docker requires host only).
174        let host = endpoint
175            .trim_start_matches("http://")
176            .trim_start_matches("https://")
177            .trim_end_matches('/')
178            .to_string();
179        Self {
180            repository_name: repository_name.to_string(),
181            repository_arn,
182            registry_id: registry_id.to_string(),
183            repository_uri: format!("{host}/{repository_name}"),
184            created_at: Utc::now(),
185            image_tag_mutability: "MUTABLE".to_string(),
186            image_scanning_configuration: ImageScanningConfiguration::default(),
187            encryption_configuration: EncryptionConfiguration::default(),
188            tags: BTreeMap::new(),
189            policy: None,
190            lifecycle_policy: None,
191            lifecycle_policy_last_evaluated_at: None,
192            scan_findings: BTreeMap::new(),
193            images: BTreeMap::new(),
194            image_tags: BTreeMap::new(),
195            layers: BTreeMap::new(),
196            replication_statuses: BTreeMap::new(),
197        }
198    }
199}
200
201#[derive(Clone, Debug, Serialize, Deserialize)]
202pub struct PullTimeExclusion {
203    pub principal_arn: String,
204    pub registered_at: DateTime<Utc>,
205}
206
207#[derive(Clone, Debug, Serialize, Deserialize)]
208pub struct ImageScanFindings {
209    pub image_digest: String,
210    pub scan_status: String,
211    pub scan_completed_at: Option<DateTime<Utc>>,
212    pub vulnerability_source_updated_at: Option<DateTime<Utc>>,
213    pub finding_severity_counts: BTreeMap<String, i64>,
214    pub findings: Vec<Value>,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct PullThroughCacheRule {
219    pub ecr_repository_prefix: String,
220    pub upstream_registry_url: String,
221    pub upstream_registry: Option<String>,
222    pub credential_arn: Option<String>,
223    pub created_at: DateTime<Utc>,
224    pub updated_at: DateTime<Utc>,
225    pub custom_role_arn: Option<String>,
226}
227
228#[derive(Clone, Debug, Serialize, Deserialize)]
229pub struct RepositoryCreationTemplate {
230    pub prefix: String,
231    pub description: Option<String>,
232    pub image_tag_mutability: String,
233    pub applied_for: Vec<String>,
234    pub resource_tags: Vec<Value>,
235    pub created_at: DateTime<Utc>,
236    pub updated_at: DateTime<Utc>,
237    pub custom_role_arn: Option<String>,
238    pub repository_policy: Option<String>,
239    pub lifecycle_policy: Option<String>,
240    pub encryption_configuration: Option<EncryptionConfiguration>,
241}
242
243#[derive(Clone, Debug, Default, Serialize, Deserialize)]
244pub struct SigningConfiguration {
245    /// Raw rule payload from `PutSigningConfiguration`. Round-trippable
246    /// via `GetSigningConfiguration` even when a rule specifies a
247    /// key algorithm we can't verify against.
248    pub rules: Vec<Value>,
249    /// PEM-parsed public keys that `DescribeImageSigningStatus` will
250    /// use to verify companion cosign signatures. Populated lazily
251    /// from `rules` at `PutSigningConfiguration` time; unrecognised
252    /// rule shapes just leave this empty.
253    #[serde(default)]
254    pub trusted_keys: Vec<crate::signing::TrustedKey>,
255}
256
257#[derive(Clone, Debug, Serialize, Deserialize)]
258pub struct Image {
259    pub image_digest: String,
260    pub image_manifest: String,
261    pub image_manifest_media_type: String,
262    pub artifact_media_type: Option<String>,
263    pub image_size_in_bytes: u64,
264    pub image_pushed_at: DateTime<Utc>,
265    pub last_recorded_pull_time: Option<DateTime<Utc>>,
266    /// Lifecycle/storage state surfaced through `ImageDetail.imageStatus`
267    /// in `DescribeImages`. One of `ACTIVE`, `ARCHIVED`, `ACTIVATING`.
268    /// Defaults to `ACTIVE` (the only value AWS exposes for newly pushed
269    /// images); transitions are driven by `UpdateImageStorageClass`.
270    #[serde(default = "default_image_status")]
271    pub image_status: String,
272    /// Last time `UpdateImageStorageClass` archived this image. `None`
273    /// while the image has never been archived. Surfaced as
274    /// `ImageDetail.lastArchivedAt`.
275    #[serde(default)]
276    pub last_archived_at: Option<DateTime<Utc>>,
277    /// Last time `UpdateImageStorageClass` restored this image from
278    /// archive. `None` while the image has never been activated from
279    /// archive. Surfaced as `ImageDetail.lastActivatedAt`.
280    #[serde(default)]
281    pub last_activated_at: Option<DateTime<Utc>>,
282    /// Last time the image was read by a pull-shaped op (`BatchGetImage`,
283    /// `GetDownloadUrlForLayer`, OCI manifest GET, OCI blob GET). Updated
284    /// alongside `last_recorded_pull_time` so callers that rely on the
285    /// fakecloud-extension `lastInUseAt`/`inUseCount` pair can introspect
286    /// pull frequency in tests.
287    #[serde(default)]
288    pub last_in_use_at: Option<DateTime<Utc>>,
289    /// Monotonic counter of pull-shaped accesses. Bumped by the same
290    /// touch points that update `last_in_use_at`. Defaults to 0; not
291    /// part of the AWS Smithy model — fakecloud surfaces it as
292    /// `inUseCount` in `DescribeImages` for parity with the user
293    /// expectation.
294    #[serde(default)]
295    pub in_use_count: u64,
296}
297
298fn default_image_status() -> String {
299    "ACTIVE".to_string()
300}
301
302#[derive(Clone, Debug, Serialize, Deserialize)]
303pub struct Layer {
304    pub digest: String,
305    pub size: u64,
306    /// Base64-encoded blob bytes. When the owning repository has
307    /// `EncryptionConfiguration.encryption_type == "KMS"`, these bytes
308    /// are the envelope produced by `fakecloud_kms::api::encrypt_blob`;
309    /// `blob_get` decrypts on the way out. For AES256 (fakecloud's
310    /// default) the bytes are the plaintext blob.
311    pub blob_b64: String,
312    pub media_type: String,
313    /// ARN of the KMS key the blob was encrypted under, when stored
314    /// encrypted. `None` means the bytes in `blob_b64` are plaintext
315    /// — either because the repo used the default AES256 encryption
316    /// (fakecloud-internal, no-op) or the layer pre-dates the KMS
317    /// wire-up.
318    #[serde(default)]
319    pub encrypted_with_kms_key: Option<String>,
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize)]
323pub struct LayerUpload {
324    pub upload_id: String,
325    pub repository_name: String,
326    pub created_at: DateTime<Utc>,
327    /// Filesystem path to the in-progress upload's spool file. Each
328    /// `UploadLayerPart` (JSON control plane) and OCI blob `PATCH`
329    /// appends raw bytes to this file; the OCI `PUT` finish step
330    /// streams the final chunk in, computes SHA-256 over the file in
331    /// constant memory, and then promotes the bytes into a `Layer`.
332    /// Storing the path means a 1 GiB push never holds the partial
333    /// upload in RAM.
334    #[serde(default)]
335    pub spool_path: String,
336    pub last_byte_received: u64,
337}
338
339#[derive(Clone, Debug, Default, Serialize, Deserialize)]
340pub struct ImageScanningConfiguration {
341    /// Whether images are scanned automatically on push. Defaults to `false`.
342    pub scan_on_push: bool,
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize)]
346pub struct EncryptionConfiguration {
347    /// `AES256` or `KMS`.
348    pub encryption_type: String,
349    /// KMS key ARN when `encryption_type == "KMS"`.
350    pub kms_key: Option<String>,
351}
352
353impl Default for EncryptionConfiguration {
354    fn default() -> Self {
355        Self {
356            encryption_type: "AES256".to_string(),
357            kms_key: None,
358        }
359    }
360}
361
362#[derive(Clone, Debug, Serialize, Deserialize)]
363pub struct RegistryScanningConfiguration {
364    /// `BASIC` or `ENHANCED`.
365    pub scan_type: String,
366    pub rules: Vec<RegistryScanningRule>,
367}
368
369impl Default for RegistryScanningConfiguration {
370    fn default() -> Self {
371        Self {
372            scan_type: "BASIC".to_string(),
373            rules: Vec::new(),
374        }
375    }
376}
377
378#[derive(Clone, Debug, Serialize, Deserialize)]
379pub struct RegistryScanningRule {
380    pub scan_frequency: String,
381    pub repository_filters: Vec<RepositoryFilter>,
382}
383
384#[derive(Clone, Debug, Serialize, Deserialize)]
385pub struct RepositoryFilter {
386    pub filter: String,
387    pub filter_type: String,
388}
389
390#[derive(Clone, Debug, Serialize, Deserialize)]
391pub struct ReplicationConfiguration {
392    pub rules: Vec<ReplicationRule>,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize)]
396pub struct ReplicationRule {
397    pub destinations: Vec<ReplicationDestination>,
398    pub repository_filters: Vec<RepositoryFilter>,
399}
400
401#[derive(Clone, Debug, Serialize, Deserialize)]
402pub struct ReplicationDestination {
403    pub region: String,
404    pub registry_id: String,
405}