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