Skip to main content

fakecloud_lambda/
state.rs

1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LambdaFunction {
9    pub function_name: String,
10    pub function_arn: String,
11    pub runtime: String,
12    pub role: String,
13    pub handler: String,
14    pub description: String,
15    pub timeout: i64,
16    pub memory_size: i64,
17    pub code_sha256: String,
18    pub code_size: i64,
19    pub version: String,
20    pub last_modified: DateTime<Utc>,
21    pub tags: BTreeMap<String, String>,
22    pub environment: BTreeMap<String, String>,
23    pub architectures: Vec<String>,
24    pub package_type: String,
25    pub code_zip: Option<Vec<u8>>,
26    /// Container image URI for `PackageType=Image` functions. Points at a
27    /// private or public ECR image that the runtime pulls at invoke time.
28    /// `None` for `PackageType=Zip`.
29    #[serde(default)]
30    pub image_uri: Option<String>,
31    /// Resource-based policy attached to this function via
32    /// `AddPermission`, serialized as a full JSON policy document
33    /// (`{"Version":"2012-10-17","Statement":[...]}`). `None` means
34    /// the function has no resource policy attached, matching the
35    /// `ResourceNotFoundException` AWS returns from `GetPolicy` in
36    /// that state. `AddPermission` lazily initializes this; every
37    /// `RemovePermission` leaves at least `{"Statement":[]}` behind,
38    /// matching AWS behavior.
39    pub policy: Option<String>,
40    /// Layer versions attached to this function, in attach order. AWS
41    /// extracts each layer's content into `/opt` of the runtime sandbox at
42    /// invoke time; fakecloud's container runtime mirrors that via
43    /// `docker cp`. `code_size` is captured at attach time from the
44    /// referenced `LayerVersion` so `GetFunctionConfiguration` can echo it
45    /// without a second state lookup; layer versions are immutable so the
46    /// cached size never goes stale.
47    #[serde(default)]
48    pub layers: Vec<AttachedLayer>,
49    /// `RevisionId` is a stable token AWS expects to round-trip through
50    /// optimistic-concurrency calls (`UpdateFunctionConfiguration`,
51    /// `UpdateFunctionCode`, `AddPermission`, …). It only changes when
52    /// the function config changes; we used to mint a fresh UUID per
53    /// `function_config_json` call which broke client-side ETag-style
54    /// guards.
55    #[serde(default = "default_revision_id")]
56    pub revision_id: String,
57    /// `TracingConfig.Mode` — `PassThrough` (default) or `Active`.
58    #[serde(default)]
59    pub tracing_mode: Option<String>,
60    /// `KMSKeyArn` for env-var encryption (defaults to AWS-managed
61    /// `aws/lambda` when unset, which we represent as `None`).
62    #[serde(default)]
63    pub kms_key_arn: Option<String>,
64    /// `EphemeralStorage.Size` in MiB. AWS default is 512.
65    #[serde(default)]
66    pub ephemeral_storage_size: Option<i64>,
67    /// `VpcConfig` (`SubnetIds`, `SecurityGroupIds`, `Ipv6AllowedForDualStack`).
68    /// fakecloud doesn't network-isolate; we just round-trip the shape.
69    #[serde(default)]
70    pub vpc_config: Option<serde_json::Value>,
71    /// `SnapStart` (`ApplyOn`, `OptimizationStatus`).
72    #[serde(default)]
73    pub snap_start: Option<serde_json::Value>,
74    /// `DeadLetterConfig.TargetArn` for async-invoke failures.
75    #[serde(default)]
76    pub dead_letter_config_arn: Option<String>,
77    /// `FileSystemConfigs` (EFS access points). Round-tripped only.
78    #[serde(default)]
79    pub file_system_configs: Vec<serde_json::Value>,
80    /// `LoggingConfig` (LogFormat, ApplicationLogLevel, SystemLogLevel,
81    /// LogGroup).
82    #[serde(default)]
83    pub logging_config: Option<serde_json::Value>,
84    /// `ImageConfigResponse.ImageConfig` for container-package functions.
85    #[serde(default)]
86    pub image_config: Option<serde_json::Value>,
87    /// `SigningProfileVersionArn` populated by code signing.
88    #[serde(default)]
89    pub signing_profile_version_arn: Option<String>,
90    /// `SigningJobArn` populated by code signing.
91    #[serde(default)]
92    pub signing_job_arn: Option<String>,
93    /// `RuntimeVersionConfig` (`RuntimeVersionArn`).
94    #[serde(default)]
95    pub runtime_version_config: Option<serde_json::Value>,
96    /// `MasterArn` — only set on numbered versions; points at the parent
97    /// `$LATEST` ARN.
98    #[serde(default)]
99    pub master_arn: Option<String>,
100    /// Free-form `StateReason` populated when the function is not in the
101    /// happy `Active`/`Successful` path (e.g. image scan failed, KMS key
102    /// disabled, code-signing rejection). `None` for normal functions.
103    #[serde(default)]
104    pub state_reason: Option<String>,
105    /// Machine-readable `StateReasonCode` paired with `state_reason`
106    /// (`Idle`, `Creating`, `Restoring`, `EniLimitExceeded`, …).
107    #[serde(default)]
108    pub state_reason_code: Option<String>,
109    /// Free-form `LastUpdateStatusReason` set on the most recent failed
110    /// or in-progress configuration update.
111    #[serde(default)]
112    pub last_update_status_reason: Option<String>,
113    /// Machine-readable code paired with `last_update_status_reason`
114    /// (`EniLimitExceeded`, `InsufficientRolePermissions`, …).
115    #[serde(default)]
116    pub last_update_status_reason_code: Option<String>,
117}
118
119fn default_revision_id() -> String {
120    uuid::Uuid::new_v4().to_string()
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AttachedLayer {
125    pub arn: String,
126    #[serde(default)]
127    pub code_size: i64,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct EventSourceMapping {
132    pub uuid: String,
133    pub function_arn: String,
134    pub event_source_arn: String,
135    pub batch_size: i64,
136    pub enabled: bool,
137    pub state: String,
138    pub last_modified: DateTime<Utc>,
139    /// Raw `Filters: [{Pattern: "..."}]` array as supplied via
140    /// `FilterCriteria`. Each pattern is an EventBridge-style JSON
141    /// pattern matched against the record body — non-matching records
142    /// are dropped.
143    #[serde(default)]
144    pub filter_patterns: Vec<String>,
145    /// Wait up to N seconds to accumulate `batch_size` records before
146    /// invoking. Implemented as a deadline check inside the poller.
147    #[serde(default)]
148    pub maximum_batching_window_in_seconds: Option<i64>,
149    /// `LATEST`, `TRIM_HORIZON`, or `AT_TIMESTAMP`. Honored on the
150    /// first poll for stream sources (Kinesis, DDB Streams).
151    #[serde(default)]
152    pub starting_position: Option<String>,
153    /// Optional epoch-second timestamp paired with
154    /// `StartingPosition=AT_TIMESTAMP`.
155    #[serde(default)]
156    pub starting_position_timestamp: Option<f64>,
157    /// Kinesis-only: number of concurrent batch invocations per shard.
158    #[serde(default)]
159    pub parallelization_factor: Option<i64>,
160    /// `["ReportBatchItemFailures"]` to opt into partial-batch failure
161    /// semantics. Empty / unset = entire batch is retried on error.
162    #[serde(default)]
163    pub function_response_types: Vec<String>,
164    /// KMS key for encrypting the filter-criteria document at rest. AWS
165    /// added this in 2024 for Kafka/Kinesis sources whose filters carry
166    /// sensitive selectors.
167    #[serde(default)]
168    pub kms_key_arn: Option<String>,
169    /// `MetricsConfig.Metrics` — set of opted-in CloudWatch metrics
170    /// (`["EventCount"]`). Round-tripped only; fakecloud doesn't yet
171    /// publish these metrics.
172    #[serde(default)]
173    pub metrics_config: Option<serde_json::Value>,
174    /// `DestinationConfig` — `OnFailure.Destination` arn (and rarely
175    /// `OnSuccess.Destination` for self-managed Kafka). Round-tripped.
176    #[serde(default)]
177    pub destination_config: Option<serde_json::Value>,
178    /// `MaximumRetryAttempts` for the source. AWS uses `-1` to mean
179    /// "infinite" so we keep the int rather than a bool.
180    #[serde(default)]
181    pub maximum_retry_attempts: Option<i64>,
182    /// `MaximumRecordAgeInSeconds`. `-1` = infinite.
183    #[serde(default)]
184    pub maximum_record_age_in_seconds: Option<i64>,
185    /// `BisectBatchOnFunctionError` — split the batch in half and retry
186    /// on Lambda invoke failure (Kinesis / DDB streams only).
187    #[serde(default)]
188    pub bisect_batch_on_function_error: Option<bool>,
189    /// `TumblingWindowInSeconds` — Kinesis-only batch aggregation window.
190    #[serde(default)]
191    pub tumbling_window_in_seconds: Option<i64>,
192    /// `Topics` — MSK / self-managed-Kafka topic list.
193    #[serde(default)]
194    pub topics: Vec<String>,
195    /// `Queues` — Amazon MQ broker queue list.
196    #[serde(default)]
197    pub queues: Vec<String>,
198}
199
200/// A recorded Lambda invocation from cross-service delivery.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct LambdaInvocation {
203    pub function_arn: String,
204    pub payload: String,
205    pub timestamp: DateTime<Utc>,
206    pub source: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct LambdaState {
211    pub account_id: String,
212    pub region: String,
213    #[serde(default)]
214    pub functions: BTreeMap<String, LambdaFunction>,
215    #[serde(default)]
216    pub event_source_mappings: BTreeMap<String, EventSourceMapping>,
217    /// Recorded invocations from cross-service integrations — not persisted.
218    #[serde(default, skip)]
219    pub invocations: Vec<LambdaInvocation>,
220    /// Per-function aliases keyed by `{function}:{alias}`.
221    #[serde(default)]
222    pub aliases: BTreeMap<String, FunctionAlias>,
223    /// Published versions per function (function_name -> Vec<version>).
224    #[serde(default)]
225    pub function_versions: BTreeMap<String, Vec<String>>,
226    /// Immutable per-version snapshot of the function (code + config),
227    /// keyed by `function_name -> version -> LambdaFunction`. AWS makes
228    /// each numbered version a frozen copy of `$LATEST` at publish time.
229    #[serde(default)]
230    pub function_version_snapshots: BTreeMap<String, BTreeMap<String, LambdaFunction>>,
231    /// Layers keyed by name.
232    #[serde(default)]
233    pub layers: BTreeMap<String, Layer>,
234    /// Function URL configs keyed by function name.
235    #[serde(default)]
236    pub function_url_configs: BTreeMap<String, FunctionUrlConfig>,
237    /// Reserved concurrency configs keyed by function name.
238    #[serde(default)]
239    pub function_concurrency: BTreeMap<String, i64>,
240    /// Provisioned concurrency configs keyed by `{function}:{qualifier}`.
241    #[serde(default)]
242    pub provisioned_concurrency: BTreeMap<String, ProvisionedConcurrencyConfig>,
243    /// Code signing configs keyed by id.
244    #[serde(default)]
245    pub code_signing_configs: BTreeMap<String, CodeSigningConfig>,
246    /// Function-to-code-signing-config association keyed by function name.
247    #[serde(default)]
248    pub function_code_signing: BTreeMap<String, String>,
249    /// Event invoke configs keyed by `{function}:{qualifier}`.
250    #[serde(default)]
251    pub event_invoke_configs: BTreeMap<String, EventInvokeConfig>,
252    /// Runtime management configs keyed by `{function}:{qualifier}`.
253    #[serde(default)]
254    pub runtime_management: BTreeMap<String, RuntimeManagementConfig>,
255    /// Scaling configs keyed by event source mapping uuid.
256    #[serde(default)]
257    pub scaling_configs: BTreeMap<String, FunctionScalingConfig>,
258    /// Recursion configs keyed by function name.
259    #[serde(default)]
260    pub recursion_configs: BTreeMap<String, String>,
261    /// Account settings (single per-account record).
262    #[serde(default)]
263    pub account_settings: Option<AccountSettings>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct FunctionAlias {
268    pub alias_arn: String,
269    pub name: String,
270    pub function_version: String,
271    pub description: String,
272    pub revision_id: String,
273    pub routing_config: Option<serde_json::Value>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct Layer {
278    pub layer_name: String,
279    pub layer_arn: String,
280    pub versions: Vec<LayerVersion>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct LayerVersion {
285    pub version: i64,
286    pub layer_version_arn: String,
287    pub description: String,
288    pub created_date: DateTime<Utc>,
289    pub compatible_runtimes: Vec<String>,
290    pub license_info: String,
291    pub policy: Option<String>,
292    /// Raw ZIP bytes from `Content.ZipFile` on `PublishLayerVersion`.
293    /// `None` only on legacy snapshots predating layer storage.
294    #[serde(default)]
295    pub code_zip: Option<Vec<u8>>,
296    #[serde(default)]
297    pub code_sha256: String,
298    #[serde(default)]
299    pub code_size: i64,
300    /// `CompatibleArchitectures` declared at publish time. AWS rejects
301    /// `GetFunction` if the function's architecture isn't in this set;
302    /// fakecloud round-trips the field but doesn't enforce.
303    #[serde(default)]
304    pub compatible_architectures: Vec<String>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct FunctionUrlConfig {
309    pub function_arn: String,
310    pub function_url: String,
311    pub auth_type: String,
312    pub cors: Option<serde_json::Value>,
313    pub creation_time: DateTime<Utc>,
314    pub last_modified_time: DateTime<Utc>,
315    pub invoke_mode: String,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ProvisionedConcurrencyConfig {
320    pub requested: i64,
321    pub allocated: i64,
322    pub status: String,
323    pub last_modified: DateTime<Utc>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct CodeSigningConfig {
328    pub csc_id: String,
329    pub csc_arn: String,
330    pub description: String,
331    pub allowed_publishers: Vec<String>,
332    pub untrusted_artifact_action: String,
333    pub last_modified: DateTime<Utc>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct EventInvokeConfig {
338    pub function_arn: String,
339    pub maximum_event_age: i64,
340    pub maximum_retry_attempts: i64,
341    pub destination_config: serde_json::Value,
342    pub last_modified: DateTime<Utc>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RuntimeManagementConfig {
347    pub update_runtime_on: String,
348    pub runtime_version_arn: String,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct FunctionScalingConfig {
353    pub maximum_concurrency: i64,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, Default)]
357pub struct AccountSettings {
358    pub concurrent_executions: i64,
359    pub code_size_zipped: i64,
360    pub code_size_unzipped: i64,
361    pub total_code_size: i64,
362}
363
364impl LambdaState {
365    pub fn new(account_id: &str, region: &str) -> Self {
366        Self {
367            account_id: account_id.to_string(),
368            region: region.to_string(),
369            functions: BTreeMap::new(),
370            event_source_mappings: BTreeMap::new(),
371            invocations: Vec::new(),
372            aliases: BTreeMap::new(),
373            function_versions: BTreeMap::new(),
374            function_version_snapshots: BTreeMap::new(),
375            layers: BTreeMap::new(),
376            function_url_configs: BTreeMap::new(),
377            function_concurrency: BTreeMap::new(),
378            provisioned_concurrency: BTreeMap::new(),
379            code_signing_configs: BTreeMap::new(),
380            function_code_signing: BTreeMap::new(),
381            event_invoke_configs: BTreeMap::new(),
382            runtime_management: BTreeMap::new(),
383            scaling_configs: BTreeMap::new(),
384            recursion_configs: BTreeMap::new(),
385            account_settings: None,
386        }
387    }
388
389    pub fn reset(&mut self) {
390        self.functions.clear();
391        self.event_source_mappings.clear();
392        self.invocations.clear();
393        self.aliases.clear();
394        self.function_versions.clear();
395        self.function_version_snapshots.clear();
396        self.layers.clear();
397        self.function_url_configs.clear();
398        self.function_concurrency.clear();
399        self.provisioned_concurrency.clear();
400        self.code_signing_configs.clear();
401        self.function_code_signing.clear();
402        self.event_invoke_configs.clear();
403        self.runtime_management.clear();
404        self.scaling_configs.clear();
405        self.recursion_configs.clear();
406        self.account_settings = None;
407    }
408}
409
410pub type SharedLambdaState =
411    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<LambdaState>>>;
412
413impl fakecloud_core::multi_account::AccountState for LambdaState {
414    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
415        Self::new(account_id, region)
416    }
417}
418
419pub const LAMBDA_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
420
421#[derive(Debug, Serialize, Deserialize)]
422pub struct LambdaSnapshot {
423    pub schema_version: u32,
424    #[serde(default)]
425    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<LambdaState>>,
426    #[serde(default)]
427    pub state: Option<LambdaState>,
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn new_has_empty_collections() {
436        let state = LambdaState::new("123456789012", "us-east-1");
437        assert_eq!(state.account_id, "123456789012");
438        assert_eq!(state.region, "us-east-1");
439        assert!(state.functions.is_empty());
440        assert!(state.event_source_mappings.is_empty());
441        assert!(state.invocations.is_empty());
442    }
443
444    #[test]
445    fn reset_clears_collections() {
446        let mut state = LambdaState::new("123456789012", "us-east-1");
447        state.invocations.push(LambdaInvocation {
448            function_arn: "arn".to_string(),
449            payload: "p".to_string(),
450            timestamp: Utc::now(),
451            source: "s".to_string(),
452        });
453        state.reset();
454        assert!(state.invocations.is_empty());
455    }
456}