@@ -57,23 +57,26 @@ struct Deployer {
5757 }
5858 }
5959
60- /// Checks whether a Lambda function with the given name already exists.
61- func functionExists(
60+ /// Looks up an existing Lambda function's configuration.
61+ ///
62+ /// Returns the function's current configuration, or `nil` if no function with the given name
63+ /// exists. The configuration is carried downstream so callers (e.g. the update path) can reuse
64+ /// it — for example to validate the execution role — without issuing a second `GetFunction` call.
65+ func existingFunctionConfiguration(
6266 _ functionName: String ,
6367 using lambdaClient: LambdaClient
64- ) async throws -> Bool {
68+ ) async throws -> FunctionConfiguration ? {
6569 do {
66- _ = try await lambdaClient. getFunction (
70+ return try await lambdaClient. getFunction (
6771 GetFunctionRequest ( functionName: functionName)
68- )
69- return true
72+ ) . configuration
7073 } catch {
7174 // If the error indicates the resource was not found, the function doesn't exist
7275 let errorDescription = " \( error) "
7376 if errorDescription. contains ( " ResourceNotFoundException " )
7477 || errorDescription. contains ( " Function not found " )
7578 {
76- return false
79+ return nil
7780 }
7881 throw DeployerErrors . awsAPIError (
7982 service: " Lambda " ,
@@ -656,13 +659,17 @@ struct Deployer {
656659 print ( " Deploying function ' \( functionName) ' to \( region. rawValue) ... " )
657660 }
658661
659- // Check if function already exists
662+ // Check if function already exists. The configuration is reused downstream (e.g. the
663+ // update path validates the execution role from it) to avoid a second GetFunction call.
660664 print ( " Checking if function ' \( functionName) ' exists... " )
661- let exists = try await functionExists ( functionName, using: lambdaClient)
662- let action = try determineDeploymentAction ( functionExists: exists, delete: configuration. delete)
665+ let existingConfiguration = try await existingFunctionConfiguration ( functionName, using: lambdaClient)
666+ let action = try determineDeploymentAction (
667+ functionExists: existingConfiguration != nil ,
668+ delete: configuration. delete
669+ )
663670
664671 if configuration. verboseLogging {
665- print ( " [verbose] Function ' \( functionName) ' exists: \( exists ) , action: \( action) " )
672+ print ( " [verbose] Function ' \( functionName) ' exists: \( existingConfiguration != nil ) , action: \( action) " )
666673 }
667674
668675 switch action {
@@ -780,6 +787,17 @@ struct Deployer {
780787 }
781788 functionArn = response. functionArn
782789 } else {
790+ // Verify the function's execution role still exists before updating.
791+ // Lambda validates the role lazily (at invoke time), so an update against a
792+ // function whose role was deleted would succeed here but fail at invoke.
793+ // Reuse the configuration fetched during the existence check above.
794+ try await verifyExecutionRoleExists (
795+ roleARN: existingConfiguration? . role,
796+ functionName: functionName,
797+ using: iamClient,
798+ verbose: configuration. verboseLogging
799+ )
800+
783801 // Update the function code
784802 print ( " Updating Lambda function ' \( functionName) '... " )
785803 let response : UpdateFunctionCodeResponse
@@ -908,6 +926,62 @@ struct Deployer {
908926 " swift-lambda- \( functionName) -role "
909927 }
910928
929+ /// Extracts the role name from an IAM role ARN.
930+ /// e.g. `arn:aws:iam::123456789012:role/my-role` -> `my-role`,
931+ /// `arn:aws:iam::123456789012:role/path/my-role` -> `my-role`.
932+ /// Returns `nil` if the ARN does not contain a role name.
933+ static func roleName( fromARN arn: String ) -> String ? {
934+ guard let slashIndex = arn. lastIndex ( of: " / " ) else { return nil }
935+ let name = arn [ arn. index ( after: slashIndex) ... ]
936+ return name. isEmpty ? nil : String ( name)
937+ }
938+
939+ /// Verifies that the IAM role referenced by a function's execution role ARN still exists.
940+ ///
941+ /// Lambda only validates that an execution role is assumable lazily, at invoke time, not when
942+ /// the function is created or updated. If the role was deleted (for example by a previous
943+ /// `--delete` run), an update would silently succeed but the function would fail to invoke with
944+ /// `The role defined for the function cannot be assumed by Lambda`. This check surfaces the
945+ /// problem at deploy time instead.
946+ ///
947+ /// - Parameters:
948+ /// - roleARN: The execution role ARN configured on the function, if any.
949+ /// - functionName: The function name, used for error reporting.
950+ /// - iamClient: The IAM client to use for the lookup.
951+ /// - verbose: Whether to emit verbose progress output.
952+ /// - Throws: `DeployerErrors.executionRoleMissing` if the role does not exist.
953+ func verifyExecutionRoleExists(
954+ roleARN: String ? ,
955+ functionName: String ,
956+ using iamClient: IAMClient ,
957+ verbose: Bool
958+ ) async throws {
959+ guard let roleARN, let roleName = Self . roleName ( fromARN: roleARN) else {
960+ // No role ARN to verify (or an unparsable ARN) — nothing to check.
961+ return
962+ }
963+
964+ if verbose {
965+ print ( " [verbose] Verifying execution role ' \( roleName) ' still exists... " )
966+ }
967+
968+ do {
969+ _ = try await iamClient. getRole ( IAMGetRoleRequest ( roleName: roleName) )
970+ if verbose {
971+ print ( " [verbose] Execution role ' \( roleName) ' exists " )
972+ }
973+ } catch {
974+ if " \( error) " . contains ( " NoSuchEntity " ) {
975+ throw DeployerErrors . executionRoleMissing ( functionName: functionName, role: roleARN)
976+ }
977+ throw DeployerErrors . awsAPIError (
978+ service: " IAM " ,
979+ operation: " GetRole " ,
980+ message: " failed to verify execution role ' \( roleName) ': \( error) "
981+ )
982+ }
983+ }
984+
911985 /// The trust policy document that allows Lambda to assume the role.
912986 private static let lambdaTrustPolicy = """
913987 { " Version " : " 2012-10-17 " , " Statement " :[{ " Effect " : " Allow " , " Principal " :{ " Service " : " lambda.amazonaws.com " }, " Action " : " sts:AssumeRole " }]}
@@ -1296,6 +1370,7 @@ enum DeployerErrors: Error, CustomStringConvertible {
12961370 case archiveNotFound( URL )
12971371 case functionURLCreationFailed( String )
12981372 case iamRoleCreationFailed( String )
1373+ case executionRoleMissing( functionName: String , role: String )
12991374 case missingProduct
13001375
13011376 var description : String {
@@ -1312,6 +1387,16 @@ enum DeployerErrors: Error, CustomStringConvertible {
13121387 return " failed to create Function URL: \( message) "
13131388 case . iamRoleCreationFailed( let message) :
13141389 return " failed to create IAM role: \( message) "
1390+ case . executionRoleMissing( let functionName, let role) :
1391+ return """
1392+ the execution role configured for function ' \( functionName) ' no longer exists in IAM:
1393+ \( role)
1394+ Lambda cannot assume a role that does not exist, so the function would fail to invoke.
1395+
1396+ Suggested action: delete the function and redeploy it so the role is recreated:
1397+ swift package --allow-network-connections all:443 lambda-deploy --delete
1398+ swift package --allow-network-connections all:443 lambda-deploy
1399+ """
13151400 case . missingProduct:
13161401 return " no product specified. Use --products or define an executable target in Package.swift. "
13171402 }
0 commit comments