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