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}
148
149impl<T: FunctionBuilderState> FunctionBuilder<T> {
150    pub fn function_name(self, name: StringWithOnlyAlphaNumericsUnderscoresAndHyphens) -> FunctionBuilder<T> {
151        Self {
152            function_name: Some(name.0),
153            ..self
154        }
155    }
156
157    pub fn add_permission(mut self, permission: IamPermission) -> FunctionBuilder<T> {
158        self.additional_policies.push(permission.into_policy());
159        Self { ..self }
160    }
161
162    /// Checks that the function has permissions for AWS services listed in Cargo.toml dependencies.
163    ///
164    /// Parses the Cargo.toml to find AWS SDK dependencies and verifies that IAM permissions
165    /// have been granted for those services.
166    pub fn check_permissions_against_dependencies(self, cargo_toml: TomlFile) -> Self {
167        let services = map_toml_dependencies_to_services(cargo_toml.0.as_ref());
168
169        Self {
170            aws_services_in_dependencies: services,
171            ..self
172        }
173    }
174
175    pub fn env_var(mut self, key: EnvVarKey, value: Value) -> FunctionBuilder<T> {
176        self.env_vars.push((key.0, value));
177        Self { ..self }
178    }
179
180    pub fn env_var_string<V: Into<String>>(mut self, key: EnvVarKey, value: V) -> FunctionBuilder<T> {
181        self.env_vars.push((key.0, Value::String(value.into())));
182        Self { ..self }
183    }
184
185    pub fn reserved_concurrent_executions(self, executions: u32) -> FunctionBuilder<T> {
186        Self {
187            reserved_concurrent_executions: Some(executions),
188            ..self
189        }
190    }
191
192    fn build_internal(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
193        let function_resource_id = Resource::generate_id("LambdaFunction");
194
195        let code = match self.code.expect("code to be present, enforced by builder") {
196            Code::Zip(z) => {
197                let asset_id = Resource::generate_id("Asset");
198                let asset_id = format!("{asset_id}.zip");
199
200                let asset = Asset {
201                    s3_bucket: z.bucket.clone(),
202                    s3_key: asset_id.clone(),
203                    path: z.file.0.to_string(),
204                };
205
206                let code = LambdaCode {
207                    s3_bucket: Some(z.bucket),
208                    s3_key: Some(asset_id),
209                    zipfile: None,
210                };
211
212                (Some(asset), code)
213            }
214            Code::Inline(inline_code) => {
215                let code = LambdaCode {
216                    s3_bucket: None,
217                    s3_key: None,
218                    zipfile: Some(inline_code),
219                };
220                (None, code)
221            }
222        };
223
224        if let Some(mapping) = self.sqs_event_source_mapping {
225            let event_id = Id::generate_id(&self.id, "ESM");
226            let event_resource_id = format!("EventSourceMapping{}", function_resource_id);
227            let event_source_mapping = EventSourceMapping {
228                id: event_id,
229                resource_id: event_resource_id.clone(),
230                r#type: "AWS::Lambda::EventSourceMapping".to_string(),
231                properties: EventSourceProperties {
232                    event_source_arn: Some(get_arn(&mapping.id)),
233                    function_name: Some(get_ref(&function_resource_id)),
234                    scaling_config: mapping.max_concurrency.map(|c| ScalingConfig { max_concurrency: c }),
235                },
236            };
237            stack_builder.add_resource(event_source_mapping);
238        };
239
240        let assume_role_statement = StatementBuilder::internal_new(vec!["sts:AssumeRole".to_string()], Effect::Allow)
241            .principal(PrincipalBuilder::new().service("lambda.amazonaws.com").build())
242            .build();
243        let assumed_role_policy_document = AssumeRolePolicyDocumentBuilder::new(vec![assume_role_statement]).build();
244        let managed_policy_arns = vec![join(
245            "",
246            vec![
247                Value::String("arn:".to_string()),
248                get_ref(AWS_PARTITION_PSEUDO_PARAM),
249                Value::String(":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole".to_string()),
250            ],
251        )];
252        let potentially_missing = find_missing_services(&self.aws_services_in_dependencies, &self.additional_policies);
253        let props = RolePropertiesBuilder::new(assumed_role_policy_document, managed_policy_arns)
254            .policies(self.additional_policies)
255            .build();
256
257        let role_id = Id::generate_id(&self.id, "Role");
258        let role_resource_id = Resource::generate_id("LambdaFunctionRole");
259        let role_ref = get_arn(&role_resource_id);
260        let role = RoleBuilder::new_with_info_on_missing(&role_id, &role_resource_id, props, potentially_missing).build(stack_builder);
261
262        let environment = if self.env_vars.is_empty() {
263            None
264        } else {
265            Some(Environment {
266                variables: self.env_vars.into_iter().collect(),
267            })
268        };
269
270        let log_group_id = Id::generate_id(&self.id, "LogGroup");
271        let log_group_name = self.function_name.clone().map(|fun_name| format!("/aws/lambda/{fun_name}"));
272        let base_builder = LogGroupBuilder::new(&log_group_id).log_group_retention(RetentionInDays(731));
273        let log_group = if let Some(name) = log_group_name {
274            base_builder.log_group_name_string(LogGroupName(name)).build(stack_builder)
275        } else {
276            base_builder.build(stack_builder)
277        };
278
279        let logging_info = LoggingInfo {
280            log_group: Some(get_ref(log_group.get_resource_id())),
281        };
282
283        let properties = LambdaFunctionProperties {
284            code: code.1,
285            architectures: vec![self.architecture.into()],
286            memory_size: self.memory,
287            timeout: self.timeout,
288            handler: self.handler,
289            runtime: self.runtime.map(Into::into),
290            role: role_ref,
291            function_name: self.function_name,
292            environment,
293            reserved_concurrent_executions: self.reserved_concurrent_executions,
294            logging_info,
295        };
296
297        stack_builder.add_resource(Function {
298            id: self.id.clone(),
299            resource_id: function_resource_id.clone(),
300            asset: code.0,
301            r#type: "AWS::Lambda::Function".to_string(),
302            properties,
303        });
304
305        let function = FunctionRef::new(self.id, function_resource_id);
306
307        (function, role, log_group)
308    }
309}
310
311// TODO does it make more sense to add runtime and handler to `new`? Other builders do this for required args
312impl FunctionBuilder<StartState> {
313    /// Creates a new Lambda function builder.
314    /// You will have to specify a handler and runtime before you are able to build a function.
315    ///
316    /// # Arguments
317    /// * `id` - Unique identifier for the function
318    /// * `architecture` - CPU architecture (x86_64 or ARM64)
319    /// * `memory` - Memory allocation in MB
320    /// * `timeout` - Maximum execution time
321    pub fn new(id: &str, architecture: Architecture, memory: Memory, timeout: Timeout) -> FunctionBuilder<StartState> {
322        FunctionBuilder {
323            state: Default::default(),
324            id: Id(id.to_string()),
325            architecture,
326            memory: memory.0,
327            timeout: timeout.0,
328            code: None,
329            handler: None,
330            runtime: None,
331            additional_policies: vec![],
332            aws_services_in_dependencies: vec![],
333            env_vars: vec![],
334            function_name: None,
335            sqs_event_source_mapping: None,
336            reserved_concurrent_executions: None,
337        }
338    }
339
340    pub fn code(self, code: Code) -> FunctionBuilder<CodeState> {
341        FunctionBuilder {
342            code: Some(code),
343            state: Default::default(),
344            id: self.id,
345            architecture: self.architecture,
346            memory: self.memory,
347            timeout: self.timeout,
348            handler: self.handler,
349            runtime: self.runtime,
350            additional_policies: self.additional_policies,
351            aws_services_in_dependencies: self.aws_services_in_dependencies,
352            env_vars: self.env_vars,
353            function_name: self.function_name,
354            sqs_event_source_mapping: self.sqs_event_source_mapping,
355            reserved_concurrent_executions: self.reserved_concurrent_executions,
356        }
357    }
358}
359
360impl FunctionBuilder<CodeState> {
361    pub fn handler<T: Into<String>>(self, handler: T) -> FunctionBuilder<ZipStateWithHandler> {
362        FunctionBuilder {
363            id: self.id,
364            handler: Some(handler.into()),
365            state: Default::default(),
366            architecture: self.architecture,
367            memory: self.memory,
368            timeout: self.timeout,
369            code: self.code,
370            runtime: self.runtime,
371            additional_policies: self.additional_policies,
372            aws_services_in_dependencies: self.aws_services_in_dependencies,
373            env_vars: self.env_vars,
374            function_name: self.function_name,
375            sqs_event_source_mapping: self.sqs_event_source_mapping,
376            reserved_concurrent_executions: self.reserved_concurrent_executions,
377        }
378    }
379}
380
381impl FunctionBuilder<ZipStateWithHandler> {
382    pub fn runtime(self, runtime: Runtime) -> FunctionBuilder<ZipStateWithHandlerAndRuntime> {
383        FunctionBuilder {
384            id: self.id,
385            runtime: Some(runtime),
386            state: Default::default(),
387            architecture: self.architecture,
388            memory: self.memory,
389            timeout: self.timeout,
390            code: self.code,
391            handler: self.handler,
392            additional_policies: self.additional_policies,
393            aws_services_in_dependencies: self.aws_services_in_dependencies,
394            env_vars: self.env_vars,
395            function_name: self.function_name,
396            sqs_event_source_mapping: self.sqs_event_source_mapping,
397            reserved_concurrent_executions: self.reserved_concurrent_executions,
398        }
399    }
400}
401
402impl FunctionBuilder<ZipStateWithHandlerAndRuntime> {
403    /// Configures the function to be triggered by an SQS queue.
404    ///
405    /// Automatically adds the necessary IAM permissions for reading from the queue.
406    pub fn sqs_event_source_mapping(
407        mut self,
408        sqs_queue: &QueueRef,
409        max_concurrency: Option<SqsEventSourceMaxConcurrency>,
410    ) -> FunctionBuilder<EventSourceMappingState> {
411        self.additional_policies.push(IamPermission::SqsRead(sqs_queue).into_policy());
412
413        let mapping = EventSourceMappingInfo {
414            id: sqs_queue.get_resource_id().to_string(),
415            max_concurrency: max_concurrency.map(|c| c.0),
416        };
417
418        FunctionBuilder {
419            id: self.id,
420            sqs_event_source_mapping: Some(mapping),
421            state: Default::default(),
422            runtime: self.runtime,
423            architecture: self.architecture,
424            memory: self.memory,
425            timeout: self.timeout,
426            code: self.code,
427            handler: self.handler,
428            additional_policies: self.additional_policies,
429            aws_services_in_dependencies: self.aws_services_in_dependencies,
430            env_vars: self.env_vars,
431            function_name: self.function_name,
432            reserved_concurrent_executions: self.reserved_concurrent_executions,
433        }
434    }
435
436    /// Builds the Lambda function and adds it to the stack.
437    ///
438    /// Creates the function along with its IAM execution role and CloudWatch log group.
439    /// Returns references to all three resources.
440    pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
441        self.build_internal(stack_builder)
442    }
443}
444
445impl FunctionBuilder<EventSourceMappingState> {
446    /// Builds the Lambda function and adds it to the stack.
447    ///
448    /// Creates the function along with its IAM execution role, CloudWatch log group, and event source mapping.
449    /// Returns references to the function, role, and log group.
450    pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
451        self.build_internal(stack_builder)
452    }
453}
454
455/// Builder for Lambda function permissions.
456///
457/// Creates permission resources that allow other AWS services to invoke a Lambda function.
458pub struct PermissionBuilder {
459    id: Id,
460    action: String,
461    function_name: Value,
462    principal: String,
463    source_arn: Option<Value>,
464    source_account: Option<Value>,
465}
466
467impl PermissionBuilder {
468    /// Creates a new permission builder for a Lambda function.
469    ///
470    /// # Arguments
471    /// * `id` - Unique identifier for the permission resource
472    /// * `action` - Lambda action to allow (e.g., "lambda:InvokeFunction")
473    /// * `function_name` - Reference to the Lambda function
474    /// * `principal` - AWS service or account that will be granted permission
475    pub fn new<R: Into<String>>(id: &str, action: LambdaPermissionAction, function_name: Value, principal: R) -> Self {
476        Self {
477            id: Id(id.to_string()),
478            action: action.0,
479            function_name,
480            principal: principal.into(),
481            source_arn: None,
482            source_account: None,
483        }
484    }
485
486    pub fn source_arn(self, arn: Value) -> Self {
487        Self {
488            source_arn: Some(arn),
489            ..self
490        }
491    }
492
493    pub fn current_account(self) -> Self {
494        Self {
495            source_account: Some(get_ref("AWS::AccountId")),
496            ..self
497        }
498    }
499
500    pub fn build(self, stack_builder: &mut StackBuilder) -> PermissionRef {
501        let permission_resource_id = Resource::generate_id("LambdaPermission");
502
503        stack_builder.add_resource(Permission {
504            id: self.id.clone(),
505            resource_id: permission_resource_id.clone(),
506            r#type: "AWS::Lambda::Permission".to_string(),
507            properties: LambdaPermissionProperties {
508                action: self.action,
509                function_name: self.function_name,
510                principal: self.principal,
511                source_arn: self.source_arn,
512                source_account: self.source_account,
513            },
514        });
515
516        PermissionRef::new(self.id, permission_resource_id)
517    }
518}