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