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
104pub 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 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 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
326impl FunctionBuilder<StartState> {
328 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 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 pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
461 self.build_internal(stack_builder)
462 }
463}
464
465impl FunctionBuilder<EventSourceMappingState> {
466 pub fn build(self, stack_builder: &mut StackBuilder) -> (FunctionRef, RoleRef, LogGroupRef) {
471 self.build_internal(stack_builder)
472 }
473}
474
475pub 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 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}