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