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    /// `DurableConfig` for AWS's durable-function feature
110    /// (`RetentionPeriodInDays`, `ExecutionTimeout`). Round-tripped
111    /// only — there's no execution-history backend in fakecloud.
112    #[serde(default)]
113    pub durable_config: Option<serde_json::Value>,
114    /// Free-form `LastUpdateStatusReason` set on the most recent failed
115    /// or in-progress configuration update.
116    #[serde(default)]
117    pub last_update_status_reason: Option<String>,
118    /// Machine-readable code paired with `last_update_status_reason`
119    /// (`EniLimitExceeded`, `InsufficientRolePermissions`, …).
120    #[serde(default)]
121    pub last_update_status_reason_code: Option<String>,
122}
123
124fn default_revision_id() -> String {
125    uuid::Uuid::new_v4().to_string()
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct AttachedLayer {
130    pub arn: String,
131    #[serde(default)]
132    pub code_size: i64,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct EventSourceMapping {
137    pub uuid: String,
138    pub function_arn: String,
139    pub event_source_arn: String,
140    pub batch_size: i64,
141    pub enabled: bool,
142    pub state: String,
143    pub last_modified: DateTime<Utc>,
144    /// Raw `Filters: [{Pattern: "..."}]` array as supplied via
145    /// `FilterCriteria`. Each pattern is an EventBridge-style JSON
146    /// pattern matched against the record body — non-matching records
147    /// are dropped.
148    #[serde(default)]
149    pub filter_patterns: Vec<String>,
150    /// Wait up to N seconds to accumulate `batch_size` records before
151    /// invoking. Implemented as a deadline check inside the poller.
152    #[serde(default)]
153    pub maximum_batching_window_in_seconds: Option<i64>,
154    /// `LATEST`, `TRIM_HORIZON`, or `AT_TIMESTAMP`. Honored on the
155    /// first poll for stream sources (Kinesis, DDB Streams).
156    #[serde(default)]
157    pub starting_position: Option<String>,
158    /// Optional epoch-second timestamp paired with
159    /// `StartingPosition=AT_TIMESTAMP`.
160    #[serde(default)]
161    pub starting_position_timestamp: Option<f64>,
162    /// Kinesis-only: number of concurrent batch invocations per shard.
163    #[serde(default)]
164    pub parallelization_factor: Option<i64>,
165    /// `["ReportBatchItemFailures"]` to opt into partial-batch failure
166    /// semantics. Empty / unset = entire batch is retried on error.
167    #[serde(default)]
168    pub function_response_types: Vec<String>,
169    /// KMS key for encrypting the filter-criteria document at rest. AWS
170    /// added this in 2024 for Kafka/Kinesis sources whose filters carry
171    /// sensitive selectors.
172    #[serde(default)]
173    pub kms_key_arn: Option<String>,
174    /// `MetricsConfig.Metrics` — set of opted-in CloudWatch metrics
175    /// (`["EventCount"]`). Round-tripped only; fakecloud doesn't yet
176    /// publish these metrics.
177    #[serde(default)]
178    pub metrics_config: Option<serde_json::Value>,
179    /// `DestinationConfig` — `OnFailure.Destination` arn (and rarely
180    /// `OnSuccess.Destination` for self-managed Kafka). Round-tripped.
181    #[serde(default)]
182    pub destination_config: Option<serde_json::Value>,
183    /// `MaximumRetryAttempts` for the source. AWS uses `-1` to mean
184    /// "infinite" so we keep the int rather than a bool.
185    #[serde(default)]
186    pub maximum_retry_attempts: Option<i64>,
187    /// `MaximumRecordAgeInSeconds`. `-1` = infinite.
188    #[serde(default)]
189    pub maximum_record_age_in_seconds: Option<i64>,
190    /// `BisectBatchOnFunctionError` — split the batch in half and retry
191    /// on Lambda invoke failure (Kinesis / DDB streams only).
192    #[serde(default)]
193    pub bisect_batch_on_function_error: Option<bool>,
194    /// `TumblingWindowInSeconds` — Kinesis-only batch aggregation window.
195    #[serde(default)]
196    pub tumbling_window_in_seconds: Option<i64>,
197    /// `Topics` — MSK / self-managed-Kafka topic list.
198    #[serde(default)]
199    pub topics: Vec<String>,
200    /// `Queues` — Amazon MQ broker queue list.
201    #[serde(default)]
202    pub queues: Vec<String>,
203}
204
205/// A recorded Lambda invocation from cross-service delivery.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct LambdaInvocation {
208    pub function_arn: String,
209    pub payload: String,
210    pub timestamp: DateTime<Utc>,
211    pub source: String,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct LambdaState {
216    pub account_id: String,
217    pub region: String,
218    #[serde(default)]
219    pub functions: BTreeMap<String, LambdaFunction>,
220    #[serde(default)]
221    pub event_source_mappings: BTreeMap<String, EventSourceMapping>,
222    /// Recorded invocations from cross-service integrations — not persisted.
223    #[serde(default, skip)]
224    pub invocations: Vec<LambdaInvocation>,
225    /// Per-function aliases keyed by `{function}:{alias}`.
226    #[serde(default)]
227    pub aliases: BTreeMap<String, FunctionAlias>,
228    /// Published versions per function (function_name -> Vec<version>).
229    #[serde(default)]
230    pub function_versions: BTreeMap<String, Vec<String>>,
231    /// Immutable per-version snapshot of the function (code + config),
232    /// keyed by `function_name -> version -> LambdaFunction`. AWS makes
233    /// each numbered version a frozen copy of `$LATEST` at publish time.
234    #[serde(default)]
235    pub function_version_snapshots: BTreeMap<String, BTreeMap<String, LambdaFunction>>,
236    /// Layers keyed by name.
237    #[serde(default)]
238    pub layers: BTreeMap<String, Layer>,
239    /// Function URL configs keyed by function name.
240    #[serde(default)]
241    pub function_url_configs: BTreeMap<String, FunctionUrlConfig>,
242    /// Reserved concurrency configs keyed by function name.
243    #[serde(default)]
244    pub function_concurrency: BTreeMap<String, i64>,
245    /// Provisioned concurrency configs keyed by `{function}:{qualifier}`.
246    #[serde(default)]
247    pub provisioned_concurrency: BTreeMap<String, ProvisionedConcurrencyConfig>,
248    /// Code signing configs keyed by id.
249    #[serde(default)]
250    pub code_signing_configs: BTreeMap<String, CodeSigningConfig>,
251    /// Function-to-code-signing-config association keyed by function name.
252    #[serde(default)]
253    pub function_code_signing: BTreeMap<String, String>,
254    /// Event invoke configs keyed by `{function}:{qualifier}`.
255    #[serde(default)]
256    pub event_invoke_configs: BTreeMap<String, EventInvokeConfig>,
257    /// Runtime management configs keyed by `{function}:{qualifier}`.
258    #[serde(default)]
259    pub runtime_management: BTreeMap<String, RuntimeManagementConfig>,
260    /// Scaling configs keyed by event source mapping uuid.
261    #[serde(default)]
262    pub scaling_configs: BTreeMap<String, FunctionScalingConfig>,
263    /// Recursion configs keyed by function name.
264    #[serde(default)]
265    pub recursion_configs: BTreeMap<String, String>,
266    /// Account settings (single per-account record).
267    #[serde(default)]
268    pub account_settings: Option<AccountSettings>,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct FunctionAlias {
273    pub alias_arn: String,
274    pub name: String,
275    pub function_version: String,
276    pub description: String,
277    pub revision_id: String,
278    pub routing_config: Option<serde_json::Value>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct Layer {
283    pub layer_name: String,
284    pub layer_arn: String,
285    pub versions: Vec<LayerVersion>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct LayerVersion {
290    pub version: i64,
291    pub layer_version_arn: String,
292    pub description: String,
293    pub created_date: DateTime<Utc>,
294    pub compatible_runtimes: Vec<String>,
295    pub license_info: String,
296    pub policy: Option<String>,
297    /// Raw ZIP bytes from `Content.ZipFile` on `PublishLayerVersion`.
298    /// `None` only on legacy snapshots predating layer storage.
299    #[serde(default)]
300    pub code_zip: Option<Vec<u8>>,
301    #[serde(default)]
302    pub code_sha256: String,
303    #[serde(default)]
304    pub code_size: i64,
305    /// `CompatibleArchitectures` declared at publish time. AWS rejects
306    /// `GetFunction` if the function's architecture isn't in this set;
307    /// fakecloud round-trips the field but doesn't enforce.
308    #[serde(default)]
309    pub compatible_architectures: Vec<String>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct FunctionUrlConfig {
314    pub function_arn: String,
315    pub function_url: String,
316    pub auth_type: String,
317    pub cors: Option<serde_json::Value>,
318    pub creation_time: DateTime<Utc>,
319    pub last_modified_time: DateTime<Utc>,
320    pub invoke_mode: String,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct ProvisionedConcurrencyConfig {
325    pub requested: i64,
326    pub allocated: i64,
327    pub status: String,
328    pub last_modified: DateTime<Utc>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct CodeSigningConfig {
333    pub csc_id: String,
334    pub csc_arn: String,
335    pub description: String,
336    pub allowed_publishers: Vec<String>,
337    pub untrusted_artifact_action: String,
338    pub last_modified: DateTime<Utc>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct EventInvokeConfig {
343    pub function_arn: String,
344    pub maximum_event_age: i64,
345    pub maximum_retry_attempts: i64,
346    /// `None` -> input omitted `DestinationConfig` entirely; AWS responds
347    /// with `{OnSuccess:{}, OnFailure:{}}` (per `@examples` for
348    /// `PutFunctionEventInvokeConfig`).
349    ///
350    /// `Some({})` -> caller explicitly sent `{}`; AWS echoes `{}` verbatim
351    /// (round-trip semantics).
352    ///
353    /// `Some({...})` -> half-populated; AWS backfills the missing half as
354    /// `{}` (per `@examples` for `UpdateFunctionEventInvokeConfig`).
355    #[serde(default)]
356    pub destination_config: Option<serde_json::Value>,
357    pub last_modified: DateTime<Utc>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RuntimeManagementConfig {
362    pub update_runtime_on: String,
363    pub runtime_version_arn: String,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize, Default)]
367pub struct FunctionScalingConfig {
368    /// `MinExecutionEnvironments` — the minimum number of execution
369    /// environments to maintain for the function. AWS's
370    /// `FunctionScalingConfig` shape uses these two members (not the
371    /// pre-2025 `MaximumConcurrency`).
372    pub min_execution_environments: Option<i64>,
373    /// `MaxExecutionEnvironments` — the upper bound on provisioned
374    /// execution environments.
375    pub max_execution_environments: Option<i64>,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize, Default)]
379pub struct AccountSettings {
380    pub concurrent_executions: i64,
381    pub code_size_zipped: i64,
382    pub code_size_unzipped: i64,
383    pub total_code_size: i64,
384}
385
386impl LambdaState {
387    pub fn new(account_id: &str, region: &str) -> Self {
388        Self {
389            account_id: account_id.to_string(),
390            region: region.to_string(),
391            functions: BTreeMap::new(),
392            event_source_mappings: BTreeMap::new(),
393            invocations: Vec::new(),
394            aliases: BTreeMap::new(),
395            function_versions: BTreeMap::new(),
396            function_version_snapshots: BTreeMap::new(),
397            layers: BTreeMap::new(),
398            function_url_configs: BTreeMap::new(),
399            function_concurrency: BTreeMap::new(),
400            provisioned_concurrency: BTreeMap::new(),
401            code_signing_configs: BTreeMap::new(),
402            function_code_signing: BTreeMap::new(),
403            event_invoke_configs: BTreeMap::new(),
404            runtime_management: BTreeMap::new(),
405            scaling_configs: BTreeMap::new(),
406            recursion_configs: BTreeMap::new(),
407            account_settings: None,
408        }
409    }
410
411    pub fn reset(&mut self) {
412        self.functions.clear();
413        self.event_source_mappings.clear();
414        self.invocations.clear();
415        self.aliases.clear();
416        self.function_versions.clear();
417        self.function_version_snapshots.clear();
418        self.layers.clear();
419        self.function_url_configs.clear();
420        self.function_concurrency.clear();
421        self.provisioned_concurrency.clear();
422        self.code_signing_configs.clear();
423        self.function_code_signing.clear();
424        self.event_invoke_configs.clear();
425        self.runtime_management.clear();
426        self.scaling_configs.clear();
427        self.recursion_configs.clear();
428        self.account_settings = None;
429    }
430}
431
432pub type SharedLambdaState =
433    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<LambdaState>>>;
434
435impl fakecloud_core::multi_account::AccountState for LambdaState {
436    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
437        Self::new(account_id, region)
438    }
439}
440
441pub const LAMBDA_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
442
443#[derive(Debug, Serialize, Deserialize)]
444pub struct LambdaSnapshot {
445    pub schema_version: u32,
446    #[serde(default)]
447    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<LambdaState>>,
448    #[serde(default)]
449    pub state: Option<LambdaState>,
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn new_has_empty_collections() {
458        let state = LambdaState::new("123456789012", "us-east-1");
459        assert_eq!(state.account_id, "123456789012");
460        assert_eq!(state.region, "us-east-1");
461        assert!(state.functions.is_empty());
462        assert!(state.event_source_mappings.is_empty());
463        assert!(state.invocations.is_empty());
464    }
465
466    #[test]
467    fn reset_clears_collections() {
468        let mut state = LambdaState::new("123456789012", "us-east-1");
469        state.invocations.push(LambdaInvocation {
470            function_arn: "arn".to_string(),
471            payload: "p".to_string(),
472            timestamp: Utc::now(),
473            source: "s".to_string(),
474        });
475        state.reset();
476        assert!(state.invocations.is_empty());
477    }
478}