rusty_cdk_core/lambda/
builder.rs

1use crate::iam::{AssumeRolePolicyDocumentBuilder, Effect, RoleBuilder, RolePropertiesBuilder, Permission as IamPermission, Policy, StatementBuilder, PrincipalBuilder, map_toml_dependencies_to_services, find_missing_services, RoleRef};
2use crate::intrinsic::{get_arn, get_ref, join, AWS_PARTITION_PSEUDO_PARAM};
3use crate::lambda::{Environment, EventSourceMapping, EventSourceProperties, LambdaCode, Function, LambdaFunctionProperties, Permission, LambdaPermissionProperties, LoggingInfo, ScalingConfig, FunctionRef, PermissionRef};
4use crate::sqs::{QueueRef};
5use crate::stack::{Asset, Resource, StackBuilder};
6use crate::wrappers::{Bucket, EnvVarKey, LambdaPermissionAction, LogGroupName, Memory, RetentionInDays, SqsEventSourceMaxConcurrency, StringWithOnlyAlphaNumericsUnderscoresAndHyphens, Timeout, TomlFile, ZipFile};
7use serde_json::Value;
8use std::marker::PhantomData;
9use std::vec;
10use crate::cloudwatch::{LogGroupBuilder, LogGroupRef};
11use crate::shared::Id;
12use crate::type_state;
13
14pub enum Runtime {
15    NodeJs22,
16    Java21,
17    Python313,
18    ProvidedAl2023
19}
20
21impl From<Runtime> for String {
22    fn from(value: Runtime) -> Self {
23        match value {
24            Runtime::NodeJs22 => "nodejs22.x".to_string(),
25            Runtime::Java21 => "java21".to_string(),
26            Runtime::Python313 => "python3.13".to_string(),
27            Runtime::ProvidedAl2023 => "provided.al2023".to_string(),
28        }
29    }
30}
31
32pub enum Architecture {
33    X86_64,
34    ARM64
35}
36
37impl From<Architecture> for String {
38    fn from(value: Architecture) -> Self {
39        match value {
40            Architecture::X86_64 => "x86_64".to_string(),
41            Architecture::ARM64 => "arm64".to_string(),
42        }
43    }
44}
45
46pub enum PackageType {
47    Image,
48    Zip,
49}
50
51impl From<PackageType> for String {
52    fn from(value: PackageType) -> Self {
53        match value {
54            PackageType::Image => "Image".to_string(),
55            PackageType::Zip => "Zip".to_string()
56        }
57    }
58}
59
60pub struct Zip {
61    bucket: String,
62    file: ZipFile,
63}
64
65impl Zip {
66    pub fn new(bucket: Bucket, file: ZipFile) -> Self {
67        Zip {
68            bucket: bucket.0,
69            file
70        }
71    }
72}
73
74pub enum Code {
75    Zip(Zip)
76}
77
78type_state!(
79    FunctionBuilderState,
80    StartState,
81    ZipState,
82    ZipStateWithHandler,
83    ZipStateWithHandlerAndRuntime,
84    EventSourceMappingState,
85);
86
87struct EventSourceMappingInfo {
88    id: String,
89    max_concurrency: Option<u16>,
90}
91
92/// Builder for creating AWS Lambda functions.
93///
94/// This builder provides a fluent API for configuring Lambda functions with their associated IAM roles, environment variables, permissions, and event sources.
95///
96/// # Example
97///
98/// ```rust,no_run
99/// use rusty_cdk_core::stack::StackBuilder;
100/// use rusty_cdk_core::lambda::{FunctionBuilder, Architecture, Runtime, Zip};
101/// use rusty_cdk_core::wrappers::*;
102/// use rusty_cdk_macros::{memory, timeout, env_var_key, zip_file};
103///
104/// let mut stack_builder = StackBuilder::new();
105///
106/// let zip = unimplemented!("create a zip");
107///
108/// let (function, role, log_group) = FunctionBuilder::new(
109///         "my-function",
110///         Architecture::ARM64,
111///         memory!(512),
112///         timeout!(30)
113///     )
114///     .zip(zip)
115///     .handler("index.handler")
116///     .runtime(Runtime::NodeJs22)
117///     .env_var_string(env_var_key!("TABLE_NAME"), "my-table")
118///     .build(&mut stack_builder);
119/// ```
120pub struct FunctionBuilder<T: FunctionBuilderState> {
121    state: PhantomData<T>,
122    id: Id,
123    architecture: Architecture,
124    memory: u16,
125    timeout: u16,
126    code: Option<Code>,
127    handler: Option<String>,
128    runtime: Option<Runtime>,
129    additional_policies: Vec<Policy>,
130    aws_services_in_dependencies: Vec<String>,
131    env_vars: Vec<(String, Value)>,
132    function_name: Option<String>,
133    sqs_event_source_mapping: Option<EventSourceMappingInfo>,
134    reserved_concurrent_executions: Option<u32>,
135}
136
137impl<T: FunctionBuilderState> FunctionBuilder<T> {
138    pub fn function_name(self, name: StringWithOnlyAlphaNumericsUnderscoresAndHyphens) -> FunctionBuilder<T> {
139        Self {
140            function_name: Some(name.0),
141            ..self
142        }
143    }
144
145    pub fn permissions(mut self, permission: IamPermission) -> FunctionBuilder<T> {
146        self.additional_policies.push(permission.into_policy());
147        Self {
148            ..self
149        }
150    }
151
152    /// Checks that the function has permissions for AWS services listed in Cargo.toml dependencies.
153    ///
154    /// Parses the Cargo.toml to find AWS SDK dependencies and verifies that IAM permissions
155    /// have been granted for those services.
156    pub fn check_permissions_against_dependencies(self, cargo_toml: TomlFile) -> Self {
157        let services = map_toml_dependencies_to_services(cargo_toml.0.as_ref());
158        
159        Self {
160            aws_services_in_dependencies: services,
161            ..self
162        }
163    }
164    
165    pub fn env_var(mut self, key: EnvVarKey, value: Value) -> FunctionBuilder<T> {
166        self.env_vars.push((key.0, value));
167        Self {
168            ..self
169        }
170    }
171
172    pub fn env_var_string<V: Into<String>>(mut self, key: EnvVarKey, value: V) -> FunctionBuilder<T> {
173        self.env_vars.push((key.0, Value::String(value.into())));
174        Self {
175            ..self
176        }
177    }
178
179    pub fn reserved_concurrent_executions(self, executions: u32) -> FunctionBuilder<T> {
180        Self {
181            reserved_concurrent_executions: Some(executions),
182            ..self
183        }
184    }
185    
186    fn build_internal(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
187        let function_resource_id = Resource::generate_id("LambdaFunction");
188        
189        let code = match self.code.expect("code to be present, enforced by builder") {
190            Code::Zip(z) => {
191                let asset_id = Resource::generate_id("Asset");
192                let asset_id = format!("{asset_id}.zip");
193                
194                let asset = Asset {
195                    s3_bucket: z.bucket.clone(),
196                    s3_key: asset_id.clone(),
197                    path: z.file.0.to_string(),
198                };
199                
200                let code = LambdaCode {
201                    s3_bucket: Some(z.bucket),
202                    s3_key: Some(asset_id),
203                };
204
205                (asset, code)
206            }
207        };
208
209        if let Some(mapping) = self.sqs_event_source_mapping {
210            let event_id = Id::generate_id(&self.id, "ESM");
211            let event_resource_id = format!("EventSourceMapping{}", function_resource_id);
212            let event_source_mapping = EventSourceMapping {
213                id: event_id,
214                resource_id: event_resource_id.clone(),
215                r#type: "AWS::Lambda::EventSourceMapping".to_string(),
216                properties: EventSourceProperties {
217                    event_source_arn: Some(get_arn(&mapping.id)),
218                    function_name: Some(get_ref(&function_resource_id)),
219                    scaling_config: mapping.max_concurrency.map(|c| ScalingConfig { max_concurrency: c }),
220                },
221            };
222            stack_builder.add_resource(event_source_mapping);
223        };
224        
225        let assume_role_statement = StatementBuilder::internal_new(vec!["sts:AssumeRole".to_string()], Effect::Allow)
226            .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
227            .build();
228        let assumed_role_policy_document = AssumeRolePolicyDocumentBuilder::new(vec![assume_role_statement]).build();
229        let managed_policy_arns = vec![join("", vec![Value::String("arn:".to_string()), get_ref(AWS_PARTITION_PSEUDO_PARAM), Value::String(":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole".to_string())])];
230        let potentially_missing = find_missing_services(&self.aws_services_in_dependencies, &self.additional_policies);
231        let props = RolePropertiesBuilder::new(assumed_role_policy_document, managed_policy_arns).policies(self.additional_policies).build();
232        
233        let role_id = Id::generate_id(&self.id, "Role");
234        let role_resource_id = Resource::generate_id("LambdaFunctionRole");
235        let role_ref = get_arn(&role_resource_id);
236        let role = RoleBuilder::new_with_info_on_missing(&role_id, &role_resource_id, props, potentially_missing).build(stack_builder);
237
238        let environment = if self.env_vars.is_empty() {
239            None
240        } else {
241            Some(Environment {
242                variables: self.env_vars.into_iter().collect(),
243            })
244        };
245
246        let log_group_id = Id::generate_id(&self.id, "LogGroup");
247        let log_group_name = self.function_name.clone().map(|fun_name| format!("/aws/lambda/{fun_name}"));
248        let base_builder = LogGroupBuilder::new(&log_group_id)
249            .log_group_retention(RetentionInDays(731));
250        let log_group = if let Some(name) = log_group_name {
251            base_builder.log_group_name_string(LogGroupName(name)).build(stack_builder)
252        } else {
253            base_builder.build(stack_builder)
254        };
255        
256        let logging_info = LoggingInfo { log_group: Some( get_ref(log_group.get_resource_id())) };
257
258        let properties = LambdaFunctionProperties {
259            code: code.1,
260            architectures: vec![self.architecture.into()],
261            memory_size: self.memory,
262            timeout: self.timeout,
263            handler: self.handler,
264            runtime: self.runtime.map(Into::into),
265            role: role_ref,
266            function_name: self.function_name,
267            environment,
268            reserved_concurrent_executions: self.reserved_concurrent_executions,
269            logging_info,
270        };
271
272        stack_builder.add_resource(Function {
273            id: self.id.clone(),
274            resource_id: function_resource_id.clone(),
275            asset: code.0,
276            r#type: "AWS::Lambda::Function".to_string(),
277            properties,
278        });
279
280        let function = FunctionRef::new(self.id, function_resource_id);
281
282        (
283            function,
284            role,
285            log_group,
286        )
287    }
288}
289
290impl FunctionBuilder<StartState> {
291    /// Creates a new Lambda function builder.
292    ///
293    /// # Arguments
294    /// * `id` - Unique identifier for the function
295    /// * `architecture` - CPU architecture (x86_64 or ARM64)
296    /// * `memory` - Memory allocation in MB
297    /// * `timeout` - Maximum execution time
298    pub fn new(id: &str, architecture: Architecture, memory: Memory, timeout: Timeout) -> FunctionBuilder<StartState> {
299        FunctionBuilder {
300            state: Default::default(),
301            id: Id(id.to_string()),
302            architecture,
303            memory: memory.0,
304            timeout: timeout.0,
305            code: None,
306            handler: None,
307            runtime: None,
308            additional_policies: vec![],
309            aws_services_in_dependencies: vec![],
310            env_vars: vec![],
311            function_name: None,
312            sqs_event_source_mapping: None,
313            reserved_concurrent_executions: None,
314        }
315    }
316
317    pub fn zip(self, zip: Zip) -> FunctionBuilder<ZipState> {
318        FunctionBuilder {
319            code: Some(Code::Zip(zip)),
320            state: Default::default(),
321            id: self.id,
322            architecture: self.architecture,
323            memory: self.memory,
324            timeout: self.timeout,
325            handler: self.handler,
326            runtime: self.runtime,
327            additional_policies: self.additional_policies,
328            aws_services_in_dependencies: self.aws_services_in_dependencies,
329            env_vars: self.env_vars,
330            function_name: self.function_name,
331            sqs_event_source_mapping: self.sqs_event_source_mapping,
332            reserved_concurrent_executions: self.reserved_concurrent_executions,
333        }
334    }
335}
336
337impl FunctionBuilder<ZipState> {
338    pub fn handler<T: Into<String>>(self, handler: T) -> FunctionBuilder<ZipStateWithHandler> {
339        FunctionBuilder {
340            id: self.id,
341            handler: Some(handler.into()),
342            state: Default::default(),
343            architecture: self.architecture,
344            memory: self.memory,
345            timeout: self.timeout,
346            code: self.code,
347            runtime: self.runtime,
348            additional_policies: self.additional_policies,
349            aws_services_in_dependencies: self.aws_services_in_dependencies,
350            env_vars: self.env_vars,
351            function_name: self.function_name,
352            sqs_event_source_mapping: self.sqs_event_source_mapping,
353            reserved_concurrent_executions: self.reserved_concurrent_executions,
354        }
355    }
356}
357
358impl FunctionBuilder<ZipStateWithHandler> {
359    pub fn runtime(self, runtime: Runtime) -> FunctionBuilder<ZipStateWithHandlerAndRuntime> {
360        FunctionBuilder {
361            id: self.id,
362            runtime: Some(runtime),
363            state: Default::default(),
364            architecture: self.architecture,
365            memory: self.memory,
366            timeout: self.timeout,
367            code: self.code,
368            handler: self.handler,
369            additional_policies: self.additional_policies,
370            aws_services_in_dependencies: self.aws_services_in_dependencies,
371            env_vars: self.env_vars,
372            function_name: self.function_name,
373            sqs_event_source_mapping: self.sqs_event_source_mapping,
374            reserved_concurrent_executions: self.reserved_concurrent_executions,
375        }
376    }
377}
378
379impl FunctionBuilder<ZipStateWithHandlerAndRuntime> {
380    /// Configures the function to be triggered by an SQS queue.
381    ///
382    /// Automatically adds the necessary IAM permissions for reading from the queue.
383    pub fn sqs_event_source_mapping(mut self, sqs_queue: &QueueRef, max_concurrency: Option<SqsEventSourceMaxConcurrency>) -> FunctionBuilder<EventSourceMappingState>  {
384        self.additional_policies.push(IamPermission::SqsRead(sqs_queue).into_policy());
385        
386        let mapping = EventSourceMappingInfo {
387            id: sqs_queue.get_resource_id().to_string(),
388            max_concurrency: max_concurrency.map(|c| c.0),
389        };
390        
391        FunctionBuilder {
392            id: self.id,
393            sqs_event_source_mapping: Some(mapping),
394            state: Default::default(),
395            runtime: self.runtime,
396            architecture: self.architecture,
397            memory: self.memory,
398            timeout: self.timeout,
399            code: self.code,
400            handler: self.handler,
401            additional_policies: self.additional_policies,
402            aws_services_in_dependencies: self.aws_services_in_dependencies,
403            env_vars: self.env_vars,
404            function_name: self.function_name,
405            reserved_concurrent_executions: self.reserved_concurrent_executions,
406        }
407    }
408
409    /// Builds the Lambda function and adds it to the stack.
410    ///
411    /// Creates the function along with its IAM execution role and CloudWatch log group.
412    /// Returns references to all three resources.
413    pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
414        self.build_internal(stack_builder)
415    }
416}
417
418impl FunctionBuilder<EventSourceMappingState> {
419    /// Builds the Lambda function and adds it to the stack.
420    ///
421    /// Creates the function along with its IAM execution role, CloudWatch log group, and event source mapping.
422    /// Returns references to the function, role, and log group.
423    pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
424        self.build_internal(stack_builder)
425    }
426}
427
428/// Builder for Lambda function permissions.
429///
430/// Creates permission resources that allow other AWS services to invoke a Lambda function.
431pub struct PermissionBuilder {
432    id: Id,
433    action: String,
434    function_name: Value,
435    principal: String,
436    source_arn: Option<Value>,
437}
438
439impl PermissionBuilder {
440    /// Creates a new permission builder for a Lambda function.
441    ///
442    /// # Arguments
443    /// * `id` - Unique identifier for the permission resource
444    /// * `action` - Lambda action to allow (e.g., "lambda:InvokeFunction")
445    /// * `function_name` - Reference to the Lambda function
446    /// * `principal` - AWS service or account that will be granted permission
447    pub fn new<R: Into<String>>(id: &str, action: LambdaPermissionAction, function_name: Value, principal: R) -> Self {
448        Self {
449            id: Id(id.to_string()),
450            action: action.0,
451            function_name,
452            principal: principal.into(),
453            source_arn: None,
454        }
455    }
456
457    pub fn source_arn(self, arn: Value) -> Self {
458        Self {
459            source_arn: Some(arn),
460            ..self
461        }
462    }
463
464    pub fn build(self , stack_builder: &mut StackBuilder) -> PermissionRef {
465        let permission_resource_id = Resource::generate_id("LambdaPermission");
466
467        stack_builder.add_resource(Permission {
468            id: self.id,
469            resource_id: permission_resource_id.clone(),
470            r#type: "AWS::Lambda::Permission".to_string(),
471            properties: LambdaPermissionProperties {
472                action: self.action,
473                function_name: self.function_name,
474                principal: self.principal,
475                source_arn: self.source_arn,
476            },
477        });
478        
479        PermissionRef::new(permission_resource_id)
480    }
481}