Skip to main content

rusty_cdk_core/lambda/
builder.rs

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