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    /// Capacity providers (Lambda Workflows, 2025-11-30 API) keyed by name.
270    #[serde(default)]
271    pub capacity_providers: BTreeMap<String, CapacityProvider>,
272    /// Durable executions (Lambda Workflows, 2025-12-01 API) keyed by ARN.
273    #[serde(default)]
274    pub durable_executions: BTreeMap<String, DurableExecution>,
275    /// Durable execution callbacks keyed by callback id. Each callback
276    /// belongs to one execution and records its outcome on Send*Callback*.
277    #[serde(default)]
278    pub durable_execution_callbacks: BTreeMap<String, DurableExecutionCallback>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct CapacityProvider {
283    pub name: String,
284    pub arn: String,
285    pub state: String,
286    pub vpc_config: serde_json::Value,
287    pub permissions_config: serde_json::Value,
288    pub instance_requirements: Option<serde_json::Value>,
289    pub scaling_config: Option<serde_json::Value>,
290    pub kms_key_arn: Option<String>,
291    pub tags: BTreeMap<String, String>,
292    pub last_modified: DateTime<Utc>,
293    /// Function versions associated with the capacity provider, as
294    /// `function_name:qualifier` strings. Populated implicitly when a
295    /// function version references the provider; we don't yet write
296    /// from `CreateFunction`, but `ListFunctionVersionsByCapacityProvider`
297    /// can read whatever is staged here.
298    #[serde(default)]
299    pub function_versions: Vec<String>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct DurableExecution {
304    pub arn: String,
305    pub function_name: String,
306    pub function_arn: String,
307    pub status: String,
308    pub input: serde_json::Value,
309    pub started_at: DateTime<Utc>,
310    pub stopped_at: Option<DateTime<Utc>>,
311    pub last_modified: DateTime<Utc>,
312    pub history: Vec<serde_json::Value>,
313    pub state: serde_json::Value,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct DurableExecutionCallback {
318    pub callback_id: String,
319    pub execution_arn: String,
320    pub outcome: String,
321    pub recorded_at: DateTime<Utc>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct FunctionAlias {
326    pub alias_arn: String,
327    pub name: String,
328    pub function_version: String,
329    pub description: String,
330    pub revision_id: String,
331    pub routing_config: Option<serde_json::Value>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct Layer {
336    pub layer_name: String,
337    pub layer_arn: String,
338    pub versions: Vec<LayerVersion>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct LayerVersion {
343    pub version: i64,
344    pub layer_version_arn: String,
345    pub description: String,
346    pub created_date: DateTime<Utc>,
347    pub compatible_runtimes: Vec<String>,
348    pub license_info: String,
349    pub policy: Option<String>,
350    /// Raw ZIP bytes from `Content.ZipFile` on `PublishLayerVersion`.
351    /// `None` only on legacy snapshots predating layer storage.
352    #[serde(default)]
353    pub code_zip: Option<Vec<u8>>,
354    #[serde(default)]
355    pub code_sha256: String,
356    #[serde(default)]
357    pub code_size: i64,
358    /// `CompatibleArchitectures` declared at publish time. AWS rejects
359    /// `GetFunction` if the function's architecture isn't in this set;
360    /// fakecloud round-trips the field but doesn't enforce.
361    #[serde(default)]
362    pub compatible_architectures: Vec<String>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct FunctionUrlConfig {
367    pub function_arn: String,
368    pub function_url: String,
369    pub auth_type: String,
370    pub cors: Option<serde_json::Value>,
371    pub creation_time: DateTime<Utc>,
372    pub last_modified_time: DateTime<Utc>,
373    pub invoke_mode: String,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct ProvisionedConcurrencyConfig {
378    pub requested: i64,
379    pub allocated: i64,
380    pub status: String,
381    pub last_modified: DateTime<Utc>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct CodeSigningConfig {
386    pub csc_id: String,
387    pub csc_arn: String,
388    pub description: String,
389    pub allowed_publishers: Vec<String>,
390    pub untrusted_artifact_action: String,
391    pub last_modified: DateTime<Utc>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct EventInvokeConfig {
396    pub function_arn: String,
397    pub maximum_event_age: i64,
398    pub maximum_retry_attempts: i64,
399    /// `None` -> input omitted `DestinationConfig` entirely; AWS responds
400    /// with `{OnSuccess:{}, OnFailure:{}}` (per `@examples` for
401    /// `PutFunctionEventInvokeConfig`).
402    ///
403    /// `Some({})` -> caller explicitly sent `{}`; AWS echoes `{}` verbatim
404    /// (round-trip semantics).
405    ///
406    /// `Some({...})` -> half-populated; AWS backfills the missing half as
407    /// `{}` (per `@examples` for `UpdateFunctionEventInvokeConfig`).
408    #[serde(default)]
409    pub destination_config: Option<serde_json::Value>,
410    pub last_modified: DateTime<Utc>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct RuntimeManagementConfig {
415    pub update_runtime_on: String,
416    pub runtime_version_arn: String,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, Default)]
420pub struct FunctionScalingConfig {
421    /// `MinExecutionEnvironments` — the minimum number of execution
422    /// environments to maintain for the function. AWS's
423    /// `FunctionScalingConfig` shape uses these two members (not the
424    /// pre-2025 `MaximumConcurrency`).
425    pub min_execution_environments: Option<i64>,
426    /// `MaxExecutionEnvironments` — the upper bound on provisioned
427    /// execution environments.
428    pub max_execution_environments: Option<i64>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, Default)]
432pub struct AccountSettings {
433    pub concurrent_executions: i64,
434    pub code_size_zipped: i64,
435    pub code_size_unzipped: i64,
436    pub total_code_size: i64,
437}
438
439impl LambdaState {
440    pub fn new(account_id: &str, region: &str) -> Self {
441        Self {
442            account_id: account_id.to_string(),
443            region: region.to_string(),
444            functions: BTreeMap::new(),
445            event_source_mappings: BTreeMap::new(),
446            invocations: Vec::new(),
447            aliases: BTreeMap::new(),
448            function_versions: BTreeMap::new(),
449            function_version_snapshots: BTreeMap::new(),
450            layers: BTreeMap::new(),
451            function_url_configs: BTreeMap::new(),
452            function_concurrency: BTreeMap::new(),
453            provisioned_concurrency: BTreeMap::new(),
454            code_signing_configs: BTreeMap::new(),
455            function_code_signing: BTreeMap::new(),
456            event_invoke_configs: BTreeMap::new(),
457            runtime_management: BTreeMap::new(),
458            scaling_configs: BTreeMap::new(),
459            recursion_configs: BTreeMap::new(),
460            account_settings: None,
461            capacity_providers: BTreeMap::new(),
462            durable_executions: BTreeMap::new(),
463            durable_execution_callbacks: BTreeMap::new(),
464        }
465    }
466
467    pub fn reset(&mut self) {
468        self.functions.clear();
469        self.event_source_mappings.clear();
470        self.invocations.clear();
471        self.aliases.clear();
472        self.function_versions.clear();
473        self.function_version_snapshots.clear();
474        self.layers.clear();
475        self.function_url_configs.clear();
476        self.function_concurrency.clear();
477        self.provisioned_concurrency.clear();
478        self.code_signing_configs.clear();
479        self.function_code_signing.clear();
480        self.event_invoke_configs.clear();
481        self.runtime_management.clear();
482        self.scaling_configs.clear();
483        self.recursion_configs.clear();
484        self.account_settings = None;
485        self.capacity_providers.clear();
486        self.durable_executions.clear();
487        self.durable_execution_callbacks.clear();
488    }
489}
490
491pub type SharedLambdaState =
492    Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<LambdaState>>>;
493
494impl fakecloud_core::multi_account::AccountState for LambdaState {
495    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
496        Self::new(account_id, region)
497    }
498}
499
500pub const LAMBDA_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
501
502#[derive(Debug, Serialize, Deserialize)]
503pub struct LambdaSnapshot {
504    pub schema_version: u32,
505    #[serde(default)]
506    pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<LambdaState>>,
507    #[serde(default)]
508    pub state: Option<LambdaState>,
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn new_has_empty_collections() {
517        let state = LambdaState::new("123456789012", "us-east-1");
518        assert_eq!(state.account_id, "123456789012");
519        assert_eq!(state.region, "us-east-1");
520        assert!(state.functions.is_empty());
521        assert!(state.event_source_mappings.is_empty());
522        assert!(state.invocations.is_empty());
523    }
524
525    #[test]
526    fn reset_clears_collections() {
527        let mut state = LambdaState::new("123456789012", "us-east-1");
528        state.invocations.push(LambdaInvocation {
529            function_arn: "arn".to_string(),
530            payload: "p".to_string(),
531            timestamp: Utc::now(),
532            source: "s".to_string(),
533        });
534        state.reset();
535        assert!(state.invocations.is_empty());
536    }
537}