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