diff --git a/.gitignore b/.gitignore index 525460d96..efe110be0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ Package.resolved .vscode .devcontainer .amazonq -.kiro nodejs .ash \ No newline at end of file diff --git a/.kiro/hooks/swift-syntax-check.kiro.hook b/.kiro/hooks/swift-syntax-check.kiro.hook new file mode 100644 index 000000000..906306646 --- /dev/null +++ b/.kiro/hooks/swift-syntax-check.kiro.hook @@ -0,0 +1,18 @@ +{ + "enabled": true, + "name": "Swift Syntax Checker", + "description": "Automatically runs Swift syntax checking in the background whenever a Swift file is saved using the check-format.sh script. You don't need to fix the issues reported. The script does it automatically. Just run the script and stay silent.", + "version": "1", + "when": { + "type": "userTriggered", + "patterns": [ + "**/*.swift", + "Sources/", + "Tests/" + ] + }, + "then": { + "type": "askAgent", + "prompt": "A Swift file has been saved. Please run the syntax check script located at scripts/check-format.sh to validate the Swift code formatting and syntax. Report any issues found or confirm that the syntax is correct." + } +} \ No newline at end of file diff --git a/.kiro/settings/lsp.json b/.kiro/settings/lsp.json new file mode 100644 index 000000000..bc0c5a563 --- /dev/null +++ b/.kiro/settings/lsp.json @@ -0,0 +1,198 @@ +{ + "languages": { + "cpp": { + "name": "clangd", + "command": "clangd", + "args": [ + "--background-index" + ], + "file_extensions": [ + "cpp", + "cc", + "cxx", + "c", + "h", + "hpp", + "hxx" + ], + "project_patterns": [ + "CMakeLists.txt", + "compile_commands.json", + "Makefile" + ], + "exclude_patterns": [ + "**/build/**", + "**/cmake-build-**/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "typescript": { + "name": "typescript-language-server", + "command": "typescript-language-server", + "args": [ + "--stdio" + ], + "file_extensions": [ + "ts", + "js", + "tsx", + "jsx" + ], + "project_patterns": [ + "package.json", + "tsconfig.json" + ], + "exclude_patterns": [ + "**/node_modules/**", + "**/dist/**" + ], + "multi_workspace": false, + "initialization_options": { + "preferences": { + "disableSuggestions": false + } + }, + "request_timeout_secs": 60 + }, + "go": { + "name": "gopls", + "command": "gopls", + "args": [], + "file_extensions": [ + "go" + ], + "project_patterns": [ + "go.mod", + "go.sum" + ], + "exclude_patterns": [ + "**/vendor/**" + ], + "multi_workspace": false, + "initialization_options": { + "usePlaceholders": true, + "completeUnimported": true + }, + "request_timeout_secs": 60 + }, + "rust": { + "name": "rust-analyzer", + "command": "rust-analyzer", + "args": [], + "file_extensions": [ + "rs" + ], + "project_patterns": [ + "Cargo.toml" + ], + "exclude_patterns": [ + "**/target/**" + ], + "multi_workspace": false, + "initialization_options": { + "cargo": { + "buildScripts": { + "enable": true + } + }, + "diagnostics": { + "enable": true, + "enableExperimental": true + }, + "workspace": { + "symbol": { + "search": { + "scope": "workspace" + } + } + } + }, + "request_timeout_secs": 60 + }, + "python": { + "name": "pyright", + "command": "pyright-langserver", + "args": [ + "--stdio" + ], + "file_extensions": [ + "py" + ], + "project_patterns": [ + "pyproject.toml", + "setup.py", + "requirements.txt", + "pyrightconfig.json" + ], + "exclude_patterns": [ + "**/__pycache__/**", + "**/venv/**", + "**/.venv/**", + "**/.pytest_cache/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "ruby": { + "name": "solargraph", + "command": "solargraph", + "args": [ + "stdio" + ], + "file_extensions": [ + "rb" + ], + "project_patterns": [ + "Gemfile", + "Rakefile" + ], + "exclude_patterns": [ + "**/vendor/**", + "**/tmp/**" + ], + "multi_workspace": false, + "initialization_options": {}, + "request_timeout_secs": 60 + }, + "java": { + "name": "jdtls", + "command": "jdtls", + "args": [], + "file_extensions": [ + "java" + ], + "project_patterns": [ + "pom.xml", + "build.gradle", + "build.gradle.kts", + ".project" + ], + "exclude_patterns": [ + "**/target/**", + "**/build/**", + "**/.gradle/**" + ], + "multi_workspace": false, + "initialization_options": { + "settings": { + "java": { + "compile": { + "nullAnalysis": { + "mode": "automatic" + } + }, + "configuration": { + "annotationProcessing": { + "enabled": true + } + } + } + } + }, + "request_timeout_secs": 60 + } + } +} \ No newline at end of file diff --git a/.kiro/specs/al2-deprecation-warning/.config.kiro b/.kiro/specs/al2-deprecation-warning/.config.kiro new file mode 100644 index 000000000..6188d24bb --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/.config.kiro @@ -0,0 +1 @@ +{"specId": "fbbaa416-0f18-4730-9125-3159b272b477", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/al2-deprecation-warning/design.md b/.kiro/specs/al2-deprecation-warning/design.md new file mode 100644 index 000000000..d8d3bf125 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/design.md @@ -0,0 +1,116 @@ +# Design Document + +## Overview + +This design describes the changes to the `AWSLambdaPackager` plugin to revert the default Docker base image to Amazon Linux 2 and introduce a prominent deprecation warning when AL2 is used. The warning informs developers about the EOL status and guides them to migrate to AL2023. + +The changes are confined to a single file (`Plugins/AWSLambdaPackager/Plugin.swift`) plus documentation updates. No new modules or dependencies are introduced. + +## Architecture + +### Flow Diagram + +``` +performCommand() + │ + ├── Parse configuration (baseDockerImage defaults to amazonlinux2) + │ + ├── if isAmazonLinux2() (native on AL2) + │ ├── print deprecation warning + │ └── proceed with native build + │ + ├── if isAmazonLinux2023() (native on AL2023) + │ └── proceed with native build (no warning) + │ + └── else (not on Amazon Linux → Docker build) + ├── if baseDockerImage contains "amazonlinux2" but NOT "amazonlinux2023" + │ └── print deprecation warning + └── proceed with Docker build +``` + +## Components and Interfaces + +### Modified Components + +| Component | File | Change | +|-----------|------|--------| +| `Configuration` struct | `Plugin.swift` | Revert `baseDockerImage` to always use `amazonlinux2` | +| `AWSLambdaPackager.performCommand()` | `Plugin.swift` | Add deprecation warning logic before build | +| `AWSLambdaPackager.isAmazonLinux()` | `Plugin.swift` | Split into `isAmazonLinux2()` and `isAmazonLinux2023()` | +| `AWSLambdaPackager.displayHelpMessage()` | `Plugin.swift` | Update default image text and add migration note | +| Quick-setup docs | `quick-setup.md` | Add migration note | +| Readme | `readme.md` | Add migration note | + +### New Methods + +```swift +/// Prints a prominent deprecation warning about Amazon Linux 2 EOL. +private func displayDeprecationWarning() + +/// Returns true if running natively on Amazon Linux 2 (not AL2023). +private func isAmazonLinux2() -> Bool + +/// Returns true if running natively on Amazon Linux 2023. +private func isAmazonLinux2023() -> Bool +``` + +### Interfaces + +No public API changes. All modifications are internal to the plugin. + +## Testing Strategy + +Since this is a SwiftPM plugin, automated unit testing is limited. Verification approach: + +1. **macOS Docker build (AL2 default)** — Run `swift package --allow-network-connections docker archive` and verify the deprecation warning appears, then the build completes +2. **macOS Docker build (AL2023 explicit)** — Run with `--base-docker-image swift:6.3-amazonlinux2023` and verify no warning appears +3. **Native on AL2** — Run inside an AL2 container and verify the warning appears, then the build completes +4. **Native on AL2023** — Run inside an AL2023 container and verify native build proceeds without warning +5. **Help message** — Run with `--help` and verify the updated text + +## Data Models + +### Configuration Change + +The `baseDockerImage` property computation reverts to the pre-7615923 logic: + +```swift +// Before (commit 7615923): version-based AL2/AL2023 selection +// After (this change): always amazonlinux2 +self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" +``` + +No new data models or stored state are introduced. + +## Error Handling + +### Native Build on AL2 + +When the plugin detects it is running natively on Amazon Linux 2, it prints the deprecation warning and proceeds with the native build. No error is thrown — this is a non-blocking warning. + +### Docker Build with AL2 Image + +When building via Docker with an AL2 image, the warning is printed and the build continues normally. No error is thrown — this is a non-blocking warning. + +### AL2023 (Native or Docker) + +No warning, no error. Build proceeds as normal. + +## Correctness Properties + +### Property 1: Warning Uniqueness +When multiple products are built in a single invocation, the deprecation warning is printed exactly once before the first build starts, not repeated per product. +**Validates: Requirements 2.9** + +### Property 2: AL2 vs AL2023 Detection +The string matching uses `hasPrefix("Amazon Linux release 2")` with a negative check for `hasPrefix("Amazon Linux release 2023")` to correctly distinguish AL2 from AL2023. +**Validates: Requirements 3.1** + +### Property 3: Docker Image String Matching +Uses `contains("amazonlinux2")` with negative `contains("amazonlinux2023")` to handle various image name formats (e.g., `swift:6.3-amazonlinux2`, `swift:amazonlinux2`). +**Validates: Requirements 2.1** + +### Property 4: Backward Compatibility +The `--base-docker-image` flag still allows developers to specify any image, bypassing the default entirely. +**Validates: Requirements 1.3** diff --git a/.kiro/specs/al2-deprecation-warning/requirements.md b/.kiro/specs/al2-deprecation-warning/requirements.md new file mode 100644 index 000000000..ef02eac18 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/requirements.md @@ -0,0 +1,92 @@ +# Requirements Document + +## Introduction + +The AWSLambdaPackager plugin defaults to Amazon Linux 2 as the base Docker image for building Lambda functions. An initial proposal (commit 7615923) switched the default to Amazon Linux 2023 for Swift >= 6.3, but community review identified that silently changing the build platform could cause deployment failures due to glibc and OpenSSL version differences between AL2 and AL2023. Binaries built for AL2023 may not run correctly when deployed to a `provided.al2` Lambda runtime. + +Instead of changing the default, this feature keeps Amazon Linux 2 as the default build platform and introduces a prominent deprecation warning informing developers that Amazon Linux 2 reached End of Life in June 2025. The warning directs developers to explicitly opt in to Amazon Linux 2023 by re-issuing the build command with `--base-docker-image swift:6.3-amazonlinux2023`. Additionally, developers who switch to building on AL2023 must be informed that they also need to update their Lambda deployment to use the `provided.al2023` runtime instead of `provided.al2`. Amazon Linux 2023 will become the new default after June 30, 2025. + +## Glossary + +- **Packager_Plugin**: The `AWSLambdaPackager` Swift Package Manager command plugin that builds and archives Lambda functions for deployment. +- **Base_Docker_Image**: The Docker image used as the build environment for cross-compiling Swift code to Amazon Linux. +- **AL2**: Amazon Linux 2, the Linux distribution that reached End of Life in June 2025. +- **AL2023**: Amazon Linux 2023, the successor Linux distribution for AWS Lambda deployments. +- **Deprecation_Warning**: A prominent multi-line message printed to standard output alerting developers that AL2 is deprecated. +- **Native_Build**: A build performed directly on the host machine (without Docker) when the host is already running Amazon Linux. +- **Docker_Build**: A build performed inside a Docker container using the specified base image. + +## Requirements + +### Requirement 1: Default Base Docker Image + +**User Story:** As a developer, I want the packager plugin to default to Amazon Linux 2 as the base Docker image, so that existing build workflows continue to work without changes until the migration deadline. + +**Implementation Note:** Revert the base Docker image logic to the code before commit 7615923 (i.e., always use `amazonlinux2` regardless of Swift version). + +#### Acceptance Criteria + +1. WHEN no `--base-docker-image` option is provided and no `--swift-version` option is provided, THE Packager_Plugin SHALL use `swift:amazonlinux2` as the Base_Docker_Image. +2. WHEN no `--base-docker-image` option is provided and a `--swift-version` option is provided with a value matching the pattern ``, `.`, or `..` where each component is a non-negative integer, THE Packager_Plugin SHALL use `swift:-amazonlinux2` as the Base_Docker_Image, where `` is the exact string provided by the user. +3. WHEN a `--base-docker-image` option is provided and no `--swift-version` option is provided, THE Packager_Plugin SHALL use the user-specified image as the Base_Docker_Image. +4. IF both `--base-docker-image` and `--swift-version` options are provided, THEN THE Packager_Plugin SHALL reject the command with an error message indicating that `--swift-version` and `--base-docker-image` are mutually exclusive. + +### Requirement 2: Deprecation Warning Display in Docker Build + +**User Story:** As a developer building with Docker, I want to see a prominent deprecation warning when AL2 is used, so that I am informed about the upcoming migration requirement. + +#### Acceptance Criteria + +1. WHEN the Base_Docker_Image contains the string "amazonlinux2" and does not contain the string "amazonlinux2023", THE Packager_Plugin SHALL print the Deprecation_Warning before starting the Docker_Build. +2. THE Deprecation_Warning SHALL include a message stating that Amazon Linux 2 reached End of Life in June 2025. +3. THE Deprecation_Warning SHALL include a message stating that developers must migrate to Amazon Linux 2023. +4. THE Deprecation_Warning SHALL include a message stating that Amazon Linux 2023 will become the default after June 30, 2025. +5. THE Deprecation_Warning SHALL include the exact command option `--base-docker-image swift:6.3-amazonlinux2023` that developers can use to switch to AL2023 immediately. +6. THE Deprecation_Warning SHALL inform developers that when switching to AL2023, they must also update their Lambda deployment to use the `provided.al2023` runtime. +7. THE Deprecation_Warning SHALL include the URL `https://aws.amazon.com/amazon-linux-2` for reference. +8. THE Deprecation_Warning SHALL be visually prominent by using a separator line of at least 60 repeated characters above and below the warning text, with at least one empty line separating the warning block from surrounding build output. +8. WHEN the Deprecation_Warning has been printed, THE Packager_Plugin SHALL continue the Docker_Build to completion without halting or requiring user confirmation. +9. WHEN multiple products are built in a single invocation and the Base_Docker_Image triggers the deprecation condition, THE Packager_Plugin SHALL print the Deprecation_Warning exactly once before the first Docker_Build starts. + +### Requirement 3: Deprecation Warning Display in Native Build on AL2 + +**User Story:** As a developer running the packager natively on Amazon Linux 2, I want to see the deprecation warning, so that I am informed about the upcoming migration requirement. + +#### Acceptance Criteria + +1. WHEN the Packager_Plugin reads `/etc/system-release` and the content starts with "Amazon Linux release 2" but does not start with "Amazon Linux release 2023", THE Packager_Plugin SHALL print the Deprecation_Warning to standard output. +2. WHEN the Packager_Plugin detects it is running natively on AL2, THE Packager_Plugin SHALL continue with the Native_Build after printing the Deprecation_Warning. +3. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL instruct the developer to switch to an Amazon Linux 2023 environment. +4. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL include the URL `https://aws.amazon.com/amazon-linux-2` for reference. +5. WHEN the Packager_Plugin detects it is running natively on AL2, THE Deprecation_Warning SHALL be visually prominent by using separator lines and whitespace to distinguish it from other build output. + +### Requirement 4: Normal Build on AL2023 + +**User Story:** As a developer who has already migrated to Amazon Linux 2023, I want the build to proceed normally without any deprecation warning, so that my workflow is not interrupted. + +#### Acceptance Criteria + +1. WHEN the Base_Docker_Image contains the string "amazonlinux2023", THE Packager_Plugin SHALL execute the Docker_Build to completion and produce the expected archive artifacts without printing the Deprecation_Warning to standard output. +2. WHEN the Packager_Plugin reads the file `/etc/system-release` and its content contains the string "Amazon Linux 2023", THE Packager_Plugin SHALL execute the Native_Build to completion and produce the expected archive artifacts without printing the Deprecation_Warning to standard output. +3. IF the Base_Docker_Image contains the string "amazonlinux2023", THEN THE Packager_Plugin SHALL NOT print any message referencing Amazon Linux 2 End of Life or migration to standard output. + +### Requirement 5: Help Message Update + +**User Story:** As a developer, I want the help message to accurately reflect the current default base Docker image, so that I understand the plugin's behavior. + +#### Acceptance Criteria + +1. THE Packager_Plugin help message for the `--base-docker-image` option SHALL state that the default base Docker image is `swift:-amazonlinux2`. +2. THE Packager_Plugin help message for the `--base-docker-image` option SHALL include a note stating that Amazon Linux 2023 will become the default after June 30, 2025. +3. WHEN the user passes `--help` to the archive command, THE Packager_Plugin SHALL display the help message containing both the current default image name and the Amazon Linux 2023 transition note. + +### Requirement 6: Documentation Update + +**User Story:** As a developer reading the project documentation, I want the documentation to reflect the current default behavior and migration path, so that I can plan my migration. + +#### Acceptance Criteria + +1. THE quick-setup documentation SHALL show `swift:amazonlinux2` as the Docker image in the example build output of the archive step. +2. THE quick-setup documentation SHALL include a note adjacent to the archive step stating that Amazon Linux 2 is the current default build environment and that a future version will migrate to Amazon Linux 2023. +3. THE readme documentation SHALL use `provided.al2` as the value of the `--runtime` flag in the `aws lambda create-function` deployment command. +4. THE readme documentation SHALL include a note adjacent to the archive command documenting the `--base-docker-image swift:6.3-amazonlinux2023` flag as an option for developers who want to migrate to Amazon Linux 2023 early. diff --git a/.kiro/specs/al2-deprecation-warning/tasks.md b/.kiro/specs/al2-deprecation-warning/tasks.md new file mode 100644 index 000000000..d7153c604 --- /dev/null +++ b/.kiro/specs/al2-deprecation-warning/tasks.md @@ -0,0 +1,35 @@ +# Implementation Plan + +- [x] 1. Revert default base Docker image to amazonlinux2 + - In `Plugins/AWSLambdaPackager/Plugin.swift`, in the `Configuration` struct `init`, remove the `amazonLinuxVersion` variable and all version-parsing logic (the `if let version = swiftVersion { ... }` block) + - Replace the `baseDockerImage` assignment with the pre-7615923 single-line (`self.baseDockerImage = baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2"`) + - Remove the verbose logging block that prints swift version/amazon linux version/base docker image info + - _Requirements: 1.1, 1.2, 1.3, 1.4_ + +- [x] 2. Refine platform detection methods + - In `Plugins/AWSLambdaPackager/Plugin.swift`, replace the `isAmazonLinux()` method with `isAmazonLinux2()` (returns true if `/etc/system-release` starts with "Amazon Linux release 2" but NOT "Amazon Linux release 2023") + - Add `isAmazonLinux2023()` method (returns true if starts with "Amazon Linux release 2023") + - Update all call sites to use the new methods + - _Requirements: 3.1, 4.2_ + +- [x] 3. Add deprecation warning method + - Add a `private func displayDeprecationWarning()` method that prints the multi-line warning with EOL notice, migration instruction, `--base-docker-image swift:6.3-amazonlinux2023` option, `provided.al2023` runtime reminder, and `https://aws.amazon.com/amazon-linux-2` URL, surrounded by separator lines + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 3.2, 3.3, 3.4, 3.5_ + +- [x] 4. Update build flow in performCommand + - Replace the `if self.isAmazonLinux()` block with three-way branching — `isAmazonLinux2()` prints warning then proceeds with native build, `isAmazonLinux2023()` does native build without warning, else does Docker build with warning if image contains "amazonlinux2" but not "amazonlinux2023" + - Warning is printed exactly once before the first product build + - No errors are thrown — the build always continues + - _Requirements: 2.1, 2.8, 2.9, 3.1, 3.2, 4.1, 4.2, 4.3_ + +- [x] 5. Update help message + - In `displayHelpMessage()`, update the `--base-docker-image` description to show `(default: swift:-amazonlinux2)` and add a note that Amazon Linux 2023 will become the default after June 30, 2025 + - _Requirements: 5.1, 5.2, 5.3_ + +- [x] 6. Update quick-setup documentation + - In `Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md`, ensure archive example shows `swift:amazonlinux2`, add a note explaining AL2 is EOL and developers should migrate using `--base-docker-image swift:6.3-amazonlinux2023`, and mention that AL2023 deployments must use `provided.al2023` runtime + - _Requirements: 6.1, 6.2_ + +- [x] 7. Update readme documentation + - In `readme.md`, ensure deployment examples reference `provided.al2`, add a note about `--base-docker-image swift:6.3-amazonlinux2023` for early migration, and mention that AL2023 deployments must use `provided.al2023` runtime + - _Requirements: 6.3, 6.4_ diff --git a/.kiro/specs/lambda-response-stream-headers/design.md b/.kiro/specs/lambda-response-stream-headers/design.md new file mode 100644 index 000000000..db9f8c5b1 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/design.md @@ -0,0 +1,145 @@ +# Design Document + +## Overview + +This design implements HTTP header and status code support for the `LambdaResponseStreamWriter` protocol through a protocol extension. The solution creates a new response structure specifically for streaming scenarios and adds a method to send HTTP response metadata before streaming the body content. The implementation ensures proper separation between metadata and streaming data using a null byte delimiter. + +## Architecture + +The feature consists of two main components: + +1. **StreamingLambdaResponse Structure**: A new `Codable` and `Sendable` struct that represents HTTP response metadata without body content +2. **LambdaResponseStreamWriter Extension**: A protocol extension that adds the `writeHeaders(_:)` method to send response metadata + +The architecture maintains compatibility with existing streaming functionality while adding the capability to send structured HTTP response metadata before streaming begins. + +## Components and Interfaces + +### StreamingLambdaResponse Structure + +```swift +public struct StreamingLambdaStatusAndHeadersResponse: Codable, Sendable { + public let statusCode: Int + public let headers: [String: String]? + + public init( + statusCode: Int, + headers: [String: String]? = nil, + ) +} +``` + +**Design Decisions:** +- All properties are public to allow full control over response metadata +- there is no `body` property +- Default values provided for optional parameters to simplify common use cases +- Follows standard AWS Lambda response format for consistency + +### LambdaResponseStreamWriter Extension + +```swift +extension LambdaResponseStreamWriter { + public func writeStatusAndHeaders(_ response: StreamingLambdaStatusAndHeadersResponse) async throws +} +``` + +**Method Behavior:** +1. Validates that `response.body` is nil, throwing an error if not +2. Serializes the response structure to JSON using `JSONEncoder` +3. Writes the JSON data to the stream using existing `write(_:)` method +4. Writes eight null bytes (0x00) as a separator +5. Propagates any errors from validation, serialization, or writing + +## Data Models + +### StreamingLambdaStatusAndHeadersResponse Properties + +- **statusCode**: HTTP status code (e.g., 200, 404, 500) +- **headers**: Dictionary of single-value HTTP headers +- **multiValueHeaders**: Dictionary of multi-value HTTP headers (e.g., Set-Cookie) + +### JSON Serialization Format + +The serialized JSON follows this structure: +```json +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Cache-Control": "no-cache" + }, + "multiValueHeaders": { + "Set-Cookie": ["session=abc123", "theme=dark"] + } +} +``` + +## Error Handling + +### Serialization Errors + +- **JSON Encoding Errors**: Propagated from `JSONEncoder.encode(_:)` +- **Write Errors**: Propagated from underlying `write(_:)` method calls + +### Error Propagation Strategy + +All errors are propagated to the caller without modification, maintaining consistency with existing protocol behavior. The method signature `async throws` ensures proper error handling integration. + +## Testing Strategy + +### Unit Tests + +1. **Successful Header Writing** + - Test with minimal response (status code only) + - Test with full response (all optional fields populated) + - Verify JSON serialization format + - Verify null byte separator is written + +2. **Validation Tests** + - Test error message content and type + +3. **Error Handling Tests** + - Test JSON serialization error propagation + - Test write method error propagation + +4. **Integration Tests** + - Test with existing streaming methods + - Test multiple header writes (should work) + - Test header write followed by body streaming + +### Mock Implementation + +Tests will use a mock `LambdaResponseStreamWriter` implementation that captures written data for verification: + +```swift +class MockLambdaResponseStreamWriter: LambdaResponseStreamWriter { + var writtenBuffers: [ByteBuffer] = [] + + func write(_ buffer: ByteBuffer) async throws { + writtenBuffers.append(buffer) + } + + func finish() async throws {} + func writeAndFinish(_ buffer: ByteBuffer) async throws {} +} +``` + +## Implementation Notes + +### File Organization + +- **File Location**: `Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift` +- **Imports**: FoundationEssentials if it can be imported, Foundation otherwise (for JSONEncoder), NIOCore (for ByteBuffer) +- **Structure**: Response struct definition followed by protocol extension + +### Performance Considerations + +- JSON serialization is performed once per header write +- Null byte separator uses minimal memory (8 bytes) +- No additional memory allocations beyond JSON encoding + +### Compatibility + +- Maintains full backward compatibility with existing `LambdaResponseStreamWriter` implementations +- Does not modify existing protocol methods +- Can be used alongside existing streaming methods without conflicts \ No newline at end of file diff --git a/.kiro/specs/lambda-response-stream-headers/requirements.md b/.kiro/specs/lambda-response-stream-headers/requirements.md new file mode 100644 index 000000000..e98959737 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/requirements.md @@ -0,0 +1,73 @@ +# Requirements Document + +## Introduction + +This feature adds HTTP header and status code support to the `LambdaResponseStreamWriter` protocol by creating an extension that allows sending HTTP response metadata before streaming the response body. This enhancement includes creating a new response structure specifically for streaming scenarios and enables Lambda functions using streaming responses to properly set HTTP status codes, headers, and multi-value headers before writing the response stream. + +## Requirements + +### Requirement 1 + +**User Story:** As a Lambda function developer using streaming responses, I want to send HTTP headers and status code before streaming the response body, so that I can properly control the HTTP response metadata. + +#### Acceptance Criteria + +1. WHEN a developer calls the new method on `LambdaResponseStreamWriter` THEN the system SHALL create a new response structure and serialize it to write to the stream +2. WHEN the method is called THEN the system SHALL accept parameters for status code, headers, multi-value headers, and base64 encoding flag +3. WHEN the method is called THEN the system SHALL be implemented as an extension in a separate file named `LambdaResponseStreamWriter+Headers.swift` +4. WHEN the method is called THEN the system SHALL use the existing `write(_:)` method to send the serialized response + +### Requirement 2 + +**User Story:** As a Lambda function developer, I want the new header method to integrate seamlessly with existing streaming functionality, so that I can use it alongside current streaming methods. + +#### Acceptance Criteria + +1. WHEN the new method is implemented THEN it SHALL be part of an extension to the existing `LambdaResponseStreamWriter` protocol +2. WHEN the method is called THEN it SHALL not interfere with existing `write(_:)`, `finish()`, and `writeAndFinish(_:)` methods +3. WHEN the method is called THEN it SHALL follow the same async/throws pattern as existing protocol methods +4. WHEN the method is called THEN it SHALL maintain compatibility with all existing `LambdaResponseStreamWriter` implementations + +### Requirement 3 + +**User Story:** As a Lambda function developer, I want a new response structure specifically designed for streaming scenarios, so that I can properly format HTTP response metadata without including body content. + +#### Acceptance Criteria + +1. WHEN the new response structure is created THEN it SHALL include properties for `isBase64Encoded`, `statusCode`, `headers`, `multiValueHeaders`, and `body` +2. WHEN the structure is defined THEN it SHALL follow the JSON format: `{"isBase64Encoded": true|false, "statusCode": httpStatusCode, "headers": {"headerName": "headerValue"}, "multiValueHeaders": {"headerName": ["headerValue1", "headerValue2"]}, "body": "..."}` +3. WHEN the structure is created THEN it SHALL be `Codable` and `Sendable` to support JSON serialization and concurrency +4. WHEN the structure is used THEN it SHALL be defined in the same file as the extension for organizational purposes + +### Requirement 4 + +**User Story:** As a Lambda function developer, I want the header response to be properly separated from the streaming data, so that the Lambda runtime can distinguish between metadata and body content. + +#### Acceptance Criteria + +1. WHEN the response structure is serialized and written THEN the system SHALL write a series of eight 0x00 characters immediately after to separate header content from user stream data +2. WHEN the separator is written THEN it SHALL serve as a delimiter between the JSON header response and the subsequent streaming body data +3. WHEN the method is called THEN the system SHALL ensure the separator is always written after the serialized response +4. WHEN streaming continues THEN user data written after the separator SHALL be treated as the response body + +### Requirement 5 + +**User Story:** As a Lambda function developer, I want to ensure the response structure is correct for streaming, so that the body content is only sent through the stream and not duplicated in the JSON response. + +#### Acceptance Criteria + +1. WHEN the response structure parameter is provided THEN the system SHALL validate that the `body` property is nil +2. WHEN the `body` property is not nil THEN the system SHALL throw an error indicating that body content must be streamed separately +3. WHEN validation passes THEN the system SHALL proceed with serialization of the response object +4. WHEN the error is thrown THEN it SHALL provide a clear message explaining that body content should be sent via streaming methods + +### Requirement 6 + +**User Story:** As a Lambda function developer, I want proper error handling when sending headers, so that serialization failures are properly communicated. + +#### Acceptance Criteria + +1. WHEN the response structure serialization fails THEN the system SHALL throw an appropriate error +2. WHEN the underlying `write(_:)` method fails THEN the system SHALL propagate the error to the caller +3. WHEN an error occurs THEN the system SHALL maintain the same error handling behavior as existing protocol methods +4. WHEN the method is called THEN it SHALL be marked as `async throws` to match the protocol's error handling pattern \ No newline at end of file diff --git a/.kiro/specs/lambda-response-stream-headers/tasks.md b/.kiro/specs/lambda-response-stream-headers/tasks.md new file mode 100644 index 000000000..eee130550 --- /dev/null +++ b/.kiro/specs/lambda-response-stream-headers/tasks.md @@ -0,0 +1,57 @@ +# Implementation Plan + +- [x] 1. Create the extension file and basic structure + - Create `Sources/AWSLambdaRuntime/LambdaResponseStreamWriter+Headers.swift` file + - Add appropriate imports for FoundationEssentials/Foundation and NIOCore + - Add file header with copyright and license information matching existing files + - _Requirements: 1.3, 3.4_ + +- [x] 2. Implement StreamingLambdaStatusAndHeadersResponse structure + - Define the `StreamingLambdaStatusAndHeadersResponse` struct with `Codable` and `Sendable` conformance + - Add `statusCode`, `headers`, and `multiValueHeaders` properties with correct types + - Implement public initializer with default values for optional parameters + - _Requirements: 3.1, 3.2, 3.3_ + +- [x] 3. Implement the writeStatusAndHeaders method + - Add `writeStatusAndHeaders(_:)` method to `LambdaResponseStreamWriter` extension + - Implement JSON serialization using `JSONEncoder` + - Write serialized JSON data using existing `write(_:)` method + - Write eight null bytes (0x00) as separator after JSON data + - Mark method as `async throws` for proper error handling + - _Requirements: 1.1, 1.4, 4.1, 4.2, 4.3, 6.1, 6.2, 6.4_ + +- [x] 4. Create unit tests for the new functionality + - Create test file `Tests/AWSLambdaRuntimeTests/LambdaResponseStreamWriter+HeadersTests.swift` + - Implement mock `LambdaResponseStreamWriter` for testing + - Write tests for successful header writing with minimal response (status code only) + - Write tests for successful header writing with full response (all fields populated) + - Verify JSON serialization format matches expected structure + - Verify null byte separator is written correctly after JSON data + - _Requirements: 1.1, 1.4, 4.1, 4.2, 4.3_ + +- [x] 5. Add error handling tests + - Write tests for JSON serialization error propagation + - Write tests for write method error propagation + - Verify error types and messages are properly handled + - _Requirements: 6.1, 6.2, 6.3_ + +- [x] 6. Add integration tests + - Test writeStatusAndHeaders method with existing streaming methods + - Test multiple header writes to ensure they work correctly + - Test header write followed by body streaming to verify compatibility + - Verify the method works with all existing `LambdaResponseStreamWriter` implementations + - _Requirements: 2.1, 2.2, 2.3, 2.4_ + +- [ ] 7. Update Examples/Streaming example + - Update `Examples/Streaming/Sources/main.swift` to demonstrate the new writeStatusAndHeaders functionality + - Add example usage showing how to set status code and headers before streaming response body + - Update `Examples/Streaming/README.md` to document the new header functionality + - Include code examples and explanation of the streaming response format + - _Requirements: 1.1, 1.2_ + +- [x] 8. Update top-level README documentation + - Update the streaming section in the main `readme.md` file + - Add documentation about the new writeStatusAndHeaders method + - Include example code showing how to use the new functionality + - Focus on user-facing API and benefits without exposing implementation details + - _Requirements: 1.1, 1.2_ \ No newline at end of file diff --git a/.kiro/specs/lambda-v2-plugins/.config.kiro b/.kiro/specs/lambda-v2-plugins/.config.kiro new file mode 100644 index 000000000..e36f7cf1c --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/.config.kiro @@ -0,0 +1 @@ +{"specId": "0116fa61-9ac9-44ac-bbc5-fc0802663a4b", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/lambda-v2-plugins/design.md b/.kiro/specs/lambda-v2-plugins/design.md new file mode 100644 index 000000000..129c9396e --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/design.md @@ -0,0 +1,609 @@ +# Design Document: Lambda V2 Plugins + +## Overview + +This design covers the v4 plugin system for `swift-aws-lambda-runtime`, delivering three SwiftPM command plugins (`lambda-init`, `lambda-build`, `lambda-deploy`) and a legacy `archive` passthrough. All functional logic lives in a single shared executable target (`AWSLambdaPluginHelper`) that dispatches to `Initializer`, `Builder`, or `Deployer` based on the first argument. The deploy command is the primary new implementation, leveraging Soto Core for AWS credential management, SigV4 signing, and HTTP transport, with generated service clients (Lambda, IAM, S3, STS) committed to the repository. + +Key design changes from the current implementation: +- Default base image moves from `amazonlinux2` to `amazonlinux2023` +- The blanket AL2 deprecation warning is removed; an informational warning is emitted only when AL2 is explicitly chosen +- `--container-cli` is replaced by a unified `--cross-compile` option accepting `docker`, `container`, `swift-static-sdk`, `custom-sdk` +- Default binary stripping with `-Xlinker -s` and `--no-strip` opt-out +- `--output-directory` is accepted as a deprecated alias for `--output-path` +- Vendored crypto/signer/HTTP code under `Vendored/` is removed in favor of `soto-core` +- The `archive` plugin is uncommented and shares sources with `AWSLambdaBuilder` + +## Architecture + +```mermaid +graph TD + subgraph "SwiftPM Plugins (thin wrappers)" + INIT["AWSLambdaInitializer
verb: lambda-init"] + BUILD["AWSLambdaBuilder
verb: lambda-build"] + DEPLOY["AWSLambdaDeployer
verb: lambda-deploy"] + ARCHIVE["AWSLambdaPackager
verb: archive (deprecated)"] + end + + subgraph "Shared Executable Target" + HELPER["AWSLambdaPluginHelper
argv[1] dispatch"] + INITIALIZER["Initializer"] + BUILDER["Builder"] + DEPLOYER["Deployer"] + end + + subgraph "Dependencies" + SOTO["SotoCore
(credentials, signing, HTTP)"] + NIO["SwiftNIO
(NIOCore, NIOHTTP1)"] + end + + subgraph "Generated Clients (committed)" + LAMBDA_CLIENT["LambdaClient"] + IAM_CLIENT["IAMClient"] + S3_CLIENT["S3Client"] + STS_CLIENT["STSClient"] + end + + subgraph "AWS" + LAMBDA_API["AWS Lambda"] + IAM_API["AWS IAM"] + S3_API["AWS S3"] + STS_API["AWS STS"] + end + + INIT -->|spawn| HELPER + BUILD -->|spawn| HELPER + DEPLOY -->|spawn| HELPER + ARCHIVE -->|spawn + deprecation warning| HELPER + + HELPER --> INITIALIZER + HELPER --> BUILDER + HELPER --> DEPLOYER + + DEPLOYER --> SOTO + DEPLOYER --> LAMBDA_CLIENT + DEPLOYER --> IAM_CLIENT + DEPLOYER --> S3_CLIENT + DEPLOYER --> STS_CLIENT + + BUILDER --> NIO + + LAMBDA_CLIENT --> LAMBDA_API + IAM_CLIENT --> IAM_API + S3_CLIENT --> S3_API + STS_CLIENT --> STS_API +``` + +### Plugin → Helper Communication + +Each plugin wrapper: +1. Resolves `PluginContext`-only values (tool paths, package ID/name/directory, products, configuration) +2. Spawns `AWSLambdaPluginHelper` as a subprocess with the command verb as `argv[1]` +3. Checks exit status: non-zero → `Diagnostics.error(...)` + immediate halt + +### Helper Dispatch Bug Fix + +The current `command(from:)` method has a guard `args.count > 2` which is too strict — the minimum valid invocation is `[binary_path, command]` (count == 2). The fix changes the guard to `args.count > 1` and reads `args[1]` for the command name. The remaining arguments (`Array(args.dropFirst(2))`) are passed to the subcommand handler. + +## Components and Interfaces + +### 1. Plugin Wrappers + +#### AWSLambdaInitializer (`Plugins/AWSLambdaInitializer/Plugin.swift`) + +Thin wrapper. Resolves `--dest-dir` from `context.package.directoryURL`. Passes `["init", "--dest-dir", path] + arguments` to the helper. + +#### AWSLambdaBuilder (`Plugins/AWSLambdaBuilder/Plugin.swift`) + +Resolves output path, products, configuration, tool paths (docker/zip). Passes `["build", ...resolved, ...user_args]` to the helper. + +#### AWSLambdaDeployer (`Plugins/AWSLambdaDeployer/Plugin.swift`) + +Resolves products list (to determine executable target name). Passes `["deploy", "--products", targetNames, ...user_args]` to the helper. + +#### AWSLambdaPackager (legacy `archive` alias) + +Implemented as a **separate plugin directory** (`Plugins/AWSLambdaPackager/Plugin.swift`) because SwiftPM does not allow two plugin targets to share the same source path (it rejects them with an "overlapping sources" error). The plugin has its own thin `Plugin.swift` that: +1. Emits a deprecation warning via `Diagnostics.warning("'archive' is deprecated. Please use 'swift package lambda-build' instead.")` +2. Spawns the `AWSLambdaPluginHelper` executable with `["build"] + arguments` (same delegation as `AWSLambdaBuilder`) +3. Checks exit status and reports a diagnostic error on failure + +This was validated at compile time — `swift package describe` confirms both `archive` (verb) and `lambda-build` (verb) resolve to valid, independent plugins without source overlap. + +### 2. Initializer (`Sources/AWSLambdaPluginHelper/lambda-init/`) + +**Existing implementation preserved.** Files: `Initializer.swift`, `Template.swift`. + +Behavior: Writes `Default_Template` or `URL_Template` (when `--with-url`) to `Sources/main.swift` in the destination directory. + +No changes required — current implementation satisfies all Requirement 1 criteria. + +### 3. Builder (`Sources/AWSLambdaPluginHelper/lambda-build/`) + +**Existing implementation preserved with minimal modifications.** + +#### Changes Required + +| Change | Requirement | Description | +|--------|------------|-------------| +| Default image | 6.1 | `baseDockerImage` default: `"swift:\(version)-amazonlinux2023"` (was `amazonlinux2`) | +| Remove blanket warning | 6.2 | Remove the unconditional `displayDeprecationWarning()` call for all AL2 scenarios | +| AL2 informational warning | 6.3 | Emit a shorter informational warning ONLY when `--base-docker-image` explicitly contains `amazonlinux2` (not `amazonlinux2023`) | +| Strip by default | 2.5 | Add `-Xlinker -s` to both native and Docker build commands | +| `--no-strip` opt-out | 2.6 | When flag present, omit the strip linker flags | +| Unified `--cross-compile` | 2.7 | Replace `--container-cli` with `--cross-compile` accepting `docker\|container\|swift-static-sdk\|custom-sdk` | +| Unsupported methods | 2.14 | `swift-static-sdk`/`custom-sdk` → error with SDK_Installation_Guide link | +| `--output-directory` alias | 7.5/7.6 | Accept as deprecated alias for `--output-path` | +| Container CLI validation | 2.11/2.12 | Check CLI exists; if not → error with download page URL | + +#### BuilderConfiguration Changes + +```swift +// New cross-compile enum replaces ContainerCLI +enum CrossCompileMethod: String { + case docker + case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" +} + +// In BuilderConfiguration.init: +// 1. Extract --cross-compile (default: "docker") +// 2. Extract --no-strip flag +// 3. Extract --output-directory as alias for --output-path +// 4. Default baseDockerImage → "swift:-amazonlinux2023" +``` + +### 4. Deployer (`Sources/AWSLambdaPluginHelper/lambda-deploy/`) + +**New implementation.** The existing stub is replaced. + +#### DeployerConfiguration + +```swift +struct DeployerConfiguration { + let help: Bool + let verboseLogging: Bool + let withURL: Bool + let delete: Bool + let region: String? // nil → resolved by Soto + let iamRole: String? // nil → create new role + let inputDirectory: URL? // nil → default build output path + let architecture: Architecture // .host by default + let products: [String] // from plugin wrapper + + enum Architecture: String { + case x64, arm64 + static var host: Architecture { + #if arch(x86_64) + return .x64 + #else + return .arm64 + #endif + } + } +} +``` + +#### Deploy Orchestration + +```swift +struct Deployer { + func deploy(arguments: [String]) async throws { + let config = try DeployerConfiguration(arguments: arguments) + if config.help { displayHelpMessage(); return } + + // 1. Initialize Soto AWSClient (credential provider chain) + // 2. Warn if ~/.aws/config missing (non-blocking) + // 3. Resolve account ID via STS GetCallerIdentity + // 4. Determine function name from product/target + // 5. Check if function exists (GetFunction) + // 6. If --delete: teardown and return + // 7. Ensure IAM role exists + // 8. Upload code (direct or via S3 staging) + // 9. Create or update function + // 10. If --with-url: configure Function URL + // 11. Report deployment success: + // - Function ARN and region + // - If --with-url: Function URL + ready-to-use curl --aws-sigv4 command + // - If no URL: ready-to-use aws lambda invoke command + // 12. Shutdown AWSClient + } +} +``` + +### 5. Generated AWS Service Clients + +Located at: `Sources/AWSLambdaPluginHelper/GeneratedClients/` + +Structure: +``` +GeneratedClients/ +├── Lambda/ +│ ├── LambdaClient.swift +│ ├── LambdaShapes.swift +│ └── LambdaErrors.swift +├── IAM/ +│ ├── IAMClient.swift +│ ├── IAMShapes.swift +│ └── IAMErrors.swift +├── S3/ +│ ├── S3Client.swift +│ ├── S3Shapes.swift +│ └── S3Errors.swift +└── STS/ + ├── STSClient.swift + ├── STSShapes.swift + └── STSErrors.swift +``` + +Each client is a lightweight struct wrapping `AWSClient` from SotoCore: + +```swift +struct LambdaClient { + let client: AWSClient + let region: Region + + func getFunction(_ input: GetFunctionRequest) async throws -> GetFunctionResponse + func createFunction(_ input: CreateFunctionRequest) async throws -> CreateFunctionResponse + func updateFunctionCode(_ input: UpdateFunctionCodeRequest) async throws -> UpdateFunctionCodeResponse + func deleteFunction(_ input: DeleteFunctionRequest) async throws + func createFunctionUrlConfig(_ input: CreateFunctionUrlConfigRequest) async throws -> CreateFunctionUrlConfigResponse + func deleteFunctionUrlConfig(_ input: DeleteFunctionUrlConfigRequest) async throws + func addPermission(_ input: AddPermissionRequest) async throws -> AddPermissionResponse + func removePermission(_ input: RemovePermissionRequest) async throws +} +``` + +### 6. Generation Script + +Located at: `scripts/generate-aws-clients.sh` + +One-time, maintainer-run script that: +1. Clones Soto Code Generator +2. Downloads AWS service model JSON files for Lambda, IAM, S3, STS +3. Runs the generator with a config specifying only needed operations +4. Copies output to `Sources/AWSLambdaPluginHelper/GeneratedClients/` + +The script is NOT part of the build. If generated files are missing, the build fails with a compile error. + +## Data Models + +### Deploy State Machine + +```swift +enum DeploymentAction { + case create(CreateFunctionRequest) + case update(UpdateFunctionCodeRequest) + case delete(functionName: String) +} + +struct DeploymentContext { + let accountId: String + let region: Region + let functionName: String + let architecture: DeployerConfiguration.Architecture + let zipArchiveURL: URL + let zipArchiveSize: Int64 + let iamRoleARN: String + let action: DeploymentAction +} +``` + +### Bucket Name Construction + +```swift +/// Constructs the deployment bucket name per the naming convention. +/// Format: `swift-aws-lambda-runtime--` +static func deploymentBucketName(region: String, accountId: String) -> String { + "swift-aws-lambda-runtime-\(region)-\(accountId)" +} +``` + +### Archive Size Threshold + +```swift +/// AWS Lambda direct upload limit (50 MB compressed). +static let directUploadLimit: Int64 = 50 * 1024 * 1024 +``` + +### Cross-Compile Configuration + +```swift +enum CrossCompileMethod: String, CustomStringConvertible { + case docker + case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" + + var isSupported: Bool { + switch self { + case .docker, .container: return true + case .swiftStaticSdk, .customSdk: return false + } + } + + var description: String { rawValue } +} +``` + +## Deploy Orchestration Sequence + +```mermaid +sequenceDiagram + participant Dev as Developer + participant Plugin as AWSLambdaDeployer + participant Helper as Plugin Helper (deploy) + participant Soto as SotoCore / AWSClient + participant STS as AWS STS + participant IAM as AWS IAM + participant S3 as AWS S3 + participant Lambda as AWS Lambda + + Dev->>Plugin: swift package lambda-deploy --with-url + Plugin->>Helper: spawn ["deploy", "--products", "MyFunc", "--with-url"] + + Helper->>Soto: Initialize AWSClient (credential chain) + Note over Helper: Warn if ~/.aws/config absent (non-blocking) + + Helper->>STS: GetCallerIdentity + STS-->>Helper: account-id + + Helper->>Lambda: GetFunction("MyFunc") + alt Function does not exist + Helper->>IAM: CreateRole("swift-lambda-MyFunc-role") + IAM-->>Helper: role ARN + Helper->>IAM: AttachRolePolicy(AWSLambdaBasicExecutionRole) + Note over Helper: Wait for role propagation + + alt ZIP ≤ 50 MB + Helper->>Lambda: CreateFunction(ZipFile: base64) + else ZIP > 50 MB + Helper->>S3: HeadBucket("swift-aws-lambda-runtime--") + alt Bucket missing + Helper->>S3: CreateBucket + end + Helper->>S3: PutObject(zip) + Helper->>Lambda: CreateFunction(S3Bucket, S3Key) + Helper->>S3: DeleteObject(zip) + end + + Lambda-->>Helper: function ARN + + Helper->>Lambda: CreateFunctionUrlConfig(AuthType: AWS_IAM) + Lambda-->>Helper: Function URL + else Function exists + alt ZIP ≤ 50 MB + Helper->>Lambda: UpdateFunctionCode(ZipFile: base64) + else ZIP > 50 MB + Helper->>S3: stage via S3 (same as create path) + Helper->>Lambda: UpdateFunctionCode(S3Bucket, S3Key) + Helper->>S3: DeleteObject(zip) + end + end + + Note over Helper: Report: function ARN, region + alt --with-url used + Note over Helper: Report: Function URL + curl --aws-sigv4 command + else no URL + Note over Helper: Report: aws lambda invoke command + end + + Helper->>Soto: shutdown AWSClient + Helper-->>Plugin: exit 0 + Plugin-->>Dev: Function URL / invoke command +``` + +## Package.swift Changes + +```swift +// Add soto-core dependency +dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.99.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.5.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"), + .package(url: "https://github.com/soto-project/soto-core.git", from: "7.0.0"), // NEW +], + +// Update AWSLambdaPluginHelper dependencies +.executableTarget( + name: "AWSLambdaPluginHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SotoCore", package: "soto-core"), // NEW + ], + swiftSettings: defaultSwiftSettings +), + +// Uncomment archive alias plugin (separate directory, NOT shared path) +// SwiftPM rejects shared-path with "overlapping sources" error. +// Instead: Plugins/AWSLambdaPackager/Plugin.swift with its own thin wrapper. +.plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. (Deprecated: use lambda-build instead)" + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + // No `path:` — uses default Plugins/AWSLambdaPackager/ directory +), +``` + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: ZIP archive always contains bootstrap binary + +*For any* compiled product with any valid name, when the packaging step produces a ZIP archive, the archive SHALL contain a file named `bootstrap` (the renamed binary) regardless of the original product name. + +**Validates: Requirements 2.3** + +### Property 2: Deprecated option alias equivalence + +*For any* path value supplied via `--output-directory`, the resulting `BuilderConfiguration.outputDirectory` SHALL be identical to the value produced when the same path is supplied via `--output-path`. + +**Validates: Requirements 7.5, 7.6** + +### Property 3: Cross-compile method parsing round-trip + +*For any* valid `CrossCompileMethod` enum case, converting the case to its `rawValue` string and parsing it back SHALL produce the original enum case. + +**Validates: Requirements 2.7** + +### Property 4: Mutual exclusion of --swift-version and --base-docker-image + +*For any* non-empty swift-version string and any non-empty base-docker-image string supplied together, argument parsing SHALL throw a mutually-exclusive-argument error. + +**Validates: Requirements 2.17** + +### Property 5: Deployment bucket name construction + +*For any* valid AWS region string and any valid 12-digit account ID, `deploymentBucketName(region:accountId:)` SHALL produce the string `"swift-aws-lambda-runtime--"` and the result SHALL always be a valid S3 bucket name (lowercase, no uppercase, 3-63 chars). + +**Validates: Requirements 3.17, 3.18** + +### Property 6: Archive size determines upload strategy + +*For any* ZIP archive size, if the size is ≤ `directUploadLimit` (50 MB) then the deploy logic SHALL choose direct upload; if the size is > `directUploadLimit` then it SHALL choose S3 staging. + +**Validates: Requirements 3.15, 3.19** + +### Property 7: Non-zero helper exit causes plugin wrapper halt + +*For any* plugin wrapper (Initializer, Builder, Deployer) and any non-zero exit code from the helper subprocess, the wrapper SHALL emit a `Diagnostics.error` and SHALL NOT continue with further work. + +**Validates: Requirements 5.6** + +### Property 8: AL2 warning emitted only for explicit AL2 image selection + +*For any* base-docker-image value containing "amazonlinux2" but NOT "amazonlinux2023", the builder SHALL emit the AL2 informational deprecation warning. *For any* base-docker-image value containing "amazonlinux2023" or when using the default image, the builder SHALL NOT emit the AL2 warning. + +**Validates: Requirements 6.2, 6.3** + +### Property 9: Unsupported cross-compile methods report error with link + +*For any* cross-compile value in {`swift-static-sdk`, `custom-sdk`}, the builder SHALL report a "not yet supported" error that contains a URL to the SDK_Installation_Guide. + +**Validates: Requirements 2.14** + +### Property 10: Host architecture default + +*For any* host machine architecture (x64 or arm64), when `--architecture` is omitted, the deployer SHALL set the Lambda function architecture to match the host. + +**Validates: Requirements 3.13** + +## Error Handling + +### Error Categories and Responses + +| Category | Example | Response | +|----------|---------|----------| +| Argument validation | Mutually exclusive options | Print error, exit non-zero immediately | +| Missing tool | Docker/container CLI not found | Print error with download URL, exit non-zero | +| Unsupported method | `--cross-compile swift-static-sdk` | Print "not yet supported" with SDK guide link, exit non-zero | +| Build failure | Compilation error in container | Stream compiler output, exit non-zero | +| Product not found | Binary missing after build | Print expected path, exit non-zero | +| Credential failure | No AWS credentials resolved | Print descriptive error, exit non-zero | +| AWS API error | CreateFunction returns 4xx/5xx | Print AWS error message and code, exit non-zero | +| File I/O error | Cannot write template | Print OS error description, exit non-zero | +| Non-blocking warning | ~/.aws/config absent | Print informational warning, CONTINUE | +| Non-blocking warning | AL2 image explicitly chosen | Print deprecation notice, CONTINUE with build | + +### Error Propagation Strategy + +1. **Helper → Plugin**: Non-zero exit code. Plugin reads exit status and emits `Diagnostics.error(...)`. +2. **Within Helper**: Swift `throw` with typed errors (`BuilderErrors`, `DeployerErrors`). Top-level `main()` catches and prints, then calls `exit(1)`. +3. **AWS errors**: Soto throws `AWSClientError` or `AWSResponseError`; the deployer catches, formats the message (including request ID when available), and re-throws as `DeployerErrors.awsError(...)`. + +### Deployer-Specific Errors + +```swift +enum DeployerErrors: Error, CustomStringConvertible { + case credentialResolutionFailed(String) + case awsAPIError(service: String, operation: String, message: String) + case archiveNotFound(URL) + case invalidArchitecture(String) + case functionURLCreationFailed(String) + case iamRoleCreationFailed(String) + case missingProduct +} +``` + +## Testing Strategy + +### Unit Tests (Swift Testing framework) + +Located at: `Tests/AWSLambdaPluginHelperTests/` + +Test categories: +- **Argument parsing**: Verify `BuilderConfiguration` and `DeployerConfiguration` parse all options correctly, handle defaults, detect mutual exclusions, and map deprecated aliases +- **Cross-compile method parsing**: Round-trip enum ↔ string, invalid values +- **Bucket name construction**: Various region/account combinations, validate S3 naming rules +- **Archive size threshold**: Boundary values (exactly 50MB, 50MB+1, 0) +- **Host architecture detection**: Verify correct enum value on current platform +- **AL2 image detection**: Various image strings, boundary between AL2 and AL2023 + +### Property-Based Tests (SwiftCheck or swift-testing parameterized) + +Configuration: Minimum 100 iterations per property test. + +Each property test references its design document property via tag comment: +```swift +// Feature: lambda-v2-plugins, Property 2: Deprecated option alias equivalence +@Test(arguments: samplePaths) +func deprecatedAliasEquivalence(path: String) { ... } +``` + +Property-based testing library: Use Swift Testing's `@Test(arguments:)` with generated input collections for parameterized testing, since the input domains are finite and well-bounded (path strings, enum cases, size values). + +### End-to-End Test (Shell Script) + +Located at: `scripts/integration-test.sh` + +Flow: +```bash +#!/bin/bash +set -euo pipefail + +FUNCTION_NAME="swift-lambda-e2e-test-$(date +%s)" +CLEANUP_NEEDED=false + +cleanup() { + if [ "$CLEANUP_NEEDED" = true ]; then + swift package lambda-deploy --delete --products "$FUNCTION_NAME" || true + fi +} +trap cleanup EXIT + +# 1. Create temp directory, init swift package +# 2. swift package lambda-init --with-url +# 3. swift package --allow-network-connections docker lambda-build +# 4. swift package --allow-network-connections all lambda-deploy --with-url +CLEANUP_NEEDED=true +# 5. Extract Function URL from output +# 6. curl --aws-sigv4 "aws:amz::lambda" \ +# --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ +# -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ +# "$FUNCTION_URL?name=World" +# 7. Verify response contains expected message +# 8. swift package lambda-deploy --delete +CLEANUP_NEEDED=false +``` + +Key aspects: +- Uses `trap cleanup EXIT` to guarantee resource cleanup on failure (Req 9.10) +- Function URL uses `AWS_IAM` auth, validated via `curl --aws-sigv4` (Req 9.7, 9.8) +- Verifies response body matches expected output (Req 9.6, 9.11) diff --git a/.kiro/specs/lambda-v2-plugins/requirements.md b/.kiro/specs/lambda-v2-plugins/requirements.md new file mode 100644 index 000000000..a1d6900e4 --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/requirements.md @@ -0,0 +1,229 @@ +# Requirements Document + +## Introduction + +This feature delivers the v4 plugin system for `swift-aws-lambda-runtime`, replacing the legacy single-purpose `archive` plugin with three focused SwiftPM command plugins that cover the end-to-end developer experience: scaffolding (`lambda-init`), building and packaging (`lambda-build`), and deployment (`lambda-deploy`). The authoritative design source is the v4 proposal (`Plugins/Documentation.docc/Proposals/0001-v4-plugins.md`). + +The three plugins are thin `CommandPlugin` wrappers (`AWSLambdaInitializer`, `AWSLambdaBuilder`, `AWSLambdaDeployer`) that spawn a shared executable target (`AWSLambdaPluginHelper`) which implements the actual `init`, `build`, and `deploy` logic. The v4 proposal changes the deployment strategy to depend on Soto Core for AWS credential management, configuration parsing, and request signing, with AWS service clients generated one-time by the Soto Code Generator and checked into the repository. Vendored crypto, signer, and HTTP client code is removed. + +This release makes Amazon Linux 2023 the default Amazon Linux target and the default base container image (`swift:-amazonlinux2023`), with the associated default Lambda runtime `provided.al2023`. Amazon Linux 2 (AL2) is no longer the default but remains usable when the developer explicitly supplies an AL2 base image through the `--base-docker-image` option. The Amazon Linux 2 deprecation warning is removed. + +The project targets Swift 6 with Swift Package Manager, is developed on macOS 15 or later, and targets the Amazon Linux 2023 (`provided.al2023`) Lambda runtime. + +## Glossary + +- **Plugin_System**: The collective set of three SwiftPM command plugins (`AWSLambdaInitializer`, `AWSLambdaBuilder`, `AWSLambdaDeployer`) and the shared `AWSLambdaPluginHelper` executable target. +- **Plugin_Helper**: The `AWSLambdaPluginHelper` executable target that implements the `init`, `build`, and `deploy` commands and is invoked as a subprocess by each plugin wrapper. +- **Init_Plugin**: The `lambda-init` command (wrapper `AWSLambdaInitializer`) that scaffolds a new Lambda function source file. +- **Build_Plugin**: The `lambda-build` command (wrapper `AWSLambdaBuilder`) that compiles and packages a Lambda function into a deployable ZIP archive. +- **Deploy_Plugin**: The `lambda-deploy` command (wrapper `AWSLambdaDeployer`) that deploys a packaged Lambda function to AWS. +- **Template**: A hardcoded Swift source file scaffolded by the Init_Plugin into `Sources/main.swift`. +- **Default_Template**: The Template that defines a Lambda function receiving a JSON request and returning a JSON response. +- **URL_Template**: The Template that defines a Lambda function invoked through a Lambda Function URL using `FunctionURLRequest` and `FunctionURLResponse`. +- **ZIP_Archive**: The deployment package produced by the Build_Plugin containing the compiled binary renamed to `bootstrap` plus any `.resources` bundles. +- **Cross_Compilation_Method**: The mechanism used to compile a Lambda binary for Amazon Linux 2023 when the build does not run natively on Amazon_Linux_2023. Accepted values are `docker` (cross-compile using Docker), `container` (cross-compile using Apple's `container` CLI, a native OCI image runtime for macOS that does not require Docker Desktop), `swift-static-sdk` (the Swift Static Linux SDK built with musl), and `custom-sdk` (a custom Swift SDK for Amazon Linux). +- **SDK_Installation_Guide**: The published documentation website that explains how to install the Swift Static Linux SDK and the custom Swift SDK for Amazon Linux used by the `swift-static-sdk` and `custom-sdk` Cross_Compilation_Method values. +- **Amazon_Linux_2023**: The Amazon Linux 2023 operating system, used as the default cross-compilation target environment and corresponding to the `provided.al2023` Lambda runtime. +- **Base_Image**: The container image used for Docker-based cross-compilation, defaulting to `swift:-amazonlinux2023` and overridable by the developer through the `--base-docker-image` option. +- **Lambda_Runtime_Identifier**: The default AWS Lambda managed runtime identifier `provided.al2023` used when deploying functions. +- **Soto_Core**: The `soto-core` Swift package providing AWS credential management, configuration file parsing, SigV4 request signing, and HTTP client functionality. +- **Credential_Provider_Chain**: Soto Core's ordered credential resolution: environment variables (including the `AWS_PROFILE` environment variable), AWS configuration files (`~/.aws/credentials` and `~/.aws/config`), ECS container credentials, and EC2 instance metadata service (IMDSv2). Soto Core resolves the active region from the same environment variables and AWS profile configuration. +- **AWS_Configuration_Resolution**: Soto_Core's standard resolution of the AWS region, credentials, and active profile from the environment variables (`AWS_REGION`, `AWS_DEFAULT_REGION`, and `AWS_PROFILE`) and the AWS configuration files (`~/.aws/config` and `~/.aws/credentials`), used instead of any plugin-specific configuration parsing. +- **AWS_Configuration_Files**: The `~/.aws/config` and `~/.aws/credentials` files created by running the AWS CLI `aws configure` command, which store the developer's AWS credentials and default region and are consumed by AWS_Configuration_Resolution and the Credential_Provider_Chain. +- **Generated_Service_Client**: AWS service API client code (Lambda, IAM, S3, and STS) produced one time by the maintainer-run Generation_Script using the Soto Code Generator and committed to the repository, not generated at build time. +- **Generation_Script**: A maintainer-invoked script that runs the Soto Code Generator one time to produce the Generated_Service_Client source code, which is then committed to the repository. The Generation_Script is a manual operation and is not part of the package build. +- **Deployment_Bucket**: The dedicated, reusable Amazon S3 bucket named `swift-aws-lambda-runtime--` used to stage large deployment ZIP_Archive uploads, where `` is the deployment region and `` is the AWS_Account_Identifier. The Deployment_Bucket is created when absent and reused when present. +- **AWS_Account_Identifier**: The AWS account number, resolved through the AWS STS `GetCallerIdentity` API, used as part of the Deployment_Bucket name. +- **Function_Name**: The Lambda function name, derived from the `executableTarget` name defined in `Package.swift`. +- **Function_URL**: An HTTPS endpoint that invokes a Lambda function directly, configured through the AWS Lambda `CreateFunctionUrlConfig` API. The Function_URL uses IAM (`AWS_IAM`) authentication, so callers must sign their requests with AWS Signature Version 4 rather than calling the endpoint anonymously. +- **IAM_Role**: The AWS Identity and Access Management role assumed by the deployed Lambda function during execution. +- **Host_Architecture**: The CPU architecture of the machine on which the Deploy_Plugin runs, expressed as `x64` or `arm64`. +- **Legacy_Archive_Plugin**: The existing `archive` SwiftPM command plugin (verb `archive`) provided before the v4 plugin system, which this release retains as a passthrough to the Build_Plugin. +- **Deprecated_Option_Alias**: A Legacy_Archive_Plugin option name (such as `--output-directory`) that the Build_Plugin still accepts and maps to its current replacement option name (such as `--output-path`), supported for backward compatibility but not the primary documented interface. +- **End_To_End_Test_Suite**: The automated test, implemented as a shell script, that scaffolds, builds, deploys, validates, and deletes a Lambda function to validate the full plugin lifecycle end to end against AWS. The suite uses the URL function variant (the URL_Template) and validates the deployed function by sending an AWS Signature Version 4 signed `curl` request to its IAM-protected Function_URL. + +## Requirements + +### Requirement 1: Scaffold a new Lambda function (lambda-init) + +**User Story:** As a Swift developer new to AWS Lambda, I want to scaffold a ready-to-build Lambda function from a template, so that I can start a new project without writing boilerplate by hand. + +#### Acceptance Criteria + +1. WHEN the Init_Plugin is invoked without a template option, THE Plugin_Helper SHALL write the Default_Template to `Sources/main.swift`. +2. WHEN the Init_Plugin is invoked with the `--with-url` option, THE Plugin_Helper SHALL write the URL_Template to `Sources/main.swift`. +3. THE Default_Template SHALL define a Lambda function that decodes a JSON request into a `Decodable` type and returns an `Encodable` JSON response. +4. THE URL_Template SHALL define a Lambda function that accepts a `FunctionURLRequest` and returns a `FunctionURLResponse`. +5. WHERE the `--allow-writing-to-package-directory` option is provided, THE Init_Plugin SHALL write to the package directory without requesting interactive permission. +6. WHEN the Init_Plugin is invoked with the `--verbose` option, THE Plugin_Helper SHALL emit the destination path of the created file. +7. WHEN the Init_Plugin is invoked with the `--help` option, THE Plugin_Helper SHALL display usage information and SHALL NOT write any file. +8. IF writing the Template to `Sources/main.swift` fails, THEN THE Plugin_Helper SHALL report a descriptive error identifying the failure. +9. WHEN the Init_Plugin completes successfully, THE Plugin_Helper SHALL report the path of the created file and the next command to package the function. + +### Requirement 2: Build and package the Lambda function (lambda-build) + +**User Story:** As a Lambda developer, I want to compile my function for Amazon Linux 2023 and package it as a deployment ZIP, so that I can upload an artifact that runs on AWS Lambda. + +#### Acceptance Criteria + +1. WHEN the Build_Plugin is invoked on Amazon_Linux_2023, THE Plugin_Helper SHALL compile the configured products natively using `swift build` with the `--static-swift-stdlib` flag. +2. WHILE running on a platform other than Amazon_Linux_2023, THE Plugin_Helper SHALL compile the configured products using the selected Cross_Compilation_Method. +3. WHEN packaging a compiled product, THE Plugin_Helper SHALL produce a ZIP_Archive containing the product binary renamed to `bootstrap`. +4. WHEN packaging a compiled product that has associated `.resources` bundles, THE Plugin_Helper SHALL include each `.resources` bundle in the ZIP_Archive. +5. WHEN compiling a product, THE Plugin_Helper SHALL pass the linker flags `-Xlinker -s` to strip debug symbols from the binary. +6. WHERE the `--no-strip` option is provided, THE Plugin_Helper SHALL compile the product without the debug-symbol-stripping linker flags. +7. THE Build_Plugin SHALL accept a `--cross-compile` option that selects the Cross_Compilation_Method from the values `docker`, `container`, `swift-static-sdk`, and `custom-sdk`, defaulting to `docker`. +8. WHEN the `--cross-compile` option is omitted, THE Plugin_Helper SHALL use the `docker` Cross_Compilation_Method. +9. WHERE the Cross_Compilation_Method is `docker`, THE Plugin_Helper SHALL execute the compilation in a container started with Docker. +10. WHERE the Cross_Compilation_Method is `container`, THE Plugin_Helper SHALL execute the compilation in a container started with Apple's `container` CLI. +11. IF the Cross_Compilation_Method is `docker` and the Docker CLI cannot be located, THEN THE Plugin_Helper SHALL report that Docker is not installed and SHALL direct the user to the Docker download and installation page. +12. IF the Cross_Compilation_Method is `container` and Apple's `container` CLI cannot be located, THEN THE Plugin_Helper SHALL report that the `container` CLI is not installed and SHALL direct the user to the Apple `container` CLI download and installation page. +13. THE Plugin_Helper SHALL support the `docker` and `container` Cross_Compilation_Method values. +14. WHEN the `--cross-compile` option selects `swift-static-sdk` or `custom-sdk`, THE Plugin_Helper SHALL report that the selected Cross_Compilation_Method is not yet supported and SHALL direct the user to the SDK_Installation_Guide describing how to install the corresponding SDK. +15. WHEN the Cross_Compilation_Method is `container`, THE Plugin_Helper SHALL pull the Base_Image and run the build using the image-pull and run command forms appropriate to the Apple `container` CLI. +16. WHEN the `--base-docker-image` option is omitted, THE Plugin_Helper SHALL use `swift:-amazonlinux2023` as the Base_Image. +17. IF both the `--swift-version` option and the `--base-docker-image` option are provided, THEN THE Plugin_Helper SHALL report a mutually-exclusive-argument error. +18. WHEN the `--disable-docker-image-update` option is omitted and the Cross_Compilation_Method is `docker` or `container`, THE Plugin_Helper SHALL pull the Base_Image before compiling. +19. WHERE the `--disable-docker-image-update` option is provided, THE Plugin_Helper SHALL compile without pulling the Base_Image. +20. THE Build_Plugin SHALL accept the options `--output-path`, `--products`, `--configuration`, `--swift-version`, `--verbose`, and `--help` with the same meaning as the existing `archive` plugin. +21. WHEN the `--configuration` option is omitted, THE Plugin_Helper SHALL build using the `release` configuration. +22. IF a configured product binary is absent after compilation, THEN THE Plugin_Helper SHALL report that the product executable was not found. +23. WHEN the Build_Plugin completes successfully, THE Plugin_Helper SHALL report the count of created archives and the path of each ZIP_Archive. + +### Requirement 3: Deploy the Lambda function to AWS (lambda-deploy) + +**User Story:** As a Lambda developer, I want to deploy my packaged function to AWS from the command line, so that I can run my function in the cloud without configuring separate deployment tooling. + +#### Acceptance Criteria + +1. WHEN the Deploy_Plugin is invoked and no Lambda function with the Function_Name exists, THE Plugin_Helper SHALL create the function using the AWS Lambda `CreateFunction` API with the `provided.al2023` Lambda_Runtime_Identifier. +2. WHEN the Deploy_Plugin is invoked and a Lambda function with the Function_Name already exists, THE Plugin_Helper SHALL update the function code using the AWS Lambda `UpdateFunctionCode` API. +3. THE Plugin_Helper SHALL determine the Function_Name from the `executableTarget` name defined in `Package.swift`. +4. WHEN the Deploy_Plugin is invoked with the `--delete` option, THE Plugin_Helper SHALL delete the Lambda function using the `DeleteFunction` API and delete its associated IAM_Role. +5. WHEN the Deploy_Plugin creates a Lambda function and no `--iam-role` option is provided, THE Plugin_Helper SHALL create a new IAM_Role with the permissions required for Lambda execution. +6. WHERE the `--iam-role` option is provided, THE Plugin_Helper SHALL configure the Lambda function to use the specified IAM_Role. +7. WHERE the `--with-url` option is provided, THE Plugin_Helper SHALL configure a Function_URL using the `CreateFunctionUrlConfig` API with the `AWS_IAM` auth type, so that the Function_URL is protected by IAM authentication and callers must sign requests with AWS Signature Version 4 rather than being granted public, unauthenticated access. +8. WHEN the `--region` option is omitted, THE Plugin_Helper SHALL deploy to the AWS region resolved through AWS_Configuration_Resolution provided by Soto_Core. +9. WHERE the `--region` option is provided, THE Plugin_Helper SHALL deploy to the specified region, overriding the region resolved through AWS_Configuration_Resolution. +10. THE Plugin_Helper SHALL respect the current AWS region, credentials, and profile selection through Soto_Core's AWS_Configuration_Resolution rather than implementing its own configuration parsing. +11. WHEN the `--input-directory` option is omitted, THE Plugin_Helper SHALL read the deployment ZIP_Archive from the default Build_Plugin output path. +12. WHERE the `--input-directory` option is provided, THE Plugin_Helper SHALL read the deployment ZIP_Archive from the specified path. +13. WHEN the `--architecture` option is omitted, THE Plugin_Helper SHALL set the function architecture to the Host_Architecture. +14. WHERE the `--architecture` option is provided with `x64` or `arm64`, THE Plugin_Helper SHALL set the function architecture to the specified value. +15. WHEN the deployment ZIP_Archive size is within the AWS Lambda direct-upload limit, THE Plugin_Helper SHALL deploy the function code as a Base64 payload through the AWS Lambda REST API. +16. THE Plugin_Helper SHALL determine the AWS_Account_Identifier through the AWS STS `GetCallerIdentity` API. +17. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit AND the Deployment_Bucket does not exist, THE Plugin_Helper SHALL create the Deployment_Bucket named `swift-aws-lambda-runtime--` using the `CreateBucket` API. +18. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit AND the Deployment_Bucket already exists, THE Plugin_Helper SHALL reuse the Deployment_Bucket without recreating it. +19. WHEN the deployment ZIP_Archive size exceeds the AWS Lambda direct-upload limit, THE Plugin_Helper SHALL upload the ZIP_Archive as an S3 object into the Deployment_Bucket using the `PutObject` API and reference that object during deployment. +20. WHEN the deployment completes, THE Plugin_Helper SHALL delete the uploaded S3 object from the Deployment_Bucket using the `DeleteObject` API while retaining the Deployment_Bucket for reuse. +21. THE Plugin_Helper SHALL resolve AWS credentials using the Credential_Provider_Chain. +22. IF AWS credentials cannot be resolved through the Credential_Provider_Chain, THEN THE Plugin_Helper SHALL report a descriptive credential-resolution error and SHALL fail the deployment. +23. WHEN the Deploy_Plugin runs and the local AWS_Configuration_Files (`~/.aws/config` and `~/.aws/credentials`) are absent, THE Plugin_Helper SHALL emit a non-blocking, informational warning that suggests installing the AWS CLI and running `aws configure`, and SHALL continue the deployment, because credentials may still be resolved from environment variables, ECS or EKS container credentials, or the EC2 instance metadata service (IMDSv2) through the Credential_Provider_Chain; the absence of the local AWS_Configuration_Files alone SHALL NOT block deployment, and the deployment fails only when credentials cannot be resolved through the Credential_Provider_Chain (criterion 3.22) or when an AWS API request returns an error response (criterion 3.24). +24. IF an AWS API request returns an error response, THEN THE Plugin_Helper SHALL report the AWS error to the developer. +25. WHEN the Deploy_Plugin is invoked with the `--help` option, THE Plugin_Helper SHALL display usage information and SHALL NOT call any AWS API. +26. WHEN the Deploy_Plugin is invoked with the `--verbose` option, THE Plugin_Helper SHALL emit detailed progress output for each AWS API interaction. +27. WHEN the Deploy_Plugin completes a successful deployment, THE Plugin_Helper SHALL report the Lambda function ARN and the deployment region to the developer. +28. WHERE the `--with-url` option is provided and the deployment completes successfully, THE Plugin_Helper SHALL report the Function_URL to the developer and SHALL display a ready-to-use `curl` command that includes AWS Signature Version 4 authentication (using curl's `--aws-sigv4` option) so the developer can immediately invoke the function. +29. WHERE the `--with-url` option is not provided and the deployment completes successfully, THE Plugin_Helper SHALL display a ready-to-use `aws lambda invoke` command that the developer can use to invoke the deployed function. + +### Requirement 4: AWS service client generation and access + +**User Story:** As a project maintainer, I want type-safe AWS service clients for Lambda, IAM, S3, and STS generated once and checked into the repository, so that the deploy plugin can call AWS APIs without depending on the entire Soto SDK at build time. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL include Generated_Service_Client code for the AWS Lambda, IAM, S3, and STS operations required by the Deploy_Plugin, committed to the repository. +2. THE Generated_Service_Client for AWS Lambda SHALL provide the operations `CreateFunction`, `UpdateFunctionCode`, `DeleteFunction`, `GetFunction`, `CreateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `AddPermission`, and `RemovePermission`. +3. THE Generated_Service_Client for AWS IAM SHALL provide the operations `CreateRole`, `DeleteRole`, `AttachRolePolicy`, `DetachRolePolicy`, `GetRole`, `PutRolePolicy`, and `DeleteRolePolicy`. +4. THE Generated_Service_Client for AWS S3 SHALL provide the operations `CreateBucket`, `HeadBucket`, `PutObject`, and `DeleteObject`. +5. THE Generated_Service_Client for AWS STS SHALL provide the operation `GetCallerIdentity`. +6. THE Plugin_System SHALL provide a Generation_Script that invokes the Soto Code Generator to produce the Generated_Service_Client code for the required AWS Lambda, IAM, S3, and STS operations. +7. THE Generation_Script SHALL be a one-time, maintainer-run operation that is separate from the package build. +8. THE Plugin_System SHALL commit the Generated_Service_Client source code produced by the Generation_Script to the git repository. +9. WHEN a developer builds the package, THE Plugin_System SHALL compile the Generated_Service_Client code present on the file system, SHALL NOT invoke the Soto Code Generator during the build, and SHALL fail the build if the Generated_Service_Client code is not found on the file system. + +### Requirement 5: Dependencies and package integration + +**User Story:** As a project maintainer, I want the package to depend on Soto Core and remove vendored AWS infrastructure code, so that credential handling and request signing are production-tested and the maintenance burden is reduced. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL declare a package dependency on `soto-core`. +2. THE Plugin_Helper target SHALL depend on the `SotoCore` product. +3. THE Plugin_System SHALL NOT include vendored crypto, signer, or HTTP client code under `Sources/AWSLambdaPluginHelper/Vendored/`. +4. THE Plugin_System SHALL implement the `init`, `build`, and `deploy` commands within the single `AWSLambdaPluginHelper` executable target. +5. WHEN a plugin wrapper is invoked, THE plugin wrapper SHALL run the Plugin_Helper as a subprocess and pass the corresponding command and arguments. +6. IF the Plugin_Helper subprocess exits with a non-zero status, THEN THE invoking plugin wrapper SHALL report a diagnostic error and SHALL halt immediately without continuing further work. + +### Requirement 6: Amazon Linux 2023 as the default target + +**User Story:** As a Lambda developer, I want Amazon Linux 2023 to be the default target, so that my functions build and deploy against the current, supported AWS Lambda runtime without legacy AL2 prompts, while I retain the ability to target Amazon Linux 2 when I explicitly need it. + +#### Acceptance Criteria + +1. THE Build_Plugin SHALL use `swift:-amazonlinux2023` as the default Base_Image. +2. THE Plugin_System SHALL NOT emit an Amazon Linux 2 deprecation warning when the developer does not explicitly select an Amazon Linux 2 base image. +3. WHERE the developer explicitly provides an Amazon Linux 2 image through the `--base-docker-image` option, THE Plugin_Helper SHALL emit an informational warning noting that Amazon Linux 2 is deprecated and recommending migration to Amazon Linux 2023. +4. WHERE the developer provides an Amazon Linux 2 image through the `--base-docker-image` option, THE Plugin_Helper SHALL compile the products using the supplied Base_Image. +5. WHEN the Deploy_Plugin creates or updates a Lambda function, THE Plugin_Helper SHALL set the Lambda_Runtime_Identifier to `provided.al2023` as the default runtime. +6. WHEN the Build_Plugin runs natively on Amazon_Linux_2023, THE Plugin_Helper SHALL compile the products without requiring a container runtime. + +### Requirement 7: Backward compatibility with the legacy `archive` plugin + +**User Story:** As a developer with existing CI pipelines that call `swift package archive`, I want the legacy `archive` command to keep working as a passthrough to the new build plugin with an unchanged CLI, so that upgrading to the v4 plugin system does not break my existing automation. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL retain an `archive` command that acts as a passthrough to the Build_Plugin, resolving to the same sources and implementation as the `lambda-build` command. +2. WHEN a developer invokes `swift package archive`, THE Plugin_System SHALL perform the same build-and-package behavior as `swift package lambda-build`. +3. WHEN a developer invokes the `archive` command, THE Plugin_System SHALL display a deprecation warning that encourages the developer to use the `lambda-build` plugin (the `swift package lambda-build` command) instead, while still performing the build-and-package work. +4. THE Build_Plugin SHALL accept the command-line interface of the Legacy_Archive_Plugin, including the options `--output-directory`, `--products`, `--configuration`, `--swift-version`, `--base-docker-image`, `--disable-docker-image-update`, `--verbose`, and `--help`, so that existing invocations continue to work unchanged. +5. THE Build_Plugin SHALL accept the legacy `--output-directory` option as a Deprecated_Option_Alias for `--output-path`. +6. WHEN a developer supplies a Deprecated_Option_Alias such as `--output-directory`, THE Plugin_Helper SHALL treat the Deprecated_Option_Alias as equivalent to its current replacement option such as `--output-path`. +7. WHERE an option name has been renamed from the Legacy_Archive_Plugin, THE Build_Plugin SHALL accept the legacy option name as a Deprecated_Option_Alias that maps to the current option name. +8. WHEN a developer supplies a Deprecated_Option_Alias, THE Plugin_Helper MAY emit a deprecation notice indicating the current option name and SHALL honor the Deprecated_Option_Alias. +9. THE documentation for the Build_Plugin SHALL document the current option names as the primary documented interface and SHALL present the Deprecated_Option_Alias names only as supported compatibility aliases. +10. THE Plugin_System SHALL accept a set of Build_Plugin options that is a superset of the Legacy_Archive_Plugin options so that existing CI invocations of `swift package archive` continue to work. + +### Requirement 8: Documentation, tutorials, and examples migration + +**User Story:** As a developer learning from the project's documentation and examples, I want all DocC documentation, tutorials, and example READMEs updated to show the new plugin usage, so that I follow the current recommended workflow instead of deprecated commands. + +#### Acceptance Criteria + +1. THE Plugin_System documentation SHALL update the DocC articles and tutorials under `Sources/AWSLambdaRuntime/Docs.docc/`, the top-level `readme.md`, and the example READMEs under `Examples/` to show the new plugin commands `lambda-init`, `lambda-build`, and `lambda-deploy`. +2. WHERE documentation or an example README currently shows `swift package archive`, THE documentation SHALL show `swift package lambda-build` using the current option names. +3. WHERE an example currently deploys using the raw AWS CLI commands such as `aws lambda create-function`, `aws lambda update-function-code`, or `aws lambda create-function-url-config`, THE example documentation SHALL show the `lambda-deploy` plugin instead. +4. WHERE an example currently deploys using AWS SAM with a `template.yaml` and `sam deploy` or using AWS CDK with `cdk deploy`, THE example SHALL continue to use AWS SAM or AWS CDK respectively and SHALL retain its existing deployment tooling rather than the `lambda-deploy` plugin. +5. THE documentation SHALL remove the references to the Amazon Linux 2 deprecation guidance that are made obsolete by Requirement 6, consistent with Amazon_Linux_2023 being the default target. +6. WHERE example documentation shows invoking the function, THE example documentation MAY retain the raw `aws lambda invoke` command for invocation, because this release provides no plugin invoke command. +7. THE documentation SHALL describe, as a prerequisite for local developer-machine deployments with the Deploy_Plugin, that the developer install the AWS CLI and run `aws configure` to create the AWS configuration in `~/.aws`, and SHALL note that on EC2, ECS, or EKS the credentials are typically provided automatically by the instance or task role through the Credential_Provider_Chain, so running `aws configure` is not required in those environments. + +### Requirement 9: End-to-end lifecycle test suite + +**User Story:** As a project maintainer, I want an automated end-to-end test that exercises the full plugin lifecycle (scaffold → build → deploy → invoke → delete), so that I can verify the three plugins work together against real AWS before release. + +#### Acceptance Criteria + +1. THE Plugin_System SHALL provide an End_To_End_Test_Suite that exercises the full lifecycle: scaffold using the Init_Plugin with the URL_Template, build and package using the Build_Plugin, deploy using the Deploy_Plugin with the `--with-url` option, validate the deployed function through its Function_URL, and delete using the Deploy_Plugin with the `--delete` option. +2. THE End_To_End_Test_Suite SHALL be implemented as a shell script and SHALL NOT be implemented as Swift code. +3. WHEN the End_To_End_Test_Suite runs, THE End_To_End_Test_Suite SHALL scaffold a new Lambda function using the Init_Plugin with the URL_Template through the `--with-url` option. +4. WHEN the function has been scaffolded, THE End_To_End_Test_Suite SHALL build and package the function using the Build_Plugin. +5. WHEN the function has been packaged, THE End_To_End_Test_Suite SHALL deploy the function to AWS using the Deploy_Plugin with the `--with-url` option so that a Function_URL is configured. +6. WHEN the function has been deployed, THE End_To_End_Test_Suite SHALL validate the deployment by sending an HTTP request to the Function_URL using a `curl` command and SHALL verify the returned response. +7. THE Function_URL created by the End_To_End_Test_Suite SHALL use IAM authentication (the `AWS_IAM` auth type) rather than public, unauthenticated access. +8. WHEN the End_To_End_Test_Suite invokes the Function_URL with `curl`, THE End_To_End_Test_Suite SHALL sign the request using AWS Signature Version 4 via curl's built-in `--aws-sigv4` option. +9. WHEN the validation completes, THE End_To_End_Test_Suite SHALL delete the deployed Lambda function and its associated IAM_Role using the Deploy_Plugin `--delete` option. +10. IF any step of the End_To_End_Test_Suite fails after the function has been deployed, THEN THE End_To_End_Test_Suite SHALL delete the deployed Lambda function and its associated resources so that no AWS resources are leaked. +11. IF the Function_URL response does not match the expected result, THEN THE End_To_End_Test_Suite SHALL report a test failure. + +### Requirement 10: Preserve existing working implementation + +**User Story:** As a project maintainer, I want the existing partially-implemented init and build code to be preferred over newly generated code, so that proven working behavior is not regressed by a rewrite. + +#### Acceptance Criteria + +1. WHERE an existing implementation of the Init_Plugin or Build_Plugin functionality is present in the repository, THE Plugin_System SHALL reuse the existing implementation rather than replacing it with newly generated code. +2. THE Plugin_System SHALL preserve the existing behavior of the Init_Plugin and Build_Plugin except where a change is required to satisfy an acceptance criterion in this specification. +3. IF existing Init_Plugin or Build_Plugin code is proven incorrect by failing an acceptance criterion or a test, THEN THE Plugin_System SHALL modify or replace only the incorrect code. +4. WHEN modifying the existing Init_Plugin or Build_Plugin code to satisfy the new requirements, including default-symbol stripping, the Amazon Linux 2023 default Base_Image, or the unified `--cross-compile` option, THE Plugin_System SHALL make the minimal changes necessary and SHALL retain the remaining working behavior. diff --git a/.kiro/specs/lambda-v2-plugins/tasks.md b/.kiro/specs/lambda-v2-plugins/tasks.md new file mode 100644 index 000000000..84d088c38 --- /dev/null +++ b/.kiro/specs/lambda-v2-plugins/tasks.md @@ -0,0 +1,280 @@ +# Implementation Plan: Lambda V2 Plugins + +## Overview + +This plan implements the v4 plugin system for `swift-aws-lambda-runtime` following a dependency-aware ordering: foundational Package.swift changes first, then the helper dispatch fix, Builder modifications, vendored code removal, generated AWS clients, the Deployer (the main new piece), documentation migration, and finally tests. Each task builds on prior tasks and ends with integration checkpoints. + +## Tasks + +- [x] 1. Package.swift changes and foundational setup + - [x] 1.1 Add soto-core dependency and update AWSLambdaPluginHelper target + - Add `.package(url: "https://github.com/soto-project/soto-core.git", from: "7.0.0")` to the package dependencies array + - Add `.product(name: "SotoCore", package: "soto-core")` to the `AWSLambdaPluginHelper` executable target dependencies + - Verify the existing `AWSLambdaPackager` plugin target (verb: `archive`) is already declared and functional + - Run `swift package resolve` to confirm dependency resolution succeeds + - _Requirements: 5.1, 5.2, 7.1_ + +- [x] 2. Fix helper dispatch bug + - [x] 2.1 Fix `args.count > 2` guard to `args.count > 1` in AWSLambdaPluginHelper.swift + - In `command(from:)`, change `guard args.count > 2` to `guard args.count > 1` + - This unblocks the minimum valid invocation `[binary_path, command]` (count == 2) + - Verify dispatch still works for `init`, `build`, and `deploy` subcommands + - _Requirements: 5.4, 5.5 (Design: Helper Dispatch Bug Fix)_ + +- [x] 3. Builder modifications (minimal changes per Req 10) + - [x] 3.1 Change default base image to Amazon Linux 2023 + - In `BuilderConfiguration.init`, change the default `baseDockerImage` from `"swift:\(version)-amazonlinux2"` to `"swift:\(version)-amazonlinux2023"` + - Update the help message default description accordingly + - _Requirements: 6.1, 2.16_ + + - [x] 3.2 Remove blanket AL2 deprecation warning and add targeted AL2 warning + - Remove the unconditional `displayDeprecationWarning()` call that fires when running on AL2 or when any AL2 image is detected + - Add a new, shorter informational warning that fires ONLY when `--base-docker-image` explicitly contains `amazonlinux2` but NOT `amazonlinux2023` + - _Requirements: 6.2, 6.3, 6.4_ + + - [x] 3.3 Add default binary stripping with `-Xlinker -s` and `--no-strip` opt-out + - Add `-Xlinker -s` flags to both native (`buildNative`) and Docker/container (`buildInDocker`) swift build commands + - Add `--no-strip` flag extraction in `BuilderConfiguration.init`; when present, omit the strip flags + - _Requirements: 2.5, 2.6_ + + - [x] 3.4 Replace `--container-cli` with `--cross-compile` option + - Replace the `ContainerCLI` enum with a `CrossCompileMethod` enum: `docker`, `container`, `swift-static-sdk`, `custom-sdk` + - Extract `--cross-compile` option (default: `docker`) instead of `--container-cli` + - For `swift-static-sdk` and `custom-sdk`, report "not yet supported" error with SDK_Installation_Guide link + - Retain Docker/container runtime behavior for `docker` and `container` values (reuse existing `ContainerCLI` pull/run logic internally) + - _Requirements: 2.7, 2.8, 2.9, 2.10, 2.13, 2.14_ + + - [x] 3.5 Add `--output-directory` deprecated alias for `--output-path` + - In `BuilderConfiguration.init`, extract `--output-directory` if `--output-path` is not provided + - Map it to the same `outputDirectory` property + - Optionally emit a deprecation notice + - _Requirements: 7.5, 7.6, 7.7, 7.8_ + + - [x] 3.6 Add container CLI existence check with helpful error messages + - Before executing Docker or container commands, verify the CLI binary exists at the resolved tool path + - If Docker CLI is missing: report error with Docker download/installation URL + - If Apple `container` CLI is missing: report error with Apple container CLI download URL + - _Requirements: 2.11, 2.12_ + +- [x] 4. Checkpoint - Verify builder compiles and existing tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 5. Remove vendored code + - [x] 5.1 Delete `Sources/AWSLambdaPluginHelper/Vendored/` directory + - Remove the entire `Vendored/` directory containing crypto, signer, and HTTP client code + - Verify the project still compiles (it should, since soto-core replaces this functionality) + - _Requirements: 5.3_ + +- [x] 6. Generated AWS service clients + - [x] 6.1 Create the generation script at `scripts/generate-aws-clients.sh` + - Write a shell script that clones/uses Soto Code Generator + - Configure it to produce clients for Lambda, IAM, S3, and STS with only the required operations + - Script copies output to `Sources/AWSLambdaPluginHelper/GeneratedClients/` + - Script is maintainer-run, NOT part of the build + - _Requirements: 4.6, 4.7_ + + - [x] 6.2 Create generated Lambda client (`GeneratedClients/Lambda/`) + - Create `LambdaClient.swift`, `LambdaShapes.swift`, `LambdaErrors.swift` + - Operations: `CreateFunction`, `UpdateFunctionCode`, `DeleteFunction`, `GetFunction`, `CreateFunctionUrlConfig`, `DeleteFunctionUrlConfig`, `AddPermission`, `RemovePermission` + - Each operation wraps SotoCore's `AWSClient` with proper request signing + - _Requirements: 4.1, 4.2_ + + - [x] 6.3 Create generated IAM client (`GeneratedClients/IAM/`) + - Create `IAMClient.swift`, `IAMShapes.swift`, `IAMErrors.swift` + - Operations: `CreateRole`, `DeleteRole`, `AttachRolePolicy`, `DetachRolePolicy`, `GetRole`, `PutRolePolicy`, `DeleteRolePolicy` + - _Requirements: 4.1, 4.3_ + + - [x] 6.4 Create generated S3 client (`GeneratedClients/S3/`) + - Create `S3Client.swift`, `S3Shapes.swift`, `S3Errors.swift` + - Operations: `CreateBucket`, `HeadBucket`, `PutObject`, `DeleteObject` + - _Requirements: 4.1, 4.4_ + + - [x] 6.5 Create generated STS client (`GeneratedClients/STS/`) + - Create `STSClient.swift`, `STSShapes.swift`, `STSErrors.swift` + - Operations: `GetCallerIdentity` + - _Requirements: 4.1, 4.5_ + +- [x] 7. Checkpoint - Verify generated clients compile with soto-core + - Ensure all tests pass, ask the user if questions arise. + +- [x] 8. Deployer implementation + - [x] 8.1 Implement `DeployerConfiguration` with full argument parsing + - Parse: `--help`, `--verbose`, `--with-url`, `--delete`, `--region`, `--iam-role`, `--input-directory`, `--architecture`, `--products` + - Default architecture to host architecture (`x64` or `arm64`) + - Default input directory to the standard build output path + - Update `displayHelpMessage()` with all options + - _Requirements: 3.3, 3.6, 3.7, 3.9, 3.11, 3.12, 3.13, 3.14, 3.25, 3.26_ + + - [x] 8.2 Implement AWS client initialization and credential/config verification + - Initialize SotoCore `AWSClient` with the credential provider chain + - Check for `~/.aws/config` and `~/.aws/credentials`; if absent, emit non-blocking informational warning suggesting `aws configure` + - If credentials cannot be resolved, report descriptive error and fail + - Handle `--region` override or fall through to Soto's region resolution + - _Requirements: 3.8, 3.10, 3.21, 3.22, 3.23_ + + - [x] 8.3 Implement account ID resolution and function existence check + - Call STS `GetCallerIdentity` to resolve AWS account ID + - Call Lambda `GetFunction` to determine if function already exists + - Determine deployment action: create, update, or delete + - _Requirements: 3.1, 3.2, 3.16_ + + - [x] 8.4 Implement IAM role management + - When creating a function without `--iam-role`: create IAM role `swift-lambda--role` with Lambda assume-role trust policy + - Attach `AWSLambdaBasicExecutionRole` managed policy + - Wait for role propagation before proceeding + - When `--iam-role` is provided: use the specified role ARN directly + - When `--delete` is used: detach policies and delete the role + - _Requirements: 3.4, 3.5, 3.6_ + + - [x] 8.5 Implement S3 staging for large archives + - Define `directUploadLimit = 50 * 1024 * 1024` (50 MB) + - Implement `deploymentBucketName(region:accountId:)` → `"swift-aws-lambda-runtime--"` + - If ZIP > 50 MB: check if bucket exists (`HeadBucket`), create if absent (`CreateBucket`) + - Upload ZIP to bucket (`PutObject`) + - After deployment, delete the uploaded object (`DeleteObject`) while retaining the bucket + - _Requirements: 3.15, 3.17, 3.18, 3.19, 3.20_ + + - [x] 8.6 Implement create/update/delete function orchestration + - **Create**: `CreateFunction` with `provided.al2023` runtime, architecture, IAM role, ZIP payload (direct or S3 reference) + - **Update**: `UpdateFunctionCode` with ZIP payload (direct or S3 reference) + - **Delete**: `DeleteFunction`, then delete IAM role and its policies + - Report AWS errors with service, operation, and message + - _Requirements: 3.1, 3.2, 3.4, 3.24, 6.5_ + + - [x] 8.7 Implement Function URL setup + - When `--with-url` is provided: call `CreateFunctionUrlConfig` with `AWS_IAM` auth type + - Add resource-based permission for Function URL invocation + - _Requirements: 3.7_ + + - [x] 8.8 Implement post-deploy output (success reporting) + - Report Lambda function ARN and deployment region + - If `--with-url`: display Function URL and a ready-to-use `curl --aws-sigv4` command + - If no URL: display a ready-to-use `aws lambda invoke` command + - Shutdown AWSClient cleanly + - _Requirements: 3.27, 3.28, 3.29_ + +- [x] 9. Checkpoint - Verify deployer compiles and integrates with generated clients + - Ensure all tests pass, ask the user if questions arise. + +- [x] 10. Documentation migration + - [x] 10.1 Update DocC articles and tutorials + - Update articles under `Sources/AWSLambdaRuntime/Docs.docc/` to show `lambda-init`, `lambda-build`, `lambda-deploy` commands + - Replace `swift package archive` references with `swift package lambda-build` + - Replace raw AWS CLI deployment commands with `lambda-deploy` plugin usage (except SAM/CDK examples) + - Remove obsolete Amazon Linux 2 deprecation guidance + - Document `aws configure` prerequisite for local deployments + - _Requirements: 8.1, 8.2, 8.3, 8.5, 8.7_ + + - [x] 10.2 Update top-level readme.md and example READMEs + - Update `readme.md` with new plugin commands and workflow + - Update example READMEs under `Examples/` to show new commands + - Keep SAM/CDK examples using their respective deployment tools + - May retain `aws lambda invoke` for invocation examples + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.6_ + +- [x] 11. Unit tests + - [x] 11.1 Write unit tests for BuilderConfiguration argument parsing + - Test `--cross-compile` parsing with all valid values and invalid values + - Test `--no-strip` flag detection + - Test `--output-directory` deprecated alias maps to `outputDirectory` + - Test mutual exclusion of `--swift-version` and `--base-docker-image` + - Test default base image is `amazonlinux2023` + - _Requirements: 2.5, 2.6, 2.7, 2.17, 6.1, 7.5_ + + - [x] 11.2 Write unit tests for DeployerConfiguration argument parsing + - Test `--architecture` parsing with valid and invalid values + - Test default architecture matches host + - Test `--region`, `--iam-role`, `--input-directory`, `--with-url`, `--delete` parsing + - Test `--help` flag produces help output without AWS calls + - _Requirements: 3.13, 3.14, 3.25_ + + - [x] 11.3 Write unit tests for deployment bucket name construction and archive threshold + - Test `deploymentBucketName(region:accountId:)` produces correct format + - Test bucket name is valid S3 name (lowercase, 3-63 chars) + - Test `directUploadLimit` boundary: exactly 50 MB → direct, 50 MB + 1 → S3 + - _Requirements: 3.15, 3.17, 3.19_ + + - [x] 11.4 Write property test for deprecated option alias equivalence (Property 2) + - **Property 2: Deprecated option alias equivalence** + - For any path value, `--output-directory ` produces the same `outputDirectory` as `--output-path ` + - **Validates: Requirements 7.5, 7.6** + + - [x] 11.5 Write property test for cross-compile method parsing round-trip (Property 3) + - **Property 3: Cross-compile method parsing round-trip** + - For any valid `CrossCompileMethod` case, `rawValue` → parse → original case + - **Validates: Requirements 2.7** + + - [x] 11.6 Write property test for mutual exclusion of --swift-version and --base-docker-image (Property 4) + - **Property 4: Mutual exclusion of --swift-version and --base-docker-image** + - For any non-empty swift-version and any non-empty base-docker-image, parsing throws an error + - **Validates: Requirements 2.17** + + - [x] 11.7 Write property test for deployment bucket name construction (Property 5) + - **Property 5: Deployment bucket name construction** + - For any valid region and 12-digit account ID, result matches `"swift-aws-lambda-runtime--"` and is a valid S3 bucket name + - **Validates: Requirements 3.17, 3.18** + + - [x] 11.8 Write property test for archive size determines upload strategy (Property 6) + - **Property 6: Archive size determines upload strategy** + - For any size ≤ 50 MB → direct upload chosen; for any size > 50 MB → S3 staging chosen + - **Validates: Requirements 3.15, 3.19** + + - [x] 11.9 Write property test for AL2 warning logic (Property 8) + - **Property 8: AL2 warning emitted only for explicit AL2 image selection** + - For any image string containing "amazonlinux2" but NOT "amazonlinux2023" → warning emitted; for "amazonlinux2023" or default → no warning + - **Validates: Requirements 6.2, 6.3** + + - [x] 11.10 Write property test for unsupported cross-compile methods (Property 9) + - **Property 9: Unsupported cross-compile methods report error with link** + - For `swift-static-sdk` and `custom-sdk`, builder reports "not yet supported" error containing SDK guide URL + - **Validates: Requirements 2.14** + +- [x] 12. Checkpoint - Ensure all unit and property tests pass + - Ensure all tests pass, ask the user if questions arise. + +- [x] 13. End-to-end test script + - [x] 13.1 Create `scripts/integration-test.sh` shell script + - Implement the full lifecycle: scaffold (lambda-init --with-url) → build (lambda-build) → deploy (lambda-deploy --with-url) → validate (curl --aws-sigv4 to Function URL) → delete (lambda-deploy --delete) + - Use `trap cleanup EXIT` to guarantee resource cleanup on failure + - Verify response body matches expected output + - Use unique function name with timestamp to avoid conflicts + - Script must be `bash`, NOT Swift + - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9, 9.10, 9.11_ + +- [x] 14. Final checkpoint - Full build verification + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- Tasks marked with `*` are optional and can be skipped for faster MVP +- Each task references specific requirements for traceability +- Checkpoints ensure incremental validation +- Property tests validate universal correctness properties from the design document +- Unit tests validate specific examples and edge cases +- The Deployer (task 8) is the largest piece of new work and is broken into focused sub-tasks +- Builder modifications (task 3) are minimal per Requirement 10 (preserve existing working code) +- Generated clients (task 6) must be in place before the Deployer can compile +- Documentation and tests can proceed in parallel once core implementation is complete +- The e2e test (task 13) depends on all prior implementation being complete + +## Task Dependency Graph + +```json +{ + "waves": [ + { "id": 0, "tasks": ["1.1"] }, + { "id": 1, "tasks": ["2.1", "5.1"] }, + { "id": 2, "tasks": ["3.1", "3.2", "3.3", "3.4", "3.5", "3.6"] }, + { "id": 3, "tasks": ["6.1"] }, + { "id": 4, "tasks": ["6.2", "6.3", "6.4", "6.5"] }, + { "id": 5, "tasks": ["8.1"] }, + { "id": 6, "tasks": ["8.2", "8.3"] }, + { "id": 7, "tasks": ["8.4", "8.5"] }, + { "id": 8, "tasks": ["8.6"] }, + { "id": 9, "tasks": ["8.7", "8.8"] }, + { "id": 10, "tasks": ["10.1", "10.2", "11.1", "11.2", "11.3"] }, + { "id": 11, "tasks": ["11.4", "11.5", "11.6", "11.7", "11.8", "11.9", "11.10"] }, + { "id": 12, "tasks": ["13.1"] } + ] +} +``` diff --git a/.kiro/specs/xray-trace-id-propagation/design.md b/.kiro/specs/xray-trace-id-propagation/design.md new file mode 100644 index 000000000..1e77e13cf --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/design.md @@ -0,0 +1,172 @@ +# Design Document + +## Overview + +This design introduces implicit trace ID propagation using Swift's `TaskLocal` mechanism, making the trace ID available to any code running within an invocation's structured concurrency tree. It also adds backward-compatible `_X_AMZN_TRACE_ID` environment variable setting in single-concurrency mode for legacy tooling. + +Note: The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to OpenTelemetry. This design targets OpenTelemetry instrumentation as the primary consumer of the `TaskLocal`-based propagation, while the env var fallback supports legacy tooling that still reads `_X_AMZN_TRACE_ID`. + +The approach mirrors the recommendations in the cross-runtime trace propagation spec: Java uses SLF4J MDC (thread-local), Node.js uses `AsyncLocalStorage`, and Swift uses `TaskLocal` — each runtime's idiomatic context-propagation primitive. + +## Architecture + +The feature touches three layers: + +1. **`LambdaContext` (public API)**: Adds a `@TaskLocal` static property for implicit trace ID access +2. **`Lambda.runLoop` (runtime core)**: Wraps each handler invocation in a `TaskLocal` `withValue` scope +3. **`Lambda.runLoop` (env var compat)**: Conditionally sets/clears `_X_AMZN_TRACE_ID` in single-concurrency mode + +### Data Flow + +``` +Control Plane HTTP Response + └─ Lambda-Runtime-Trace-Id header + └─ InvocationMetadata.traceID (already exists) + ├─ LambdaContext.traceID (instance property, already exists) + ├─ LambdaContext.$currentTraceID TaskLocal (NEW) + │ └─ Available to all code in the handler's async task tree + └─ _X_AMZN_TRACE_ID env var (NEW, single-concurrency only) + └─ Available to legacy tooling / OTel auto-instrumentation via process environment +``` + +## Components and Interfaces + +### TaskLocal Declaration on LambdaContext + +```swift +@available(LambdaSwift 2.0, *) +extension LambdaContext { + /// The trace ID for the current invocation, available via Swift's TaskLocal mechanism. + /// This enables OpenTelemetry instrumentation and other tracing libraries to discover + /// the trace ID without an explicit LambdaContext reference. + /// Returns `nil` when accessed outside of a Lambda invocation scope. + @TaskLocal + public static var currentTraceID: String? +} +``` + +**Design Decisions:** +- Placed on `LambdaContext` rather than a new type, since the trace ID is already part of the context's domain +- Returns `Optional` — `nil` outside invocation scope is a clear signal, no sentinel values +- Named `currentTraceID` to distinguish from the instance property `traceID` and to convey "current invocation" + +### Concurrency Mode Detection + +The run loop needs to know whether it's operating in single or multi-concurrency mode to decide whether to set the environment variable. Rather than re-reading `AWS_LAMBDA_MAX_CONCURRENCY` in the run loop, the concurrency mode is passed as a parameter from the caller. + +```swift +@available(LambdaSwift 2.0, *) +extension Lambda { + @inlinable + package static func runLoop( + runtimeClient: RuntimeClient, + handler: Handler, + loggingConfiguration: LoggingConfiguration, + logger: Logger, + isSingleConcurrencyMode: Bool = true + ) async throws where Handler: StreamingLambdaHandler +} +``` + +**Design Decisions:** +- Default value `true` preserves backward compatibility — `LambdaRuntime` (single-concurrency) doesn't need changes +- `LambdaManagedRuntime` passes `false` when `maxConcurrency > 1` +- Boolean is simpler than passing the integer concurrency value since we only need a binary decision + +### Modified Run Loop (Lambda.swift) + +The core change wraps the handler invocation in a `TaskLocal` scope: + +```swift +// Inside the while loop, after creating requestLogger: + +try await LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { + + // Set env var only in single-concurrency mode + if isSingleConcurrencyMode { + setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1) + } + defer { + if isSingleConcurrencyMode { + unsetenv("_X_AMZN_TRACE_ID") + } + } + + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + // ... rest unchanged + ) + ) + } catch { + try await writer.reportError(error) + } +} +``` + +**Design Decisions:** +- `TaskLocal.withValue` is used (not `TaskLocal.withValue(operation:)` with a stored binding) to ensure the scope is tied to structured concurrency +- The env var is set inside the `withValue` scope so both mechanisms are active simultaneously +- `defer` ensures cleanup even if the handler throws +- The `setenv`/`unsetenv` calls use POSIX functions already imported in the file (`Darwin.C` / `Glibc` / `Musl`) + +### LambdaManagedRuntime Changes + +```swift +// In _run(), when maxConcurrency > 1: +try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: self.handler, + eventLoop: self.eventLoop, + loggingConfiguration: self.loggingConfiguration, + logger: logger, + isSingleConcurrencyMode: false // NEW +) +``` + +And the `startRuntimeInterfaceClient` method passes this through to `Lambda.runLoop`. + +## Error Handling + +No new error types are introduced. `TaskLocal.withValue` does not throw on its own. The `setenv`/`unsetenv` calls are best-effort (their return values are ignored, matching the convention in other Lambda runtimes). + +## Testing Strategy + +### Unit Tests + +1. **TaskLocal propagation in single-concurrency mode** + - Verify `LambdaContext.currentTraceID` returns the correct trace ID inside a handler + - Verify `LambdaContext.currentTraceID` returns `nil` outside a handler scope + +2. **TaskLocal isolation in multi-concurrency mode** + - Spawn multiple concurrent tasks, each with a different trace ID via `withValue` + - Verify each task sees only its own trace ID + +3. **Environment variable behavior** + - In single-concurrency mode: verify `_X_AMZN_TRACE_ID` is set during handler execution and cleared after + - In multi-concurrency mode: verify `_X_AMZN_TRACE_ID` is NOT set + +4. **Background task propagation** + - Verify the `TaskLocal` remains available during background work after the response is sent + +### Integration Tests + +- Use the existing local test server infrastructure to run a handler that reads `LambdaContext.currentTraceID` and returns it in the response, verifying it matches the trace ID sent in the invocation headers. + +## Performance Considerations + +- `TaskLocal` access is O(1) — it's a lookup in the task's local storage, comparable to reading a local variable +- `setenv`/`unsetenv` are syscalls but only execute in single-concurrency mode (one invocation at a time), so there's no contention +- No heap allocations beyond the `String` value already present in `InvocationMetadata.traceID` +- No new dependencies introduced + +## Compatibility + +- Fully backward compatible: existing `context.traceID` instance property is unchanged +- The `TaskLocal` is additive — code that doesn't use it is unaffected +- The `isSingleConcurrencyMode` parameter defaults to `true`, so `LambdaRuntime` callers don't need changes +- The env var behavior in single-concurrency mode matches what other Lambda runtimes (Python, Node.js, Java) do today for backward compatibility with legacy tooling diff --git a/.kiro/specs/xray-trace-id-propagation/github-issue-633.md b/.kiro/specs/xray-trace-id-propagation/github-issue-633.md new file mode 100644 index 000000000..227d7a5ee --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/github-issue-633.md @@ -0,0 +1,104 @@ +## [core] Implement Trace ID Propagation for Multi-Concurrent Environments + +The Swift AWS Lambda Runtime currently receives the trace ID from the Lambda Runtime API via the `Lambda-Runtime-Trace-Id` header and stores it in `LambdaContext.traceID`. However, there's no implicit propagation mechanism — downstream code (OpenTelemetry instrumentation, HTTP client middleware) can't discover the current invocation's trace ID without an explicit `LambdaContext` reference. + +According to the [AWS Lambda Runtime API documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), the runtime should also set the `_X_AMZN_TRACE_ID` environment variable. This is missing from the Swift runtime. + +> **Note:** The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to [OpenTelemetry](https://docs.aws.amazon.com/xray/latest/devguide/xray-otel-migration.html). The `TaskLocal`-based propagation proposed here is the forward-looking mechanism for OpenTelemetry integration. The `_X_AMZN_TRACE_ID` environment variable is maintained for backward compatibility with legacy tooling only. + +### Current Behavior + +- The runtime receives the `Lambda-Runtime-Trace-Id` header from the Lambda Runtime API +- The trace ID is stored in `LambdaContext.traceID` +- The `_X_AMZN_TRACE_ID` environment variable is **not** set +- No implicit propagation mechanism exists for downstream libraries + +### Expected Behavior + +- The runtime receives the `Lambda-Runtime-Trace-Id` header +- The trace ID is stored in `LambdaContext.traceID` (unchanged) +- A `@TaskLocal` makes the trace ID implicitly available to all code in the handler's async task tree +- In single-concurrency mode only, the `_X_AMZN_TRACE_ID` environment variable is set per invocation and cleared after + +### Implementation Considerations + +#### Standard Lambda Functions (single concurrency) + +Setting the environment variable with `setenv()` is straightforward since only one invocation runs at a time. The `TaskLocal` is also set for consistency. + +#### Lambda Managed Instances (multi-concurrency) + +Multiple concurrent invocations share the same process. Environment variables are process-global, so setting `_X_AMZN_TRACE_ID` would cause trace ID conflicts between concurrent invocations. In this mode, the runtime must **skip** the env var entirely and rely solely on the `TaskLocal`. + +The runtime detects the mode via `AWS_LAMBDA_MAX_CONCURRENCY` (already read by `LambdaManagedRuntime`). + +### How Other Runtimes Handle This + +- **Python:** Uses `os.environ['_X_AMZN_TRACE_ID']` — safe because Python's multi-concurrency model uses separate processes with isolated `os.environ` ([ref](https://github.com/aws/aws-lambda-python-runtime-interface-client)) +- **Java:** Uses SLF4J MDC (thread-local map) to avoid `SystemProperty` conflicts across threads ([ref](https://github.com/aws/aws-lambda-java-libs)) +- **Node.js:** Uses `AsyncLocalStorage` to bind trace ID to the current async call chain ([ref](https://github.com/aws/aws-lambda-nodejs-runtime-interface-client)) + +### Solution: TaskLocal + Conditional Environment Variable + +Swift's `TaskLocal` is the direct equivalent of Java's MDC and Node.js's `AsyncLocalStorage`. It isolates values to the current structured concurrency tree. + +#### 1. Define TaskLocal on LambdaContext + +```swift +@available(LambdaSwift 2.0, *) +extension LambdaContext { + @TaskLocal + public static var currentTraceID: String? +} +``` + +#### 2. Wrap Handler Invocation in TaskLocal Scope + +In `Sources/AWSLambdaRuntime/Lambda.swift`, inside `Lambda.runLoop`: + +```swift +try await LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { + if isSingleConcurrencyMode { + setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1) + } + defer { + if isSingleConcurrencyMode { + unsetenv("_X_AMZN_TRACE_ID") + } + } + + try await handler.handle(invocation.event, responseWriter: writer, context: context) +} +``` + +#### 3. Thread Concurrency Mode Through the Call Chain + +Add `isSingleConcurrencyMode: Bool = true` parameter to: +- `Lambda.runLoop` (default `true` for backward compat) +- `LambdaRuntime.startRuntimeInterfaceClient` + +`LambdaManagedRuntime` passes `false` when `maxConcurrency > 1`. + +### Files to Modify + +- **`Sources/AWSLambdaRuntime/LambdaContext.swift`** — Add `@TaskLocal public static var currentTraceID: String?` +- **`Sources/AWSLambdaRuntime/Lambda.swift`** — Wrap handler call in `withValue` scope, add `isSingleConcurrencyMode` parameter, conditionally set/clear env var +- **`Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift`** — Add `isSingleConcurrencyMode` parameter to `startRuntimeInterfaceClient`, pass through to `Lambda.runLoop` +- **`Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift`** — Pass `isSingleConcurrencyMode: false` when `maxConcurrency > 1` + +### Testing Requirements + +- [ ] `LambdaContext.currentTraceID` returns `nil` outside invocation scope +- [ ] `LambdaContext.currentTraceID` returns correct value inside handler +- [ ] Concurrent tasks with different trace IDs see their own values (no cross-contamination) +- [ ] Single-concurrency mode: `_X_AMZN_TRACE_ID` is set during handler execution and cleared after +- [ ] Multi-concurrency mode: `_X_AMZN_TRACE_ID` is NOT set +- [ ] TaskLocal remains available during background work after response is sent + +### References + +- [AWS Lambda Runtime API Documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html) +- [X-Ray Tracing Header Documentation](https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader) +- [Migrating from X-Ray to OpenTelemetry](https://docs.aws.amazon.com/xray/latest/devguide/xray-otel-migration.html) +- [Swift TaskLocal Documentation](https://developer.apple.com/documentation/swift/tasklocal) +- Design and implementation plan: `.kiro/specs/xray-trace-id-propagation/` diff --git a/.kiro/specs/xray-trace-id-propagation/requirements.md b/.kiro/specs/xray-trace-id-propagation/requirements.md new file mode 100644 index 000000000..87e6d7fb2 --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/requirements.md @@ -0,0 +1,61 @@ +# Requirements Document + +## Introduction + +This feature adds proper X-Ray Trace ID propagation for multi-concurrent Lambda environments (Managed Instances / "Elevator"). Today, the Swift Lambda runtime extracts the `Lambda-Runtime-Trace-Id` header per invocation and passes it through `LambdaContext.traceID`, but it does not provide an implicit propagation mechanism. Downstream libraries (e.g., OpenTelemetry instrumentation or HTTP client middleware) have no way to discover the current invocation's trace ID without an explicit `LambdaContext` reference. In multi-concurrent mode (`AWS_LAMBDA_MAX_CONCURRENCY > 1`), multiple invocations run simultaneously in the same process, making process-level environment variables unsuitable for trace propagation. + +Note: The AWS X-Ray SDK/Daemon entered maintenance mode on February 25, 2026. AWS recommends migrating to OpenTelemetry for instrumentation. This design targets OpenTelemetry as the primary consumer of implicit trace ID propagation, while maintaining the `_X_AMZN_TRACE_ID` environment variable in single-concurrency mode for backward compatibility with legacy tooling. + +The solution uses Swift's `TaskLocal` to store the trace ID in the current task's scope, making it implicitly available to all code running within an invocation's async call tree. In single-concurrency mode, the runtime additionally sets the `_X_AMZN_TRACE_ID` environment variable for backward compatibility with legacy tooling that reads it. + +## Requirements + +### Requirement 1 + +**User Story:** As a library author building OpenTelemetry or tracing middleware for Swift Lambda functions, I want to implicitly access the current invocation's trace ID from anywhere in the async call tree, so that I can auto-inject trace context headers on outbound HTTP calls without requiring an explicit `LambdaContext` reference. + +#### Acceptance Criteria + +1. WHEN a Lambda invocation is being handled THEN the system SHALL make the X-Ray Trace ID available via a `TaskLocal` property accessible as `LambdaContext.currentTraceID` +2. WHEN code running inside the handler's async call tree accesses `LambdaContext.currentTraceID` THEN it SHALL return the trace ID for the current invocation +3. WHEN code running outside any invocation scope accesses `LambdaContext.currentTraceID` THEN it SHALL return `nil` +4. WHEN multiple invocations run concurrently THEN each invocation's `TaskLocal` SHALL contain its own trace ID without cross-contamination + +### Requirement 2 + +**User Story:** As a Lambda function developer using the traditional single-concurrency mode, I want the runtime to set the `_X_AMZN_TRACE_ID` environment variable per invocation, so that legacy tooling and OpenTelemetry auto-instrumentation that reads this variable continues to work without code changes. + +#### Acceptance Criteria + +1. WHEN `AWS_LAMBDA_MAX_CONCURRENCY` is 1 or unset THEN the runtime SHALL set the `_X_AMZN_TRACE_ID` process environment variable to the current invocation's trace ID before calling the handler +2. WHEN the invocation completes THEN the runtime SHALL clear the `_X_AMZN_TRACE_ID` environment variable +3. WHEN `AWS_LAMBDA_MAX_CONCURRENCY` is greater than 1 THEN the runtime SHALL NOT set the `_X_AMZN_TRACE_ID` environment variable (to avoid cross-invocation contamination) + +### Requirement 3 + +**User Story:** As a Lambda function developer, I want the `TaskLocal`-based trace ID propagation to work transparently with all handler protocols (`StreamingLambdaHandler`, `LambdaHandler`, `LambdaWithBackgroundProcessingHandler`), so that I don't need to change my handler code. + +#### Acceptance Criteria + +1. WHEN the runtime invokes any handler type THEN the trace ID `TaskLocal` SHALL be set before the handler's `handle` function is called +2. WHEN background work executes after the response is sent (via `LambdaWithBackgroundProcessingHandler`) THEN the `TaskLocal` trace ID SHALL remain available during background execution +3. WHEN the handler function returns or throws THEN the `TaskLocal` scope SHALL end naturally via structured concurrency + +### Requirement 4 + +**User Story:** As a Lambda function developer, I want a public API to read the current trace ID for manual propagation scenarios, so that I can pass it to libraries that don't automatically read the `TaskLocal`. + +#### Acceptance Criteria + +1. WHEN a developer accesses `LambdaContext.currentTraceID` THEN it SHALL return an `Optional` containing the trace ID or `nil` if not in an invocation scope +2. WHEN a developer accesses `context.traceID` on a `LambdaContext` instance THEN it SHALL continue to work as before (no breaking changes) + +### Requirement 5 + +**User Story:** As a maintainer of the Swift Lambda runtime, I want the trace propagation mechanism to have minimal performance overhead, so that it does not impact cold start times or invocation latency. + +#### Acceptance Criteria + +1. WHEN the `TaskLocal` is set per invocation THEN the overhead SHALL be negligible (sub-microsecond, comparable to a dictionary lookup) +2. WHEN running in single-concurrency mode THEN the `setenv`/`unsetenv` calls SHALL add no measurable latency to invocation processing +3. WHEN the feature is compiled THEN it SHALL NOT introduce any new external dependencies beyond what the runtime already uses diff --git a/.kiro/specs/xray-trace-id-propagation/tasks.md b/.kiro/specs/xray-trace-id-propagation/tasks.md new file mode 100644 index 000000000..99957b16f --- /dev/null +++ b/.kiro/specs/xray-trace-id-propagation/tasks.md @@ -0,0 +1,45 @@ +# Implementation Plan + +- [ ] 1. Add `@TaskLocal` property to `LambdaContext` + - In `Sources/AWSLambdaRuntime/LambdaContext.swift`, add a `@TaskLocal` static property `currentTraceID` of type `String?` to `LambdaContext` via an extension + - Add documentation comment explaining the property returns the trace ID for the current invocation, or `nil` outside an invocation scope. Note that this is intended for use by OpenTelemetry instrumentation and tracing middleware. + - _Requirements: 1.1, 1.3, 4.1, 4.2_ + +- [ ] 2. Add `isSingleConcurrencyMode` parameter to `Lambda.runLoop` + - In `Sources/AWSLambdaRuntime/Lambda.swift`, add a `isSingleConcurrencyMode: Bool = true` parameter to both `runLoop` overloads (the current one and the deprecated one) + - The deprecated overload should forward the parameter to the current overload + - _Requirements: 2.1, 2.3_ + +- [ ] 3. Wrap handler invocation in `TaskLocal.withValue` scope in `Lambda.runLoop` + - In `Sources/AWSLambdaRuntime/Lambda.swift`, inside the `while !Task.isCancelled` loop, wrap the handler invocation (from `handler.handle(...)` through the catch block) in `LambdaContext.$currentTraceID.withValue(invocation.metadata.traceID) { ... }` + - Inside the `withValue` scope, conditionally call `setenv("_X_AMZN_TRACE_ID", invocation.metadata.traceID, 1)` when `isSingleConcurrencyMode` is `true` + - Add a `defer` block that calls `unsetenv("_X_AMZN_TRACE_ID")` when `isSingleConcurrencyMode` is `true` + - _Requirements: 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3_ + +- [ ] 4. Add `isSingleConcurrencyMode` parameter to `LambdaRuntime.startRuntimeInterfaceClient` + - In `Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift`, add `isSingleConcurrencyMode: Bool = true` parameter to the `startRuntimeInterfaceClient` static method + - Pass the parameter through to `Lambda.runLoop` + - _Requirements: 2.1, 2.3_ + +- [ ] 5. Pass `isSingleConcurrencyMode: false` from `LambdaManagedRuntime` when concurrency > 1 + - In `Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift`, in the `_run()` method, when `maxConcurrency > 1`, pass `isSingleConcurrencyMode: false` to `LambdaRuntime.startRuntimeInterfaceClient` + - The single-concurrency path (`maxConcurrency <= 1`) should pass `isSingleConcurrencyMode: true` (or rely on the default) + - _Requirements: 2.1, 2.3_ + +- [ ] 6. Write unit tests for `TaskLocal` trace ID propagation + - Create or extend test file in `Tests/AWSLambdaRuntimeTests/` to test `LambdaContext.currentTraceID` + - Test that `LambdaContext.currentTraceID` returns `nil` outside an invocation scope + - Test that `LambdaContext.$currentTraceID.withValue("test-trace-id") { ... }` makes the value accessible inside the closure + - Test that concurrent tasks with different trace IDs via `withValue` each see their own value (no cross-contamination) + - _Requirements: 1.2, 1.4_ + +- [ ] 7. Write unit tests for environment variable behavior + - Test that in single-concurrency mode, `_X_AMZN_TRACE_ID` is set during handler execution (use the local test server or mock runtime client) + - Test that in single-concurrency mode, `_X_AMZN_TRACE_ID` is cleared after handler execution + - Test that in multi-concurrency mode, `_X_AMZN_TRACE_ID` is NOT set during handler execution + - _Requirements: 2.1, 2.2, 2.3_ + +- [ ] 8. Update documentation + - Update `Sources/AWSLambdaRuntime/Docs.docc/` if there is existing documentation about tracing or context to mention `LambdaContext.currentTraceID` + - Add a brief note in the managed instances documentation (`Sources/AWSLambdaRuntime/Docs.docc/managed-instances.md`) about trace ID propagation in multi-concurrency mode + - _Requirements: 1.1, 4.1_ diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 000000000..ead87679e --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,22 @@ +# Swift AWS Lambda Runtime + +The Swift AWS Lambda Runtime is a library that enables developers to build AWS Lambda functions using Swift. It provides a multi-tier API for creating serverless functions ranging from simple closures to complex, performance-sensitive event handlers. + +## Key Features + +- **Performance-optimized**: Low memory footprint, deterministic performance, and quick start times +- **Developer-friendly**: Emphasizes safety, expressiveness, and ease of use +- **Multi-tier API**: Supports both simple closures and complex event handlers +- **AWS Integration**: Built-in support for AWS services through events and responses +- **Response Streaming**: Support for streaming large responses back to clients +- **Background Tasks**: Ability to run code after returning the main response +- **Local Testing**: Built-in local HTTP server for testing functions before deployment + +## Target Use Cases + +- Event-driven serverless functions +- API Gateway backends +- AWS service integrations (S3, SNS, SQS) +- Cost-effective compute workloads +- Mission-critical microservices +- Data-intensive workloads requiring efficient resource utilization \ No newline at end of file diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 000000000..84b075636 --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,105 @@ +# Project Structure + +## Root Directory + +- `Package.swift` - Swift Package Manager manifest with dependencies and targets +- `Package@swift-6.0.swift` - Swift 6.0 specific package manifest +- `Makefile` - Build automation and convenience commands +- `readme.md` - Main project documentation and getting started guide + +## Core Source Code + +### `Sources/AWSLambdaRuntime/` +Main runtime library implementation: + +- `Lambda.swift` - Core Lambda runtime loop and execution logic +- `LambdaRuntime.swift` - Main runtime class and public API +- `LambdaHandlers.swift` - Handler protocol definitions and implementations +- `LambdaContext.swift` - Request context and metadata +- `LambdaRuntimeClient.swift` - HTTP client for AWS Lambda Runtime API +- `Lambda+Codable.swift` - JSON encoding/decoding support +- `Lambda+LocalServer.swift` - Local testing server implementation +- `LambdaRuntimeError.swift` - Error types and handling +- `ControlPlaneRequest.swift` - AWS control plane communication +- `Utils.swift` - Utility functions and helpers + +### `Sources/AWSLambdaRuntime/FoundationSupport/` +Foundation integration and JSON support + +### `Sources/AWSLambdaRuntime/Docs.docc/` +Documentation and tutorials using DocC + +### `Sources/MockServer/` +Mock server for performance testing + +## Examples Directory + +### `Examples/` +Comprehensive example implementations: + +- `_MyFirstFunction/` - Quick start tutorial with deployment script +- `HelloWorld/` - Basic Lambda function example +- `HelloJSON/` - JSON input/output handling +- `APIGateway/` - REST API with API Gateway integration +- `APIGateway+LambdaAuthorizer/` - API with Lambda authorizer +- `Streaming/` - Response streaming example +- `StreamingFromEvent/` - Event-driven streaming +- `BackgroundTasks/` - Background processing after response +- `S3EventNotifier/` - S3 event handling +- `S3_AWSSDK/` - AWS SDK integration +- `S3_Soto/` - Soto library integration +- `CDK/` - AWS CDK deployment example +- `Testing/` - Unit testing patterns +- `Tutorial/` - Step-by-step learning materials + +## Testing + +### `Tests/AWSLambdaRuntimeTests/` +Comprehensive test suite covering: +- Runtime functionality +- Handler implementations +- Error scenarios +- Integration tests + +## Build and Deployment + +### `Plugins/` +Swift Package Manager plugins: +- `AWSLambdaPackager` - Creates deployment-ready ZIP archives + +### `.build/` +Build artifacts and intermediate files (generated) + +## Configuration Files + +- `.swift-version` - Swift toolchain version specification +- `.swift-format` - Code formatting configuration +- `.gitignore` - Git ignore patterns +- `.licenseignore` - License checking exclusions + +## Development Tools + +### `.devcontainer/` +VS Code development container configuration + +### `.vscode/` +VS Code workspace settings and configurations + +### `scripts/` +Build and deployment automation scripts + +## Documentation + +- `CODE_OF_CONDUCT.md` - Community guidelines +- `CONTRIBUTING.md` - Contribution guidelines +- `CONTRIBUTORS.txt` - Project contributors +- `SECURITY.md` - Security policy and reporting +- `LICENSE.txt` - Apache 2.0 license +- `NOTICE.txt` - Third-party notices + +## Naming Conventions + +- **Files**: PascalCase for Swift files (e.g., `LambdaRuntime.swift`) +- **Directories**: PascalCase for major components, lowercase for utilities +- **Examples**: Descriptive names indicating functionality +- **Tests**: Mirror source structure with `Tests` suffix \ No newline at end of file diff --git a/.kiro/steering/swift-api-design-guidelines.md b/.kiro/steering/swift-api-design-guidelines.md new file mode 100644 index 000000000..0f349e77b --- /dev/null +++ b/.kiro/steering/swift-api-design-guidelines.md @@ -0,0 +1,222 @@ +# Swift API Design Guidelines + +*Content adapted from [Swift.org API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) for compliance with licensing restrictions* + +## Table of Contents + +- [Introduction](#introduction) +- [Fundamentals](#fundamentals) +- [Naming](#naming) + - [Promote Clear Usage](#promote-clear-usage) + - [Strive for Fluent Usage](#strive-for-fluent-usage) + - [Use Terminology Well](#use-terminology-well) +- [Conventions](#conventions) + - [General Conventions](#general-conventions) + - [Parameters](#parameters) + - [Argument Labels](#argument-labels) +- [Special Instructions](#special-instructions) + +## Introduction + +Delivering a clear, consistent developer experience when writing Swift code is largely defined by the names and idioms that appear in APIs. These design guidelines explain how to make sure that your code feels like a part of the larger Swift ecosystem. + +## Fundamentals + +- **Clarity at the point of use** is your most important goal. Entities such as methods and properties are declared only once but used repeatedly. Design APIs to make those uses clear and concise. + +- **Clarity is more important than brevity.** Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters. + +- **Write a documentation comment** for every declaration. Insights gained by writing documentation can have a profound impact on your design. + +### Documentation Guidelines + +- Use Swift's dialect of Markdown +- Begin with a summary that describes the entity being declared +- Focus on the summary; it's the most important part +- Use a single sentence fragment if possible, ending with a period +- Describe what a function or method does and what it returns +- Describe what a subscript accesses +- Describe what an initializer creates +- For all other declarations, describe what the declared entity is + +Example: +```swift +/// Returns a "view" of `self` containing the same elements in reverse order. +func reversed() -> ReverseCollection +``` + +## Naming + +### Promote Clear Usage + +- **Include all the words needed to avoid ambiguity** for a person reading code where the name is used. + +```swift +// Good +employees.remove(at: x) + +// Bad - unclear intent +employees.remove(x) // are we removing x? +``` + +- **Omit needless words.** Every word in a name should convey salient information at the use site. + +```swift +// Bad - redundant type information +public mutating func removeElement(_ member: Element) -> Element? +allViews.removeElement(cancelButton) + +// Good - clearer without redundancy +public mutating func remove(_ member: Element) -> Element? +allViews.remove(cancelButton) +``` + +- **Name variables, parameters, and associated types according to their roles,** rather than their type constraints. + +```swift +// Bad - names based on types +var string = "Hello" +func restock(from widgetFactory: WidgetFactory) + +// Good - names based on roles +var greeting = "Hello" +func restock(from supplier: WidgetFactory) +``` + +- **Compensate for weak type information** to clarify a parameter's role. + +```swift +// Bad - vague usage +func add(_ observer: NSObject, for keyPath: String) +grid.add(self, for: graphics) // vague + +// Good - clear roles +func addObserver(_ observer: NSObject, forKeyPath path: String) +grid.addObserver(self, forKeyPath: graphics) // clear +``` + +### Strive for Fluent Usage + +- **Prefer method and function names that make use sites form grammatical English phrases.** + +```swift +x.insert(y, at: z) // "x, insert y at z" +x.subviews(havingColor: y) // "x's subviews having color y" +x.capitalizingNouns() // "x, capitalizing nouns" +``` + +- **Begin names of factory methods with "make"** + +```swift +x.makeIterator() +``` + +- **Name functions and methods according to their side-effects** + - Those without side-effects should read as noun phrases: `x.distance(to: y)` + - Those with side-effects should read as imperative verb phrases: `x.sort()` + +- **Name Mutating/nonmutating method pairs consistently** + - When naturally described by a verb, use imperative for mutating and past participle for nonmutating: + +```swift +// Mutating +x.sort() +x.append(y) + +// Nonmutating +z = x.sorted() +z = x.appending(y) +``` + +- **Uses of Boolean methods and properties should read as assertions about the receiver** + +```swift +x.isEmpty +line1.intersects(line2) +``` + +- **Protocols that describe what something is should read as nouns** + +```swift +Collection +``` + +- **Protocols that describe a capability should use suffixes `able`, `ible`, or `ing`** + +```swift +Equatable +ProgressReporting +``` + +### Use Terminology Well + +- **Avoid obscure terms** if a more common word conveys meaning just as well +- **Stick to the established meaning** if you do use a term of art +- **Avoid abbreviations** - they are effectively terms-of-art +- **Embrace precedent** - don't optimize for total beginners at the expense of existing culture + +## Conventions + +### General Conventions + +- **Document the complexity of any computed property that is not O(1)** +- **Prefer methods and properties to free functions** (exceptions: no obvious self, unconstrained generics, established domain notation) +- **Follow case conventions**: Types and protocols are `UpperCamelCase`, everything else is `lowerCamelCase` +- **Methods can share a base name** when they share the same basic meaning or operate in distinct domains + +### Parameters + +- **Choose parameter names to serve documentation** +- **Take advantage of defaulted parameters** when it simplifies common uses +- **Prefer to locate parameters with defaults toward the end** of the parameter list +- **If your API will run in production, prefer `#fileID`** over alternatives + +### Argument Labels + +- **Omit all labels when arguments can't be usefully distinguished** + +```swift +min(number1, number2) +zip(sequence1, sequence2) +``` + +- **In initializers that perform value preserving type conversions, omit the first argument label** + +```swift +Int64(someUInt32) +String(veryLargeNumber) +``` + +- **When the first argument forms part of a prepositional phrase, give it an argument label** + +```swift +x.removeBoxes(havingLength: 12) +``` + +- **Otherwise, if the first argument forms part of a grammatical phrase, omit its label** + +```swift +x.addSubview(y) +``` + +- **Label all other arguments** + +## Special Instructions + +- **Label tuple members and name closure parameters** where they appear in your API +- **Take extra care with unconstrained polymorphism** to avoid ambiguities in overload sets + +Example of resolving ambiguity: +```swift +// Ambiguous +public mutating func append(_ newElement: Element) +public mutating func append(_ newElements: S) + +// Clear +public mutating func append(_ newElement: Element) +public mutating func append(contentsOf newElements: S) +``` + +--- + +*Content was rephrased for compliance with licensing restrictions* diff --git a/.kiro/steering/swift-general.md b/.kiro/steering/swift-general.md new file mode 100644 index 000000000..1421493eb --- /dev/null +++ b/.kiro/steering/swift-general.md @@ -0,0 +1,104 @@ +You are a coding assistant--with access to tools--specializing +in analyzing codebases. Below is the content of the file the +user is working on. Your job is to to answer questions, provide +insights, and suggest improvements when the user asks questions. + +Do not answer with any code until you are sure the user has +provided all code snippets and type implementations required to +answer their question. + +Briefly--in as little text as possible--walk through the solution +in prose to identify types you need that are missing from the files +that have been sent to you. + +Whenever possible, favor Apple programming languages and +frameworks or APIs that are already available on Apple devices. +Whenever suggesting code, you should assume that the user wants +Swift, unless they show or tell you they are interested in +another language. + +Always prefer Swift, Objective-C, C, and C++ over alternatives. + +Pay close attention to the platform that this code is for. +For example, if you see clues that the user is writing a Mac +app, avoid suggesting iOS-only APIs. + +Refer to Apple platforms with their official names, like iOS, +iPadOS, macOS, watchOS and visionOS. Avoid mentioning specific +products and instead use these platform names. + +In most projects, you can also provide code examples using the new +Swift Testing framework that uses Swift Macros. An example of this +code is below: + +```swift + +import Testing + +// Optional, you can also just say `@Suite` with no parentheses. +@Suite("You can put a test suite name here, formatted as normal text.") +struct AddingTwoNumbersTests { + + @Test("Adding 3 and 7") + func add3And7() async throws { + let three = 3 + let seven = 7 + + // All assertions are written as "expect" statements now. + #expect(three + seven == 10, "The sums should work out.") + } + + @Test + func add3And7WithOptionalUnwrapping() async throws { + let three: Int? = 3 + let seven = 7 + + // Similar to `XCTUnwrap` + let unwrappedThree = try #require(three) + + let sum = three + seven + + #expect(sum == 10) + } + +} +``` +When asked to write unit tests, always prefer the new Swift testing framework over XCTest. + +In general, prefer the use of Swift Concurrency (async/await, +actors, etc.) over tools like Dispatch or Combine, but if the +user's code or words show you they may prefer something else, +you should be flexible to this preference. + +Sometimes, the user may provide specific code snippets for your +use. These may be things like the current file, a selection, other +files you can suggest changing, or +code that looks like generated Swift interfaces — which represent +things you should not try to change. + +However, this query will start without any additional context. + +When it makes sense, you should propose changes to existing code. +Whenever you are proposing changes to an existing file, +it is imperative that you repeat the entire file, without ever +eliding pieces, even if they will be kept identical to how they are +currently. To indicate that you are revising an existing file +in a code sample, put "```language:filename" before the revised +code. It is critical that you only propose replacing files that +have been sent to you. For example, if you are revising +FooBar.swift, you would say: + +```swift:FooBar.swift +// the entire code of the file with your changes goes here. +// Do not skip over anything. +``` + +However, less commonly, you will either need to make entirely new +things in new files or show how to write a kind of code generally. +When you are in this rarer circumstance, you can just show the +user a code snippet, with normal markdown: +```swift +// Swift code here +``` + + diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 000000000..35229f735 --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,87 @@ +# Technology Stack + +## Core Technologies + +- **Swift 6.x**: Primary programming language (minimum Swift 6.0) +- **SwiftNIO**: Asynchronous networking framework for HTTP client implementation +- **Swift Package Manager**: Dependency management and build system +- **Docker**: Required for cross-compilation to Amazon Linux 2 + +## Key Dependencies + +- `swift-nio`: Asynchronous event-driven network application framework +- `swift-log`: Logging API for Swift +- `swift-collections`: Additional collection types (DequeModule) +- `swift-service-lifecycle`: Service lifecycle management (optional trait) + +## Platform Requirements + +- **macOS**: Version 15 (Sequoia) or later for development +- **Target Runtime**: Amazon Linux 2 (provided.al2 runtime) +- **Architecture**: Supports both x86_64 and ARM64 (Apple Silicon) + +## Build System + +### Swift Package Manager Commands + +```bash +# Build the project +swift build + +# Run tests +swift test + +# Format code +swift format format --parallel --recursive --in-place ./Package.swift Examples/ Sources/ Tests/ + +# Generate documentation +swift package generate-documentation --target AWSLambdaRuntime + +# Preview documentation +swift package --disable-sandbox preview-documentation --target AWSLambdaRuntime +``` + +### Lambda-Specific Commands + +```bash +# Initialize a new Lambda function +swift package lambda-init --allow-writing-to-package-directory + +# Build and archive for AWS deployment +swift package archive --allow-network-connections docker + +# Add Lambda runtime dependency +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main +swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime +``` + +### Local Testing + +```bash +# Run locally (starts HTTP server on port 7000) +swift run + +# Test with custom endpoint +LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocations swift run + +# Invoke local function +curl -v --header "Content-Type: application/json" --data @events/test.json http://127.0.0.1:7000/invoke +``` + +### Cross-Platform Build + +```bash +# Build on Linux using Docker +docker run --rm -v $(pwd):/work swift:6.1 /bin/bash -c "cd /work && swift build && swift test" + +# Using container command (alternative) +CONTAINER=podman make build-linux +``` + +## Deployment Tools + +- **AWS CLI**: Command-line deployment +- **AWS SAM**: Serverless Application Model for infrastructure as code +- **AWS CDK**: Cloud Development Kit for programmatic infrastructure +- **Terraform**: Third-party infrastructure as code +- **Serverless Framework**: Third-party deployment framework \ No newline at end of file diff --git a/.licenseignore b/.licenseignore index fd35b5d6f..34fce131e 100644 --- a/.licenseignore +++ b/.licenseignore @@ -36,4 +36,5 @@ Package.resolved **/.npmignore **/*.json **/*.txt -*.toml \ No newline at end of file +*.toml +.kiro/* diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 000000000..ea8271df6 --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1 @@ +Plugins/AWSLambdaInitializer/Template.swift diff --git a/Examples/APIGatewayV1/README.md b/Examples/APIGatewayV1/README.md index 5f579a497..921037fd3 100644 --- a/Examples/APIGatewayV1/README.md +++ b/Examples/APIGatewayV1/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/APIGatewayV2+LambdaAuthorizer/README.md b/Examples/APIGatewayV2+LambdaAuthorizer/README.md index 597d00a54..502833a64 100644 --- a/Examples/APIGatewayV2+LambdaAuthorizer/README.md +++ b/Examples/APIGatewayV2+LambdaAuthorizer/README.md @@ -19,7 +19,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there are two ZIP files ready to deploy, one for the authorizer function and one for the business function. diff --git a/Examples/APIGatewayV2/README.md b/Examples/APIGatewayV2/README.md index ad33f3cd2..e7ffccee8 100644 --- a/Examples/APIGatewayV2/README.md +++ b/Examples/APIGatewayV2/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md index 11d7b7766..f3cebceea 100644 --- a/Examples/BackgroundTasks/README.md +++ b/Examples/BackgroundTasks/README.md @@ -25,39 +25,34 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip` -## Deploy with the AWS CLI +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ### Create the function ```bash -AWS_ACCOUNT_ID=012345678901 -aws lambda create-function \ ---function-name BackgroundTasks \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ ---environment "Variables={LOG_LEVEL=debug}" \ ---timeout 15 +swift package --allow-network-connections all:443 lambda-deploy ``` -> [!IMPORTANT] -> The timeout value must be bigger than the time it takes for your function to complete its background tasks. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish the tasks. Here, the sample function waits for 10 seconds and we set the timeout for 15 seconds. - -The `--environment` arguments sets the `LOG_LEVEL` environment variable to `debug`. This will ensure the debugging statements in the handler `context.logger.debug("...")` are printed in the Lambda function logs. +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. +> [!IMPORTANT] +> After deploying, update the function timeout to be bigger than the time it takes for your function to complete its background tasks. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish the tasks. Here, the sample function waits for 10 seconds so the timeout should be at least 15 seconds: +> ```bash +> aws lambda update-function-configuration \ +> --function-name BackgroundTasks \ +> --timeout 15 \ +> --environment "Variables={LOG_LEVEL=debug}" +> ``` -Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). +The `--environment` argument sets the `LOG_LEVEL` environment variable to `debug`. This will ensure the debugging statements in the handler `context.logger.debug("...")` are printed in the Lambda function logs. ### Invoke your Lambda function @@ -115,7 +110,7 @@ Type CTRL-C to stop tailing the logs. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name BackgroundTasks +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/CDK/README.md b/Examples/CDK/README.md index 37383df08..c0abfea61 100644 --- a/Examples/CDK/README.md +++ b/Examples/CDK/README.md @@ -12,7 +12,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/HelloJSON/README.md b/Examples/HelloJSON/README.md index d923c03aa..3b35f023d 100644 --- a/Examples/HelloJSON/README.md +++ b/Examples/HelloJSON/README.md @@ -21,7 +21,7 @@ The function return value will be encoded to a `HelloResponse` as your Lambda fu To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -29,24 +29,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -# Replace with your AWS Account ID -AWS_ACCOUNT_ID=012345678901 - -aws lambda create-function \ ---function-name HelloJSON \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to define the `AWS_ACCOUNT_ID` environment variable with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -76,7 +65,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name HelloJSON +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md index f3d48cd3b..8471157d8 100644 --- a/Examples/HelloWorld/README.md +++ b/Examples/HelloWorld/README.md @@ -46,8 +46,7 @@ curl -d '"seb"' http://127.0.0.1:7000/invoke To build & archive the package, type the following commands. ```bash -swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -55,21 +54,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to replace with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -99,7 +90,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HelloWorldNoTraits/README.md b/Examples/HelloWorldNoTraits/README.md index 59c493732..7706a39e2 100644 --- a/Examples/HelloWorldNoTraits/README.md +++ b/Examples/HelloWorldNoTraits/README.md @@ -53,7 +53,7 @@ To build & archive the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -61,21 +61,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to replace with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -103,7 +95,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/HummingbirdLambda/README.md b/Examples/HummingbirdLambda/README.md index 8291b6e43..6c2d8b683 100644 --- a/Examples/HummingbirdLambda/README.md +++ b/Examples/HummingbirdLambda/README.md @@ -18,7 +18,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/JSONLogging/README.md b/Examples/JSONLogging/README.md index 12c83ad62..4fbc6dc0d 100644 --- a/Examples/JSONLogging/README.md +++ b/Examples/JSONLogging/README.md @@ -84,7 +84,7 @@ AWS_LAMBDA_LOG_FORMAT=JSON swift run ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` The deployment package will be at: @@ -128,17 +128,19 @@ sam deploy --guided ## Deploy with AWS CLI -As an alternative to SAM, you can use the AWS CLI: +As an alternative to SAM, you can use the `lambda-deploy` plugin: ```bash -ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) -aws lambda create-function \ - --function-name JSONLoggingExample \ - --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip \ - --runtime provided.al2023 \ - --handler swift.bootstrap \ - --architectures arm64 \ - --role arn:aws:iam::${ACCOUNT_ID}:role/lambda_basic_execution \ +swift package --allow-network-connections all:443 lambda-deploy +``` + +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. + +After deploying, configure logging format: + +```bash +aws lambda update-function-configuration \ + --function-name JSONLogging \ --logging-config LogFormat=JSON,ApplicationLogLevel=DEBUG,SystemLogLevel=INFO ``` @@ -256,8 +258,8 @@ The runtime maps Swift's `Logger.Level` to AWS Lambda log levels: # SAM deployment sam delete -# AWS CLI deployment -aws lambda delete-function --function-name JSONLoggingExample +# Plugin deployment +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Important Notes diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md index 630f93d65..1c9015b4c 100644 --- a/Examples/ManagedInstances/README.md +++ b/Examples/ManagedInstances/README.md @@ -28,7 +28,7 @@ arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 ```bash # Build and package the Swift Lambda function -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build # Change the values below to match your setup REGION=us-west-2 diff --git a/Examples/MultiSourceAPI/README.md b/Examples/MultiSourceAPI/README.md index f5ae855e8..5e7015cec 100644 --- a/Examples/MultiSourceAPI/README.md +++ b/Examples/MultiSourceAPI/README.md @@ -13,7 +13,7 @@ Based on the successfully decoded type, it returns an appropriate response. ## Building ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` ## Deploying diff --git a/Examples/MultiTenant/README.md b/Examples/MultiTenant/README.md index 3bf99a978..7fdc6870a 100644 --- a/Examples/MultiTenant/README.md +++ b/Examples/MultiTenant/README.md @@ -166,7 +166,7 @@ requestParameters: 1. **Build the Lambda function**: ```bash - swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 + swift package --allow-network-connections docker lambda-build ``` 2. **Deploy using SAM**: diff --git a/Examples/README.md b/Examples/README.md index 725f6c88c..4f27b365c 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -6,9 +6,9 @@ This directory contains example code for Lambda functions. - When developing on macOS, be sure you use macOS 15 (Sequoia) or a more recent macOS version. -- To build and archive your Lambda functions, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/). +- To build and archive your Lambda functions, you need to [install docker](https://docs.docker.com/desktop/install/mac-install/) or Apple [container](https://github.com/apple/container). -- To deploy your Lambda functions and invoke them, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). +- To deploy your Lambda functions and invoke them, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (run `aws configure` to set up your credentials). - Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. diff --git a/Examples/ResourcesPackaging/README.md b/Examples/ResourcesPackaging/README.md index 53960fece..23383ce41 100644 --- a/Examples/ResourcesPackaging/README.md +++ b/Examples/ResourcesPackaging/README.md @@ -49,7 +49,7 @@ curl -d '"hello"' http://127.0.0.1:7000/invoke To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -57,21 +57,13 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa ## Deploy -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x86_64`. - -Be sure to replace with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. ## Invoke your Lambda function @@ -99,7 +91,7 @@ This should output the following result. When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name MyLambda +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## ⚠️ Security and Reliability Notice diff --git a/Examples/S3EventNotifier/README.md b/Examples/S3EventNotifier/README.md index 08cdce561..b2c2ae023 100644 --- a/Examples/S3EventNotifier/README.md +++ b/Examples/S3EventNotifier/README.md @@ -23,7 +23,7 @@ To build & archive the package you can use the following commands: ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`. @@ -33,23 +33,13 @@ If there are no errors, a ZIP file should be ready to deploy, located at `.build > [!IMPORTANT] > The Lambda function and the S3 bucket must be located in the same AWS Region. In the code below, we use `eu-west-1` (Ireland). -To deploy the Lambda function, you can use the `aws` command line: +To deploy the Lambda function, you can use the `lambda-deploy` plugin: ```bash -REGION=eu-west-1 -aws lambda create-function \ - --region "${REGION}" \ - --function-name S3EventNotifier \ - --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip \ - --runtime provided.al2023 \ - --handler provided \ - --architectures arm64 \ - --role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy --region eu-west-1 ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to define `REGION` with the region where you want to deploy your Lambda function and replace `` with your actual AWS account ID (for example: 012345678901). +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. Besides deploying the Lambda function you also need to create the S3 bucket and configure it to send events to the Lambda function. You can do this using the following commands: diff --git a/Examples/S3_AWSSDK/README.md b/Examples/S3_AWSSDK/README.md index 45c585b46..5d546cc7f 100644 --- a/Examples/S3_AWSSDK/README.md +++ b/Examples/S3_AWSSDK/README.md @@ -25,7 +25,7 @@ To build the package, type the following commands. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/S3_Soto/README.md b/Examples/S3_Soto/README.md index 92af31801..e378ae595 100644 --- a/Examples/S3_Soto/README.md +++ b/Examples/S3_Soto/README.md @@ -25,7 +25,7 @@ To build the package, type the following command. ```bash swift build -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/ServiceLifecycle+Postgres/README.md b/Examples/ServiceLifecycle+Postgres/README.md index f03e48717..5cd6520b7 100644 --- a/Examples/ServiceLifecycle+Postgres/README.md +++ b/Examples/ServiceLifecycle+Postgres/README.md @@ -80,7 +80,7 @@ The Lambda function uses the following environment variables for database connec 1. **Build the Lambda function:** ```bash - swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 + swift package --allow-network-connections docker lambda-build ``` 2. **Deploy with SAM:** diff --git a/Examples/Streaming+APIGateway/README.md b/Examples/Streaming+APIGateway/README.md index badecccef..423a2bda8 100644 --- a/Examples/Streaming+APIGateway/README.md +++ b/Examples/Streaming+APIGateway/README.md @@ -66,7 +66,7 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. diff --git a/Examples/Streaming+Codable/README.md b/Examples/Streaming+Codable/README.md index db562d955..119dd9fcd 100644 --- a/Examples/Streaming+Codable/README.md +++ b/Examples/Streaming+Codable/README.md @@ -78,7 +78,7 @@ Key features demonstrated: To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -107,65 +107,17 @@ curl -v \ http://127.0.0.1:7000/invoke ``` -## Deploy with the AWS CLI +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin with a Function URL. -### Step 1: Create the function +### Step 1: Deploy the function with a URL ```bash -# Replace with your AWS Account ID -AWS_ACCOUNT_ID=012345678901 -aws lambda create-function \ ---function-name StreamingFromEvent \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingFromEvent/StreamingFromEvent.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy --with-url ``` -> [!IMPORTANT] -> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function stream responses during 10 seconds and we set the timeout for 15 seconds. - -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). - -### Step 2: Give permission to invoke that function through a URL - -Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. - -```bash -aws lambda add-permission \ - --function-name StreamingFromEvent \ - --action lambda:InvokeFunctionUrl \ - --principal ${AWS_ACCOUNT_ID} \ - --function-url-auth-type AWS_IAM \ - --statement-id allowURL -``` - -### Step 3: Create the URL - -This creates [a URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. - -```bash -aws lambda create-function-url-config \ - --function-name StreamingFromEvent \ - --auth-type AWS_IAM \ - --invoke-mode RESPONSE_STREAM -``` -This call returns various information, including the URL to invoke your function. - -```json -{ - "FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", - "FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingFromEvent", - "AuthType": "AWS_IAM", - "CreationTime": "2024-10-22T07:57:23.112599Z", - "InvokeMode": "RESPONSE_STREAM" -} -``` +This creates the Lambda function, provisions the necessary IAM role, configures a Function URL with IAM authentication, and uploads the deployment package. The output will include the Function URL and a ready-to-use `curl` command. ### Invoke your Lambda function @@ -205,7 +157,7 @@ This should output the following result, with configurable delays between each m When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name StreamingFromEvent +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## Deploy with AWS SAM diff --git a/Examples/Streaming+FunctionUrl/README.md b/Examples/Streaming+FunctionUrl/README.md index be09483ec..2238bfdc0 100644 --- a/Examples/Streaming+FunctionUrl/README.md +++ b/Examples/Streaming+FunctionUrl/README.md @@ -68,7 +68,7 @@ Once the struct is created and the `handle(...)` method is defined, the sample c To build & archive the package, type the following commands. ```bash -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` If there is no error, there is a ZIP file ready to deploy. @@ -88,66 +88,25 @@ curl -v --output response.txt \ http://127.0.0.1:7000/invoke ``` -## Deploy with the AWS CLI +## Deploy with the lambda-deploy plugin -Here is how to deploy using the `aws` command line. +Here is how to deploy using the `lambda-deploy` plugin with a Function URL. -### Step 1: Create the function +### Step 1: Deploy the function with a URL ```bash -# Replace with your AWS Account ID -AWS_ACCOUNT_ID=012345678901 -aws lambda create-function \ ---function-name StreamingNumbers \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ ---timeout 15 +swift package --allow-network-connections all:443 lambda-deploy --with-url ``` -> [!IMPORTANT] -> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function stream responses during 3 seconds and we set the timeout for 5 seconds. - -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). - -### Step2: Give permission to invoke that function through an URL - -Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. - -```bash -aws lambda add-permission \ - --function-name StreamingNumbers \ - --action lambda:InvokeFunctionUrl \ - --principal ${AWS_ACCOUNT_ID} \ - --function-url-auth-type AWS_IAM \ - --statement-id allowURL -``` - -### Step3: Create the URL - -This creates [a URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. +This creates the Lambda function, provisions the necessary IAM role, configures a Function URL with IAM authentication, and uploads the deployment package. The output will include the Function URL and a ready-to-use `curl` command. -```bash -aws lambda create-function-url-config \ - --function-name StreamingNumbers \ - --auth-type AWS_IAM \ - --invoke-mode RESPONSE_STREAM -``` -This calls return various information, including the URL to invoke your function. - -```json -{ - "FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", - "FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingNumbers", - "AuthType": "AWS_IAM", - "CreationTime": "2024-10-22T07:57:23.112599Z", - "InvokeMode": "RESPONSE_STREAM" -} -``` +> [!IMPORTANT] +> After deploying, update the function timeout to be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, the sample function streams responses during 3 seconds so set the timeout for at least 5 seconds: +> ```bash +> aws lambda update-function-configuration \ +> --function-name StreamingNumbers \ +> --timeout 15 +> ``` ### Invoke your Lambda function @@ -187,7 +146,7 @@ Streaming complete! When done testing, you can delete the Lambda function with this command. ```bash -aws lambda delete-function --function-name StreamingNumbers +swift package --allow-network-connections all:443 lambda-deploy --delete ``` ## Deploy with AWS SAM diff --git a/Examples/_MyFirstFunction/clean.sh b/Examples/_MyFirstFunction/clean.sh index 7be3ac400..5108b7b83 100755 --- a/Examples/_MyFirstFunction/clean.sh +++ b/Examples/_MyFirstFunction/clean.sh @@ -14,24 +14,19 @@ ## ##===----------------------------------------------------------------------===## -echo "This script deletes the Lambda function and the IAM role created in the previous step and deletes the project files." -read -r -p "Are you you sure you want to delete everything that was created? [y/n] " continue +echo "This script deletes the Lambda function and IAM role, then removes local project files." +read -r -p "Are you sure you want to delete everything? [y/n] " continue if [[ ! $continue =~ ^[Yy]$ ]]; then echo "OK, try again later when you feel ready" exit 1 fi -echo "🚀 Deleting the Lambda function and the role" -aws lambda delete-function --function-name MyLambda -aws iam detach-role-policy \ - --role-name lambda_basic_execution \ - --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole -aws iam delete-role --role-name lambda_basic_execution +echo "🗑️ Deleting the Lambda function and IAM role" +swift package --allow-network-connections all:443 lambda-deploy --delete || true -echo "🚀 Deleting the project files" +echo "🧹 Deleting local project files" rm -rf .build rm -rf ./Sources -rm trust-policy.json -rm Package.swift Package.resolved +rm -f Package.swift Package.resolved -echo "🎉 Done! Your project is cleaned up and ready for a fresh start." \ No newline at end of file +echo "🎉 Done! Your project is cleaned up and ready for a fresh start." diff --git a/Examples/_MyFirstFunction/create_and_deploy_function.sh b/Examples/_MyFirstFunction/create_and_deploy_function.sh deleted file mode 100755 index 398155ca0..000000000 --- a/Examples/_MyFirstFunction/create_and_deploy_function.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright SwiftAWSLambdaRuntime project authors -## Copyright (c) Amazon.com, Inc. or its affiliates. -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -# Stop the script execution if an error occurs -set -e -o pipefail - -check_prerequisites() { - # check if docker is installed - which docker > /dev/null || (echo "Docker is not installed. Please install Docker and try again." && exit 1) - - # check if aws cli is installed - which aws > /dev/null || (echo "AWS CLI is not installed. Please install AWS CLI and try again." && exit 1) - - # check if user has an access key and secret access key - echo "This script creates and deploys a Lambda function on your AWS Account. - - You must have an AWS account and know an AWS access key, secret access key, and an optional session token. - These values are read from '~/.aws/credentials'. - " - - read -r -p "Are you ready to create your first Lambda function in Swift? [y/n] " continue - if [[ ! $continue =~ ^[Yy]$ ]]; then - echo "OK, try again later when you feel ready" - exit 1 - fi -} - -create_lambda_execution_role() { - role_name=$1 - - # Allow the Lambda service to assume the IAM role - cat < trust-policy.json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] -} -EOF - - # Create the IAM role - echo "🔐 Create the IAM role for the Lambda function" - aws iam create-role \ - --role-name "${role_name}" \ - --assume-role-policy-document file://trust-policy.json > /dev/null 2>&1 - - # Attach basic permissions to the role - # The AWSLambdaBasicExecutionRole policy grants permissions to write logs to CloudWatch Logs - echo "🔒 Attach basic permissions to the role" - aws iam attach-role-policy \ - --role-name "${role_name}" \ - --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole > /dev/null 2>&1 - - echo "⏰ Waiting 10 secs for IAM role to propagate..." - sleep 10 -} - -create_swift_project() { - echo "⚡️ Create your Swift Lambda project" - swift package init --type executable --name MyLambda > /dev/null - - echo "📦 Add the AWS Lambda Swift runtime to your project" - # The following commands are commented out until the `lambad-init` plugin will be release - # swift package add-dependency https://github.com/awslabs/swift-aws-lambda-runtime.git --from 2.0.0 - # swift package add-dependency https://github.com/awslabs/swift-aws-lambda-events.git --from 1.0.0 - # swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime - # swift package add-target-dependency AWSLambdaEvents MyLambda --package swift-aws-lambda-events - cat < Package.swift -// swift-tools-version:6.3 - -import PackageDescription - -let package = Package( - name: "swift-aws-lambda-runtime-example", - platforms: [.macOS(.v15)], - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]) - ], - dependencies: [ - .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0") - ], - targets: [ - .executableTarget( - name: "MyLambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") - ], - path: "." - ) - ] -) -EOF - - echo "📝 Write the Swift code" - # The following command is commented out until the `lambad-init` plugin will be release - # swift package lambda-init --allow-writing-to-package-directory - cat < Sources/main.swift -import AWSLambdaRuntime - -let runtime = LambdaRuntime { - (event: String, context: LambdaContext) in - "Hello \(event)" -} - -try await runtime.run() -EOF - - echo "📦 Compile and package the function for deployment (this might take a while)" - swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 > /dev/null 2>&1 -} - -deploy_lambda_function() { - echo "🚀 Deploy to AWS Lambda" - - # retrieve your AWS Account ID - echo "🔑 Retrieve your AWS Account ID" - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - export AWS_ACCOUNT_ID - - # Check if the role already exists - echo "🔍 Check if a Lambda execution IAM role already exists" - aws iam get-role --role-name lambda_basic_execution > /dev/null 2>&1 || create_lambda_execution_role lambda_basic_execution - - # Create the Lambda function - echo "🚀 Create the Lambda function" - aws lambda create-function \ - --function-name MyLambda \ - --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ - --runtime provided.al2023 \ - --handler provided \ - --architectures "$(uname -m)" \ - --role arn:aws:iam::"${AWS_ACCOUNT_ID}":role/lambda_basic_execution > /dev/null 2>&1 - - echo "⏰ Waiting 10 secs for the Lambda function to be ready..." - sleep 10 -} - -invoke_lambda_function() { - # Invoke the Lambda function - echo "🔗 Invoke the Lambda function" - aws lambda invoke \ - --function-name MyLambda \ - --cli-binary-format raw-in-base64-out \ - --payload '"Lambda Swift"' \ - output.txt > /dev/null 2>&1 - - echo "👀 Your Lambda function returned:" - cat output.txt && rm output.txt -} - -main() { - # - # Check prerequisites - # - check_prerequisites - - # - # Create the Swift project - # - create_swift_project - - # - # Now the function is ready to be deployed to AWS Lambda - # - deploy_lambda_function - - # - # Invoke the Lambda function - # - invoke_lambda_function - - echo "" - echo "🎉 Done! Your first Lambda function in Swift is now deployed on AWS Lambda. 🚀" -} - -main "$@" \ No newline at end of file diff --git a/Examples/_MyFirstFunction/create_function.sh b/Examples/_MyFirstFunction/create_function.sh new file mode 100755 index 000000000..bea1173e9 --- /dev/null +++ b/Examples/_MyFirstFunction/create_function.sh @@ -0,0 +1,71 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# Stop the script execution if an error occurs +set -e -o pipefail + +# check if docker is installed +which docker > /dev/null || (echo "Docker is not installed. Please install Docker and try again." && exit 1) + +# check if aws cli is installed +which aws > /dev/null || (echo "AWS CLI is not installed. Please install AWS CLI and try again." && exit 1) + +echo "This script creates, builds, deploys, and invokes a Lambda function on your AWS Account. + +You must have an AWS account and have run 'aws configure' to set up your credentials in ~/.aws/. +" + +printf "Are you ready to create your first Lambda function in Swift? [y/n] " +read -r continue +case $continue in + [Yy]*) ;; + *) echo "OK, try again later when you feel ready"; exit 1 ;; +esac + +echo "⚡️ Create your Swift command line project" +swift package init --type executable --name MyLambda + +echo "📦 Add the AWS Lambda Swift runtime to your project" +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch main +swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --branch main +swift package add-target-dependency AWSLambdaRuntime MyLambda --package swift-aws-lambda-runtime +swift package add-target-dependency AWSLambdaEvents MyLambda --package swift-aws-lambda-events + +echo "📝 Scaffold the Lambda function code" +swift package lambda-init --allow-writing-to-package-directory + +echo "📦 Compile and package the function for deployment (this might take a while)" +swift package --allow-network-connections docker lambda-build + +echo "🚀 Deploy to AWS Lambda" +swift package --allow-network-connections all:443 lambda-deploy + +echo "" +echo "⏰ Waiting 5 secs for the Lambda function to be ready..." +sleep 5 + +echo "🔗 Invoke the Lambda function" +aws lambda invoke \ + --function-name MyLambda \ + --payload "$(echo '{"name":"World","age":30}' | base64)" \ + /tmp/out.json > /dev/null && cat /tmp/out.json + +echo "" +echo "" +echo "🎉 Done! Your first Lambda function in Swift is deployed on AWS Lambda." +echo "" +echo "To delete the function and clean up:" +echo " swift package --allow-network-connections all:443 lambda-deploy --delete" diff --git a/Package.swift b/Package.swift index 6bfe02b48..8f92d10c9 100644 --- a/Package.swift +++ b/Package.swift @@ -13,9 +13,27 @@ let package = Package( name: "swift-aws-lambda-runtime", products: [ .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + + // + // The plugins + // 'lambda-init' creates a new Lambda function + // 'lambda-build' packages the Lambda function + // 'lambda-deploy' deploys the Lambda function + // + // Plugins requires Linux or at least macOS v15 + // + + // plugin to create a new Lambda function, based on a template + .plugin(name: "AWSLambdaInitializer", targets: ["AWSLambdaInitializer"]), + // plugin to package the lambda, creating an archive that can be uploaded to AWS - // requires Linux or at least macOS v15 + .plugin(name: "AWSLambdaBuilder", targets: ["AWSLambdaBuilder"]), + + // legacy 'archive' command — deprecated passthrough to lambda-build .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + + // plugin to deploy a Lambda function + .plugin(name: "AWSLambdaDeployer", targets: ["AWSLambdaDeployer"]), ], traits: [ "ManagedRuntimeSupport", @@ -36,6 +54,10 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.5.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.11.0"), + // .package(url: "https://github.com/soto-project/soto-core.git", from: "7.13.0"), + .package(url: "https://github.com/sebsto/soto-core.git", branch: "remove-platforms-use-availability-macro"), + // .package(name: "soto-core", path: "../../soto-core") + ], targets: [ .target( @@ -54,13 +76,31 @@ let package = Package( ], swiftSettings: defaultSwiftSettings ), + .plugin( + name: "AWSLambdaInitializer", + capability: .command( + intent: .custom( + verb: "lambda-init", + description: + "Create a new Lambda function in the current project directory." + ), + permissions: [ + .writeToPackageDirectory(reason: "Create a file with an HelloWorld Lambda function.") + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + // keep this one (with "archive") to not break workflows + // Uses its own Plugin.swift that emits a deprecation warning then delegates to the helper .plugin( name: "AWSLambdaPackager", capability: .command( intent: .custom( verb: "archive", description: - "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + "Archive the Lambda binary and prepare it for uploading to AWS. (Deprecated: use lambda-build instead)" ), permissions: [ .allowNetworkConnections( @@ -68,7 +108,57 @@ let package = Package( reason: "This plugin uses Docker to create the AWS Lambda ZIP package." ) ] - ) + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + .plugin( + name: "AWSLambdaBuilder", + capability: .command( + intent: .custom( + verb: "lambda-build", + description: + "Compile and archive (zip) the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to compile code for Amazon Linux." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + .plugin( + name: "AWSLambdaDeployer", + capability: .command( + intent: .custom( + verb: "lambda-deploy", + description: + "Deploy the Lambda function. You must have an AWS account and an access key and secret access key." + ), + permissions: [ + .allowNetworkConnections( + scope: .all(ports: [443]), + reason: "This plugin uses the AWS Lambda API to deploy the function." + ) + ] + ), + dependencies: [ + .target(name: "AWSLambdaPluginHelper") + ] + ), + .executableTarget( + name: "AWSLambdaPluginHelper", + dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "SotoCore", package: "soto-core"), + ], + swiftSettings: defaultSwiftSettings ), .testTarget( name: "AWSLambdaRuntimeTests", @@ -91,5 +181,14 @@ let package = Package( ], swiftSettings: defaultSwiftSettings ), + .testTarget( + name: "AWSLambdaPluginHelperTests", + dependencies: [ + .byName(name: "AWSLambdaPluginHelper"), + .product(name: "Logging", package: "swift-log"), + ], + swiftSettings: defaultSwiftSettings + ), + ] ) diff --git a/Plugins/AWSLambdaBuilder/Plugin.swift b/Plugins/AWSLambdaBuilder/Plugin.swift new file mode 100644 index 000000000..ab4d64416 --- /dev/null +++ b/Plugins/AWSLambdaBuilder/Plugin.swift @@ -0,0 +1,180 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +struct AWSLambdaPackager: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + + // values to pass to the AWSLambdaPluginHelper + let outputDirectory: URL + let products: [Product] + let buildConfiguration: PackageManager.BuildConfiguration + let packageID: String = context.package.id + let packageDisplayName = context.package.displayName + let packageDirectory = context.package.directoryURL + let dockerToolPath = try context.tool(named: "docker").url + let zipToolPath = try context.tool(named: "zip").url + + // extract arguments that require PluginContext to fully resolve + // resolve them here and pass them to the AWSLambdaPluginHelper as arguments + var argumentExtractor = ArgumentExtractor(arguments) + + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + + if let outputPath = outputPathArgument.first { + #if os(Linux) + var isDirectory: Bool = false + #else + var isDirectory: ObjCBool = false + #endif + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) + else { + throw BuilderErrors.invalidArgument("invalid output directory '\(outputPath)'") + } + outputDirectory = URL(string: outputPath)! + } else { + outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") + } + + let explicitProducts = !productsArgument.isEmpty + if explicitProducts { + let _products = try context.package.products(named: productsArgument) + for product in _products { + guard product is ExecutableProduct else { + throw BuilderErrors.invalidArgument("product named '\(product.name)' is not an executable product") + } + } + products = _products + + } else { + products = context.package.products.filter { $0 is ExecutableProduct } + } + + if let _buildConfigurationName = configurationArgument.first { + guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: _buildConfigurationName) else { + throw BuilderErrors.invalidArgument("invalid build configuration named '\(_buildConfigurationName)'") + } + buildConfiguration = _buildConfiguration + } else { + buildConfiguration = .release + } + + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin + // launch the build, then call the helper just for the ZIP part + + let tool = try context.tool(named: "AWSLambdaPluginHelper") + let args = + [ + "build", + "--output-path", outputDirectory.path(), + "--products", products.map { $0.name }.joined(separator: ","), + "--configuration", buildConfiguration.rawValue, + "--package-id", packageID, + "--package-display-name", packageDisplayName, + "--package-directory", packageDirectory.path(), + "--docker-tool-path", dockerToolPath.path, + "--zip-tool-path", zipToolPath.path, + ] + arguments + + // Invoke the plugin helper, passing the current environment so that + // AWS credentials and HOME are available to the subprocess. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() + process.waitUntilExit() + + // Check whether the subprocess invocation was successful. + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") + } + } + + // TODO: When running on Amazon Linux 2, we have to build directly from the plugin + // private func build( + // packageIdentity: Package.ID, + // products: [Product], + // buildConfiguration: PackageManager.BuildConfiguration, + // verboseLogging: Bool + // ) throws -> [LambdaProduct: URL] { + // print("-------------------------------------------------------------------------") + // print("building \"\(packageIdentity)\"") + // print("-------------------------------------------------------------------------") + // + // var results = [LambdaProduct: URL]() + // for product in products { + // print("building \"\(product.name)\"") + // var parameters = PackageManager.BuildParameters() + // parameters.configuration = buildConfiguration + // parameters.otherSwiftcFlags = ["-static-stdlib"] + // parameters.logging = verboseLogging ? .verbose : .concise + // + // let result = try packageManager.build( + // .product(product.name), + // parameters: parameters + // ) + // guard let artifact = result.executableArtifact(for: product) else { + // throw Errors.productExecutableNotFound(product.name) + // } + // results[.init(product)] = artifact.url + // } + // return results + // } + + // private func isAmazonLinux2() -> Bool { + // if let data = FileManager.default.contents(atPath: "/etc/system-release"), + // let release = String(data: data, encoding: .utf8) + // { + // return release.hasPrefix("Amazon Linux release 2") + // } else { + // return false + // } + // } +} + +private enum BuilderErrors: Error, CustomStringConvertible { + case invalidArgument(String) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + } + } +} + +extension PackageManager.BuildResult { + // find the executable produced by the build + func executableArtifact(for product: Product) -> PackageManager.BuildResult.BuiltArtifact? { + let executables = self.builtArtifacts.filter { + $0.kind == .executable && $0.url.lastPathComponent == product.name + } + guard !executables.isEmpty else { + return nil + } + guard executables.count == 1, let executable = executables.first else { + return nil + } + return executable + } +} diff --git a/Plugins/AWSLambdaDeployer/Plugin.swift b/Plugins/AWSLambdaDeployer/Plugin.swift new file mode 100644 index 000000000..d0237b369 --- /dev/null +++ b/Plugins/AWSLambdaDeployer/Plugin.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +struct AWSLambdaDeployer: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + + let tool = try context.tool(named: "AWSLambdaPluginHelper") + + // Resolve products: use --products if provided, otherwise default to all executable targets + var argumentExtractor = ArgumentExtractor(arguments) + let productsArgument = argumentExtractor.extractOption(named: "products") + + let products: [Product] + if !productsArgument.isEmpty { + products = try context.package.products(named: productsArgument) + } else { + products = context.package.products.filter { $0 is ExecutableProduct } + } + + let productNames = products.map { $0.name }.joined(separator: ",") + + let args = ["deploy", "--products", productNames] + arguments + + // Invoke the plugin helper, passing the current environment so that + // AWS credentials (env vars, HOME for ~/.aws/credentials) are available. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() + process.waitUntilExit() + + // Check whether the subprocess invocation was successful. + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") + } + } + +} diff --git a/Plugins/AWSLambdaInitializer/Plugin.swift b/Plugins/AWSLambdaInitializer/Plugin.swift new file mode 100644 index 000000000..1f0d04505 --- /dev/null +++ b/Plugins/AWSLambdaInitializer/Plugin.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +struct AWSLambdaPackager: CommandPlugin { + + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { + let tool = try context.tool(named: "AWSLambdaPluginHelper") + + let args = ["init", "--dest-dir", context.package.directoryURL.path()] + arguments + + // Invoke the plugin helper, passing the current environment. + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() + process.waitUntilExit() + + // Check whether the subprocess invocation was successful. + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") + } + } +} diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift index a0f7534c7..ed862b61f 100644 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -17,475 +17,33 @@ import Foundation import PackagePlugin @main -@available(macOS 15.0, *) struct AWSLambdaPackager: CommandPlugin { - func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - let configuration = try Configuration(context: context, arguments: arguments) - - if configuration.help { - self.displayHelpMessage() - return - } - - guard !configuration.products.isEmpty else { - throw Errors.unknownProduct("no appropriate products found to package") - } - - if configuration.products.count > 1 && !configuration.explicitProducts { - let productNames = configuration.products.map(\.name) - print( - "No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'" - ) - } - - // display deprecation warning when building on or for Amazon Linux 2 - if self.isAmazonLinux(.al2) - || (configuration.baseDockerImage.contains("amazonlinux2") - && !configuration.baseDockerImage.contains("amazonlinux2023")) - { - self.displayDeprecationWarning() - } - - let builtProducts: [LambdaProduct: URL] - if self.isAmazonLinux(.al2) || self.isAmazonLinux(.al2023) { - // native build on Amazon Linux - builtProducts = try self.build( - packageIdentity: context.package.id, - products: configuration.products, - buildConfiguration: configuration.buildConfiguration, - verboseLogging: configuration.verboseLogging - ) - } else { - // build with docker - builtProducts = try self.buildInDocker( - packageIdentity: context.package.id, - packageDirectory: context.package.directoryURL, - products: configuration.products, - toolsProvider: { name in try context.tool(named: name).url }, - outputDirectory: configuration.outputDirectory, - containerCLI: configuration.containerCLI, - baseImage: configuration.baseDockerImage, - disableDockerImageUpdate: configuration.disableDockerImageUpdate, - buildConfiguration: configuration.buildConfiguration, - verboseLogging: configuration.verboseLogging - ) - } - - // create the archive - let archives = try self.package( - packageName: context.package.displayName, - products: builtProducts, - toolsProvider: { name in try context.tool(named: name).url }, - outputDirectory: configuration.outputDirectory, - verboseLogging: configuration.verboseLogging - ) - - print( - "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" - ) - for (product, archivePath) in archives { - print(" * \(product.name) at \(archivePath.path())") - } - } - - private func buildInDocker( - packageIdentity: Package.ID, - packageDirectory: URL, - products: [Product], - toolsProvider: (String) throws -> URL, - outputDirectory: URL, - containerCLI: ContainerCLI, - baseImage: String, - disableDockerImageUpdate: Bool, - buildConfiguration: PackageManager.BuildConfiguration, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - let containerCLIPath = try toolsProvider(containerCLI.executableName) - - print("-------------------------------------------------------------------------") - print("building \"\(packageIdentity)\" in \(containerCLI.displayName)") - print("-------------------------------------------------------------------------") - - if !disableDockerImageUpdate { - // update the underlying image, if necessary - print("updating \"\(baseImage)\" image") - try Utils.execute( - executable: containerCLIPath, - arguments: containerCLI.pullArguments(image: baseImage), - logLevel: verboseLogging ? .debug : .output - ) - } - - // get the build output path - let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" - let dockerBuildOutputPath = try Utils.execute( - executable: containerCLIPath, - arguments: containerCLI.runArguments( - baseImage: baseImage, - workingDirectory: "/workspace", - mounts: ["\(packageDirectory.path()):/workspace"], - env: nil, - command: buildOutputPathCommand - ), - logLevel: verboseLogging ? .debug : .silent - ) - guard let buildPathOutput = dockerBuildOutputPath.split(separator: "\n").last else { - throw Errors.failedParsingDockerOutput(dockerBuildOutputPath) - } - let buildOutputPath = URL( - string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) - )! - - // build the products - var builtProducts = [LambdaProduct: URL]() - for product in products { - print("building \"\(product.name)\"") - let buildCommand = - "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" - if let localPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] { - // when developing locally, we must have the full swift-aws-lambda-runtime project in the container - // because Examples' Package.swift have a dependency on ../.. - // just like Package.swift's examples assume ../.., we assume we are two levels below the root project - let slice = packageDirectory.pathComponents.suffix(2) - try Utils.execute( - executable: containerCLIPath, - arguments: containerCLI.runArguments( - baseImage: baseImage, - workingDirectory: "/workspace/\(slice.joined(separator: "/"))", - mounts: ["\(packageDirectory.path())../..:/workspace"], - env: ["LAMBDA_USE_LOCAL_DEPS": localPath], - command: buildCommand - ), - logLevel: verboseLogging ? .debug : .output - ) - } else { - try Utils.execute( - executable: containerCLIPath, - arguments: containerCLI.runArguments( - baseImage: baseImage, - workingDirectory: "/workspace", - mounts: ["\(packageDirectory.path()):/workspace"], - env: nil, - command: buildCommand - ), - logLevel: verboseLogging ? .debug : .output - ) - } - let productPath = buildOutputPath.appending(path: product.name) - - guard FileManager.default.fileExists(atPath: productPath.path()) else { - Diagnostics.error("expected '\(product.name)' binary at \"\(productPath.path())\"") - throw Errors.productExecutableNotFound(product.name) - } - builtProducts[.init(product)] = productPath - } - return builtProducts - } - - private func build( - packageIdentity: Package.ID, - products: [Product], - buildConfiguration: PackageManager.BuildConfiguration, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - print("-------------------------------------------------------------------------") - print("building \"\(packageIdentity)\"") - print("-------------------------------------------------------------------------") - - var results = [LambdaProduct: URL]() - for product in products { - print("building \"\(product.name)\"") - var parameters = PackageManager.BuildParameters() - parameters.configuration = buildConfiguration - parameters.otherSwiftcFlags = ["-static-stdlib"] - parameters.logging = verboseLogging ? .verbose : .concise - - let result = try packageManager.build( - .product(product.name), - parameters: parameters - ) - guard let artifact = result.executableArtifact(for: product) else { - throw Errors.productExecutableNotFound(product.name) - } - results[.init(product)] = artifact.url - } - return results - } - - // TODO: explore using ziplib or similar instead of shelling out - private func package( - packageName: String, - products: [LambdaProduct: URL], - toolsProvider: (String) throws -> URL, - outputDirectory: URL, - verboseLogging: Bool - ) throws -> [LambdaProduct: URL] { - let zipToolPath = try toolsProvider("zip") - - var archives = [LambdaProduct: URL]() - for (product, artifactPath) in products { - print("-------------------------------------------------------------------------") - print("archiving \"\(product.name)\"") - print("-------------------------------------------------------------------------") - - // prep zipfile location - let workingDirectory = outputDirectory.appending(path: product.name) - let zipfilePath = workingDirectory.appending(path: "\(product.name).zip") - if FileManager.default.fileExists(atPath: workingDirectory.path()) { - try FileManager.default.removeItem(atPath: workingDirectory.path()) - } - try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) - - // rename artifact to "bootstrap" - let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") - try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) - - var arguments: [String] = [] - #if os(macOS) || os(Linux) - arguments = [ - "--recurse-paths", - "--symlinks", - zipfilePath.lastPathComponent, - relocatedArtifactPath.lastPathComponent, - ] - #else - throw Errors.unsupportedPlatform("can't or don't know how to create a zip file on this platform") - #endif - - // add resources - var artifactPathComponents = artifactPath.pathComponents - _ = artifactPathComponents.removeFirst() // Get rid of beginning "/" - _ = artifactPathComponents.removeLast() // Get rid of the name of the package - let artifactDirectory = "/\(artifactPathComponents.joined(separator: "/"))" - for fileInArtifactDirectory in try FileManager.default.contentsOfDirectory(atPath: artifactDirectory) { - guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else { - continue - } - - guard artifactURL.pathExtension == "resources" else { - continue // Not resources, so don't copy - } - let resourcesDirectoryName = artifactURL.lastPathComponent - let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) - if FileManager.default.fileExists(atPath: artifactURL.path()) { - do { - arguments.append(resourcesDirectoryName) - try FileManager.default.copyItem( - atPath: artifactURL.path(), - toPath: relocatedResourcesDirectory.path() - ) - } catch let error as CocoaError { - - // On Linux, when the build has been done with Docker, - // the source file are owned by root - // this causes a permission error **after** the files have been copied - // see https://github.com/awslabs/swift-aws-lambda-runtime/issues/449 - // see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282 - - // because this error happens after the files have been copied, we can ignore it - // this code checks if the destination file exists - // if they do, just ignore error, otherwise throw it up to the caller. - if !(error.code == CocoaError.Code.fileWriteNoPermission - && FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path())) - { - throw error - } // else just ignore it - } - } - } - - // run the zip tool - try Utils.execute( - executable: zipToolPath, - arguments: arguments, - customWorkingDirectory: workingDirectory, - logLevel: verboseLogging ? .debug : .silent - ) - - archives[product] = zipfilePath - } - return archives - } - - private enum AmazonLinuxVersion { - case al2 - case al2023 - } - - private func isAmazonLinux(_ version: AmazonLinuxVersion) -> Bool { - guard let data = FileManager.default.contents(atPath: "/etc/system-release"), - let release = String(data: data, encoding: .utf8) - else { - return false - } - switch version { - case .al2023: - return release.hasPrefix("Amazon Linux release 2023") - case .al2: - return release.hasPrefix("Amazon Linux release 2") - && !release.hasPrefix("Amazon Linux release 2023") - } - } - - private func displayDeprecationWarning() { - let separator = String(repeating: "=", count: 68) - let red = "\u{001b}[38;2;255;66;69m" - let reset = "\u{001b}[0m" - print("") - print("\(red)\(separator)") - print("WARNING: Amazon Linux 2 reaches End of Life on June 30, 2026.") - print("") - print("You must migrate to Amazon Linux 2023.") - print("Amazon Linux 2023 will become the default after June 30, 2026.") - print("") - print("To switch now, re-run with:") - print(" --base-docker-image swift:amazonlinux2023") - print("") - print("When using Amazon Linux 2023, you must also update your Lambda") - print("deployment to use the provided.al2023 runtime.") - print("") - print("For more information: https://aws.amazon.com/amazon-linux-2") - print("Available images: https://hub.docker.com/_/swift/tags?name=amazonlinux") - print("\(separator)\(reset)") - print("") - } - - private func displayHelpMessage() { - print( - """ - OVERVIEW: A SwiftPM plugin to build and package your lambda function. - - REQUIREMENTS: To use this plugin, you must have docker or container installed and started. - USAGE: swift package --allow-network-connections docker archive - [--help] [--verbose] - [--output-path ] - [--products ] - [--configuration debug | release] - [--swift-version ] - [--base-docker-image ] - [--disable-docker-image-update] - [--container-cli ] - + func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - OPTIONS: - --verbose Produce verbose output for debugging. - --output-path The path of the binary package. - (default is `.build/plugins/AWSLambdaPackager/outputs/...`) - --products The list of executable targets to build. - (default is taken from Package.swift) - --configuration The build configuration (debug or release) - (default is release) - --swift-version The swift version to use for building. - (default is latest) - This parameter cannot be used when --base-docker-image is specified. - --base-docker-image The name of the base docker image to use for the build. - (default: swift:-amazonlinux2) - Note: Amazon Linux 2023 will become the default after June 30, 2026. - Visit Docker Hub for all available swift tags: - https://hub.docker.com/_/swift/tags?name=amazonlinux - This parameter cannot be used when --swift-version is specified. - --disable-docker-image-update Do not attempt to update the docker image - --container-cli The container CLI to use (docker or container) - (default is docker) - --help Show help information. - """ + Diagnostics.warning( + "'archive' is deprecated. Please use 'swift package lambda-build' instead." ) - } -} - -@available(macOS 15.0, *) -private enum ContainerCLI: String, CustomStringConvertible { - case docker - case container - - var executableName: String { - self.rawValue - } - - var displayName: String { - self.rawValue - } - - static func parse(_ value: String?) throws -> Self { - guard let value else { - return .docker - } - - guard let tool = ContainerCLI(rawValue: value.lowercased()) else { - throw Errors.invalidArgument("invalid container CLI '\(value)'. Use 'docker' or 'container'.") - } - return tool - } - - func pullArguments(image: String) -> [String] { - switch self { - case .docker: - return ["pull", image] - case .container: - return ["image", "pull", image] - } - } - func runArguments( - baseImage: String, - workingDirectory: String, - mounts: [String], - env: [String: String]?, - command: String - ) -> [String] { - var args: [String] = ["run", "--rm"] - for mount in mounts { - args += ["-v", mount] - } - if let env { - for (key, value) in env.sorted(by: { $0.key < $1.key }) { - args += ["--env", "\(key)=\(value)"] - } - } - args += ["-w", workingDirectory, baseImage, "bash", "-cl", command] - return args - } - - var description: String { - self.rawValue - } -} - -@available(macOS 15.0, *) -private struct Configuration: CustomStringConvertible { - public let help: Bool - public let outputDirectory: URL - public let products: [Product] - public let explicitProducts: Bool - public let buildConfiguration: PackageManager.BuildConfiguration - public let verboseLogging: Bool - public let baseDockerImage: String - public let disableDockerImageUpdate: Bool - public let containerCLI: ContainerCLI + // Resolve context-dependent values (same as AWSLambdaBuilder) + let outputDirectory: URL + let products: [Product] + let buildConfiguration: PackageManager.BuildConfiguration + let packageID: String = context.package.id + let packageDisplayName = context.package.displayName + let packageDirectory = context.package.directoryURL + let dockerToolPath = try context.tool(named: "docker").url + let zipToolPath = try context.tool(named: "zip").url - public init( - context: PluginContext, - arguments: [String] - ) throws { var argumentExtractor = ArgumentExtractor(arguments) - let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let outputDirectoryArgument = argumentExtractor.extractOption(named: "output-directory") let productsArgument = argumentExtractor.extractOption(named: "products") let configurationArgument = argumentExtractor.extractOption(named: "configuration") - let swiftVersionArgument = argumentExtractor.extractOption(named: "swift-version") - let baseDockerImageArgument = argumentExtractor.extractOption(named: "base-docker-image") - let disableDockerImageUpdateArgument = argumentExtractor.extractFlag(named: "disable-docker-image-update") > 0 - let containerCliArgument = argumentExtractor.extractOption(named: "container-cli") - let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 - - // help required ? - self.help = helpArgument - // verbose logging required ? - self.verboseLogging = verboseArgument - - if let outputPath = outputPathArgument.first { + // output directory + if let outputPath = outputPathArgument.first ?? outputDirectoryArgument.first { #if os(Linux) var isDirectory: Bool = false #else @@ -493,153 +51,79 @@ private struct Configuration: CustomStringConvertible { #endif guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory) else { - throw Errors.invalidArgument("invalid output directory '\(outputPath)'") + throw PackagerErrors.invalidArgument("invalid output directory '\(outputPath)'") } - self.outputDirectory = URL(string: outputPath)! + outputDirectory = URL(string: outputPath)! } else { - self.outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") + outputDirectory = context.pluginWorkDirectoryURL.appending(path: "\(AWSLambdaPackager.self)") } - self.explicitProducts = !productsArgument.isEmpty - if self.explicitProducts { - let products = try context.package.products(named: productsArgument) - for product in products { + // products + let explicitProducts = !productsArgument.isEmpty + if explicitProducts { + let _products = try context.package.products(named: productsArgument) + for product in _products { guard product is ExecutableProduct else { - throw Errors.invalidArgument("product named '\(product.name)' is not an executable product") + throw PackagerErrors.invalidArgument("product named '\(product.name)' is not an executable product") } } - self.products = products - + products = _products } else { - self.products = context.package.products.filter { $0 is ExecutableProduct } + products = context.package.products.filter { $0 is ExecutableProduct } } + // build configuration if let buildConfigurationName = configurationArgument.first { - guard let buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else { - throw Errors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") + guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: buildConfigurationName) else { + throw PackagerErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") } - self.buildConfiguration = buildConfiguration + buildConfiguration = _buildConfiguration } else { - self.buildConfiguration = .release - } - - guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { - throw Errors.invalidArgument("--swift-version and --base-docker-image are mutually exclusive") - } - - let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image - - self.baseDockerImage = - baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" - - self.disableDockerImageUpdate = disableDockerImageUpdateArgument - self.containerCLI = try ContainerCLI.parse( - containerCliArgument.first - ) - - if self.verboseLogging { - print("-------------------------------------------------------------------------") - print("configuration") - print("-------------------------------------------------------------------------") - print(self) + buildConfiguration = .release + } + + // Build the resolved arguments for the helper + let tool = try context.tool(named: "AWSLambdaPluginHelper") + let args = + [ + "build", + "--output-path", outputDirectory.path(), + "--products", products.map { $0.name }.joined(separator: ","), + "--configuration", buildConfiguration.rawValue, + "--package-id", packageID, + "--package-display-name", packageDisplayName, + "--package-directory", packageDirectory.path(), + "--docker-tool-path", dockerToolPath.path, + "--zip-tool-path", zipToolPath.path, + ] + arguments + + // Invoke the plugin helper, passing the current environment + let process = Process() + process.executableURL = tool.url + process.arguments = args + process.environment = ProcessInfo.processInfo.environment + try process.run() + process.waitUntilExit() + + if !(process.terminationReason == .exit && process.terminationStatus == 0) { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("AWSLambdaPluginHelper invocation failed: \(problem)") } } - - var description: String { - """ - { - outputDirectory: \(self.outputDirectory) - products: \(self.products.map(\.name)) - buildConfiguration: \(self.buildConfiguration) - baseDockerImage: \(self.baseDockerImage) - disableDockerImageUpdate: \(self.disableDockerImageUpdate) - containerCLI: \(self.containerCLI) - } - """ - } - } -private enum ProcessLogLevel: Comparable { - case silent - case output(outputIndent: Int) - case debug(outputIndent: Int) - - var naturalOrder: Int { - switch self { - case .silent: - return 0 - case .output: - return 1 - case .debug: - return 2 - } - } - - static var output: Self { - .output(outputIndent: 2) - } - - static var debug: Self { - .debug(outputIndent: 2) - } - - static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool { - lhs.naturalOrder < rhs.naturalOrder - } -} - -private enum Errors: Error, CustomStringConvertible { +private enum PackagerErrors: Error, CustomStringConvertible { case invalidArgument(String) - case unsupportedPlatform(String) - case unknownProduct(String) - case productExecutableNotFound(String) - case failedWritingDockerfile - case failedParsingDockerOutput(String) - case processFailed([String], Int32) var description: String { switch self { case .invalidArgument(let description): return description - case .unsupportedPlatform(let description): - return description - case .unknownProduct(let description): - return description - case .productExecutableNotFound(let product): - return "product executable not found '\(product)'" - case .failedWritingDockerfile: - return "failed writing dockerfile" - case .failedParsingDockerOutput(let output): - return "failed parsing docker output: '\(output)'" - case .processFailed(let arguments, let code): - return "\(arguments.joined(separator: " ")) failed with code \(code)" } } } -private struct LambdaProduct: Hashable { - let underlying: Product - - init(_ underlying: Product) { - self.underlying = underlying - } - - var name: String { - self.underlying.name - } - - func hash(into hasher: inout Hasher) { - self.underlying.id.hash(into: &hasher) - } - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.underlying.id == rhs.underlying.id - } -} - extension PackageManager.BuildResult { - // find the executable produced by the build func executableArtifact(for product: Product) -> PackageManager.BuildResult.BuiltArtifact? { let executables = self.builtArtifacts.filter { $0.kind == .executable && $0.url.lastPathComponent == product.name diff --git a/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md index 9b95f0880..0fb1661d5 100644 --- a/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md +++ b/Plugins/Documentation.docc/Proposals/0001-v2-plugins.md @@ -11,7 +11,7 @@ This document describes a proposal for the v2 plugins for `swift-aws-lambda-runt Versions: * v1 (2024-12-25): Initial version -* v2 (2025-03-13): +* v2 (2025-03-13): - Include [comments from the community](https://forums.swift.org/t/lambda-plugins-for-v2/76859). - [init] Add the templates for `main.swift` - [build] Add the section **Cross-compiling options** @@ -19,6 +19,9 @@ Versions: - [deploy] Add `--input-path` parameter. - [deploy] Add details how the function name is computed. - [deploy] Add `--architecture` option and details how the default is computed. +* v3 (2026-02-17): +- [build] Add `--container-cli` option to support Apple's `container` CLI as an alternative to Docker (addresses [#644](https://github.com/awslabs/swift-aws-lambda-runtime/issues/644)). +- [build] Add the section **Container CLI options** ## Motivation @@ -160,7 +163,8 @@ The plugin interface is based on the existing `archive` plugin, with the additio ```text OVERVIEW: A SwiftPM plugin to build and package your Lambda function. -REQUIREMENTS: To use this plugin, Docker must be installed and running. +REQUIREMENTS: To use this plugin, Docker or Apple container must be installed and running + (when using docker or container cross-compilation methods). USAGE: swift package archive [--help] [--verbose] @@ -172,6 +176,7 @@ USAGE: swift package archive [--disable-docker-image-update] [--no-strip] [--cross-compile ] + [--container-cli ] [--allow-network-connections docker] OPTIONS: @@ -192,6 +197,9 @@ OPTIONS: --no-strip Do not strip the binary of debug symbols. --cross-compile Cross-compile the binary using the specified method. (default: docker) Accepted values are: docker, swift-static-sdk, custom-sdk +--container-cli Specify the container CLI to use for Docker-based builds. + (default: docker) Accepted values are: docker, container + This parameter is only used when --cross-compile is set to docker. ``` #### Cross compiling options @@ -205,6 +213,40 @@ For an ideal developer experience, we would imagine the following sequence: - if not installed or outdated, the plugin downloads a custom SDK from a safe source and installs it [questions : who should maintain such SDK binaries? Where to host them? We must have a kind of signature to ensure the SDK has not been modified. How to manage Swift version and align with the local toolchain?] - the plugin build the archive using the custom sdk +#### Container CLI options + +The plugin supports using different container CLIs to build Lambda packages. By default, it uses Docker, but it can also use Apple's `container` CLI on macOS, which provides native support for OCI (Open Container Initiative) images. + +**Supported Container CLIs:** + +- **Docker** (default): Uses the standard Docker CLI + - Pull images: `docker pull ` + - Run containers: `docker run --rm -v -w bash -cl ""` + +- **Apple container**: Uses Apple's native container CLI (available on macOS) + - Pull images: `container image pull ` + - Run containers: `container run --rm -v -w bash -cl ""` + +**Configuration:** + +The container CLI can be specified via the command-line flag: `--container-cli docker` or `--container-cli container`. If not specified, the plugin defaults to `docker`. + +**Example usage:** + +```bash +# Use Docker (default) +swift package lambda-build + +# Use Apple container +swift package lambda-build --container-cli container +``` + +**Requirements:** + +- When using `docker`, Docker Desktop or Docker Engine must be installed and running +- When using `container`, Apple's container CLI must be installed and running +- The `--container-cli` option is only applicable when using Docker-based cross-compilation (`--cross-compile docker`) + ### Deploy (lambda-deploy) The `lambda-deploy` plugin will assist developers in deploying their Lambda function to AWS. It will handle the deployment process, including creating the IAM role, the Lambda function itself, and optionally configuring a Lambda function URL. diff --git a/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift new file mode 100644 index 000000000..389b47712 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/AWSLambdaPluginHelper.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@main +@available(LambdaSwift 2.0, *) +struct AWSLambdaPluginHelper { + + private enum Command: String { + case `init` + case build + case deploy + } + + public static func main() async throws { + let args = CommandLine.arguments + let helper = AWSLambdaPluginHelper() + + guard let command = helper.command(from: args) else { + helper.displayHelpMessage() + return + } + + switch command { + case .`init`: + try await Initializer().initialize(arguments: args) + case .build: + try await Builder().build(arguments: args) + case .deploy: + try await Deployer().deploy(arguments: args) + } + } + + /// Returns nil when help should be displayed (no args, "help", "--help", or invalid command). + private func command(from arguments: [String]) -> Command? { + let args = CommandLine.arguments + + guard args.count > 1 else { + return nil + } + + let commandName = args[1] + + if commandName == "help" || commandName == "--help" || commandName == "-h" { + return nil + } + + return Command(rawValue: commandName) + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: AWS Lambda Plugin Helper + + A shared helper executable for the Swift AWS Lambda Runtime plugins. + This tool is normally invoked by SwiftPM plugins (lambda-init, lambda-build, + lambda-deploy) and not called directly. + + USAGE: AWSLambdaPluginHelper [options] + + COMMANDS: + init Scaffold a new Lambda function from a template. + build Compile and package the Lambda function for deployment. + deploy Deploy the packaged Lambda function to AWS. + + Use 'AWSLambdaPluginHelper --help' for more information about a command. + + SWIFTPM PLUGIN USAGE: + swift package lambda-init --allow-writing-to-package-directory [--with-url] + swift package --allow-network-connections docker lambda-build [options] + swift package --allow-network-connections all:443 lambda-deploy [options] + """ + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/ArgumentExtractor.swift b/Sources/AWSLambdaPluginHelper/ArgumentExtractor.swift new file mode 100644 index 000000000..15e460d47 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/ArgumentExtractor.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A rudimentary helper for extracting options and flags from a string list representing command line arguments. The idea is to extract all known options and flags, leaving just the positional arguments. This does not handle well the case in which positional arguments (or option argument values) happen to have the same name as an option or a flag. It only handles the long `--` form of options, but it does respect `--` as an indication that all remaining arguments are positional. +public struct ArgumentExtractor { + private var args: [String] + private let literals: [String] + + /// Initializes a ArgumentExtractor with a list of strings from which to extract flags and options. If the list contains `--`, any arguments that follow it are considered to be literals. + public init(_ arguments: [String]) { + // Split the array on the first `--`, if there is one. Everything after that is a literal. + let parts = arguments.split(separator: "--", maxSplits: 1, omittingEmptySubsequences: false) + self.args = Array(parts[0]) + self.literals = Array(parts.count == 2 ? parts[1] : []) + } + + /// Extracts options of the form `-- ` or `--=` from the remaining arguments, and returns the extracted values. + public mutating func extractOption(named name: String) -> [String] { + var values: [String] = [] + var idx = 0 + while idx < args.count { + var arg = args[idx] + if arg == "--\(name)" { + args.remove(at: idx) + if idx < args.count { + let val = args[idx] + values.append(val) + args.remove(at: idx) + } + } else if arg.starts(with: "--\(name)=") { + arg.removeFirst(2 + name.count + 1) + values.append(arg) + args.remove(at: idx) + } else { + idx += 1 + } + } + return values + } + + /// Extracts flags of the form `--` from the remaining arguments, and returns the count. + public mutating func extractFlag(named name: String) -> Int { + var count = 0 + var idx = 0 + while idx < args.count { + let arg = args[idx] + if arg == "--\(name)" { + args.remove(at: idx) + count += 1 + } else { + idx += 1 + } + } + return count + } + + /// Returns any unextracted flags or options (based strictly on whether remaining arguments have a "--" prefix). + public var unextractedOptionsOrFlags: [String] { + args.filter { $0.hasPrefix("--") } + } + + /// Returns all remaining arguments, including any literals after the first `--` if there is one. + public var remainingArguments: [String] { + args + literals + } +} diff --git a/Sources/AWSLambdaPluginHelper/Extensions.swift b/Sources/AWSLambdaPluginHelper/Extensions.swift new file mode 100644 index 000000000..f5c5aa829 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/Extensions.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// extension Array where Element == UInt8 { +// public var base64: String { +// Data(self).base64EncodedString() +// } +// } + +extension Data { + var bytes: [UInt8] { + [UInt8](self) + } +} + +extension String { + public var array: [UInt8] { + Array(self.utf8) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift new file mode 100644 index 000000000..c4a788357 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMClient.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import SotoCore + +/// AWS Identity and Access Management (IAM) service client. +/// +/// IAM uses the AWS Query protocol. The endpoint is global: +/// `https://iam.amazonaws.com` +/// +/// Operations use POST with `Action=&Version=2010-05-08` +/// URL-encoded form body. Responses are XML. +@available(LambdaSwift 2.0, *) +public struct IAMClient: AWSService { + /// The underlying AWS client used for making requests. + public let client: AWSClient + /// The AWS region where requests are sent. + public let region: Region + /// Service configuration for AWS IAM. + public let config: AWSServiceConfig + + /// Initialize an IAM client. + /// - Parameters: + /// - client: The AWSClient to use for requests. + /// - region: The AWS region to target (IAM is global, defaults to us-east-1). + public init(client: AWSClient, region: Region = .useast1) { + self.client = client + self.region = region + self.config = AWSServiceConfig( + region: .useast1, + partition: .aws, + serviceName: "IAM", + serviceIdentifier: "iam", + signingName: "iam", + serviceProtocol: .query, + apiVersion: "2010-05-08", + endpoint: "https://iam.amazonaws.com", + errorType: IAMErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + public init(from: IAMClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.region = from.region + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Creates a new IAM role. + /// - Parameter input: The request parameters. + /// - Returns: The newly created role. + @discardableResult + public func createRole( + _ input: IAMCreateRoleRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> IAMCreateRoleResponse { + try await self.client.execute( + operation: "CreateRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes the specified IAM role. + /// - Parameter input: The request parameters. + public func deleteRole(_ input: IAMDeleteRoleRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "DeleteRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Retrieves information about the specified IAM role. + /// - Parameter input: The request parameters. + /// - Returns: The role details. + @discardableResult + public func getRole( + _ input: IAMGetRoleRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> IAMGetRoleResponse { + try await self.client.execute( + operation: "GetRole", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Attaches the specified managed policy to the specified IAM role. + /// - Parameter input: The request parameters. + public func attachRolePolicy( + _ input: IAMAttachRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { + try await self.client.execute( + operation: "AttachRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Removes the specified managed policy from the specified IAM role. + /// - Parameter input: The request parameters. + public func detachRolePolicy( + _ input: IAMDetachRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { + try await self.client.execute( + operation: "DetachRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Adds or updates an inline policy document that is embedded in the specified IAM role. + /// - Parameter input: The request parameters. + public func putRolePolicy(_ input: IAMPutRolePolicyRequest, logger: Logger = AWSClient.loggingDisabled) async throws + { + try await self.client.execute( + operation: "PutRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes the specified inline policy from the specified IAM role. + /// - Parameter input: The request parameters. + public func deleteRolePolicy( + _ input: IAMDeleteRolePolicyRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { + try await self.client.execute( + operation: "DeleteRolePolicy", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift new file mode 100644 index 000000000..2937b979d --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMErrors.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS IAM service operations. +@available(LambdaSwift 2.0, *) +public struct IAMErrorType: AWSErrorType { + enum Code: String { + case entityAlreadyExistsException = "EntityAlreadyExists" + case entityTemporarilyUnmodifiableException = "EntityTemporarilyUnmodifiable" + case invalidInputException = "InvalidInput" + case limitExceededException = "LimitExceeded" + case malformedPolicyDocumentException = "MalformedPolicyDocument" + case noSuchEntityException = "NoSuchEntity" + case deleteConflictException = "DeleteConflict" + case serviceFailureException = "ServiceFailure" + case unmodifiableEntityException = "UnmodifiableEntity" + case policyNotAttachableException = "PolicyNotAttachable" + } + + private let error: Code + public let context: AWSErrorContext? + + /// The error code returned by IAM. + public var errorCode: String { self.error.rawValue } + + /// Initialize from error code string. + public init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + internal init(_ error: Code) { + self.error = error + self.context = nil + } + + /// Human-readable description. + public var description: String { + "\(self.error.rawValue): \(self.message ?? "")" + } + + /// The request was rejected because it attempted to create a resource that already exists. + public static var entityAlreadyExistsException: Self { .init(.entityAlreadyExistsException) } + + /// The request was rejected because the entity is temporarily unmodifiable. + public static var entityTemporarilyUnmodifiableException: Self { .init(.entityTemporarilyUnmodifiableException) } + + /// The request was rejected because an invalid or out-of-range value was supplied for an input parameter. + public static var invalidInputException: Self { .init(.invalidInputException) } + + /// The request was rejected because it attempted to create resources beyond the current AWS account limits. + public static var limitExceededException: Self { .init(.limitExceededException) } + + /// The request was rejected because the policy document was malformed. + public static var malformedPolicyDocumentException: Self { .init(.malformedPolicyDocumentException) } + + /// The request was rejected because it referenced a resource entity that does not exist. + public static var noSuchEntityException: Self { .init(.noSuchEntityException) } + + /// The request was rejected because it attempted to delete a resource that has attached subordinate entities. + public static var deleteConflictException: Self { .init(.deleteConflictException) } + + /// The request processing has failed because of an unknown error, exception, or failure. + public static var serviceFailureException: Self { .init(.serviceFailureException) } + + /// The request was rejected because only the service that depends on the service-linked role can modify or delete the role. + public static var unmodifiableEntityException: Self { .init(.unmodifiableEntityException) } + + /// The request failed because the provided policy is not attachable. + public static var policyNotAttachableException: Self { .init(.policyNotAttachableException) } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift new file mode 100644 index 000000000..3184c1c3f --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/IAM/IAMShapes.swift @@ -0,0 +1,227 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +// MARK: - Common Shapes + +/// Represents an IAM role. +@available(LambdaSwift 2.0, *) +public struct IAMRole: AWSDecodableShape, Sendable { + /// The friendly name that identifies the role. + public let roleName: String? + /// The stable and unique string identifying the role. + public let roleId: String? + /// The Amazon Resource Name (ARN) specifying the role. + public let arn: String? + /// The path to the role. + public let path: String? + /// The date and time when the role was created. + public let createDate: String? + /// The policy document that grants an entity permission to assume the role. + public let assumeRolePolicyDocument: String? + /// A description of the role. + public let description: String? + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case roleId = "RoleId" + case arn = "Arn" + case path = "Path" + case createDate = "CreateDate" + case assumeRolePolicyDocument = "AssumeRolePolicyDocument" + case description = "Description" + } +} + +// MARK: - CreateRole + +/// Request for the CreateRole operation. +@available(LambdaSwift 2.0, *) +public struct IAMCreateRoleRequest: AWSEncodableShape, Sendable { + /// The name of the role to create. + public let roleName: String + /// The trust relationship policy document that grants an entity permission to assume the role. + public let assumeRolePolicyDocument: String + /// The path to the role. + public let path: String? + /// A description of the role. + public let description: String? + + public init( + roleName: String, + assumeRolePolicyDocument: String, + path: String? = nil, + description: String? = nil + ) { + self.roleName = roleName + self.assumeRolePolicyDocument = assumeRolePolicyDocument + self.path = path + self.description = description + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case assumeRolePolicyDocument = "AssumeRolePolicyDocument" + case path = "Path" + case description = "Description" + } +} + +/// Response for the CreateRole operation. +@available(LambdaSwift 2.0, *) +public struct IAMCreateRoleResponse: AWSDecodableShape, Sendable { + /// The role that was created. + public let role: IAMRole? + + private enum CodingKeys: String, CodingKey { + case role = "Role" + } +} + +// MARK: - DeleteRole + +/// Request for the DeleteRole operation. +@available(LambdaSwift 2.0, *) +public struct IAMDeleteRoleRequest: AWSEncodableShape, Sendable { + /// The name of the role to delete. + public let roleName: String + + public init(roleName: String) { + self.roleName = roleName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + } +} + +// MARK: - GetRole + +/// Request for the GetRole operation. +@available(LambdaSwift 2.0, *) +public struct IAMGetRoleRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to get information about. + public let roleName: String + + public init(roleName: String) { + self.roleName = roleName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + } +} + +/// Response for the GetRole operation. +@available(LambdaSwift 2.0, *) +public struct IAMGetRoleResponse: AWSDecodableShape, Sendable { + /// The role details. + public let role: IAMRole? + + private enum CodingKeys: String, CodingKey { + case role = "Role" + } +} + +// MARK: - AttachRolePolicy + +/// Request for the AttachRolePolicy operation. +@available(LambdaSwift 2.0, *) +public struct IAMAttachRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to attach the policy to. + public let roleName: String + /// The Amazon Resource Name (ARN) of the IAM policy to attach. + public let policyArn: String + + public init(roleName: String, policyArn: String) { + self.roleName = roleName + self.policyArn = policyArn + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyArn = "PolicyArn" + } +} + +// MARK: - DetachRolePolicy + +/// Request for the DetachRolePolicy operation. +@available(LambdaSwift 2.0, *) +public struct IAMDetachRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the IAM role to detach the policy from. + public let roleName: String + /// The Amazon Resource Name (ARN) of the IAM policy to detach. + public let policyArn: String + + public init(roleName: String, policyArn: String) { + self.roleName = roleName + self.policyArn = policyArn + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyArn = "PolicyArn" + } +} + +// MARK: - PutRolePolicy + +/// Request for the PutRolePolicy operation. +@available(LambdaSwift 2.0, *) +public struct IAMPutRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the role to associate the policy with. + public let roleName: String + /// The name of the policy document. + public let policyName: String + /// The policy document (JSON string). + public let policyDocument: String + + public init(roleName: String, policyName: String, policyDocument: String) { + self.roleName = roleName + self.policyName = policyName + self.policyDocument = policyDocument + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyName = "PolicyName" + case policyDocument = "PolicyDocument" + } +} + +// MARK: - DeleteRolePolicy + +/// Request for the DeleteRolePolicy operation. +@available(LambdaSwift 2.0, *) +public struct IAMDeleteRolePolicyRequest: AWSEncodableShape, Sendable { + /// The name of the role the policy is associated with. + public let roleName: String + /// The name of the inline policy to delete. + public let policyName: String + + public init(roleName: String, policyName: String) { + self.roleName = roleName + self.policyName = policyName + } + + private enum CodingKeys: String, CodingKey { + case roleName = "RoleName" + case policyName = "PolicyName" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift new file mode 100644 index 000000000..bc1195af4 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaClient.swift @@ -0,0 +1,214 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import NIOCore +import SotoCore + +/// AWS Lambda service client +/// +/// Provides operations for managing AWS Lambda functions. +@available(LambdaSwift 2.0, *) +public struct LambdaClient: AWSService, Sendable { + /// The underlying AWS client used for making requests. + public let client: AWSClient + /// The AWS region where requests are sent. + public let region: Region + /// Service configuration for AWS Lambda. + public let config: AWSServiceConfig + + /// Initialize a Lambda client. + /// - Parameters: + /// - client: The AWSClient to use for requests. + /// - region: The AWS region to target. + public init(client: AWSClient, region: Region) { + self.client = client + self.region = region + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "Lambda", + serviceIdentifier: "lambda", + serviceProtocol: .restjson, + apiVersion: "2015-03-31", + errorType: LambdaErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + public init(from: LambdaClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.region = from.region + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Returns information about the function or function version. + /// - Parameter input: The request parameters. + /// - Returns: The function configuration and code location. + @discardableResult + public func getFunction( + _ input: GetFunctionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> GetFunctionResponse { + try await self.client.execute( + operation: "GetFunction", + path: "/2015-03-31/functions/{FunctionName}", + httpMethod: .GET, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Creates a Lambda function. + /// - Parameter input: The request parameters. + /// - Returns: The function configuration. + @discardableResult + public func createFunction( + _ input: CreateFunctionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateFunctionResponse { + try await self.client.execute( + operation: "CreateFunction", + path: "/2015-03-31/functions", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Updates a Lambda function's code. + /// - Parameter input: The request parameters. + /// - Returns: The updated function configuration. + @discardableResult + public func updateFunctionCode( + _ input: UpdateFunctionCodeRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> UpdateFunctionCodeResponse { + try await self.client.execute( + operation: "UpdateFunctionCode", + path: "/2015-03-31/functions/{FunctionName}/code", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes a Lambda function. + /// - Parameter input: The request parameters. + public func deleteFunction(_ input: DeleteFunctionRequest, logger: Logger = AWSClient.loggingDisabled) async throws + { + try await self.client.execute( + operation: "DeleteFunction", + path: "/2015-03-31/functions/{FunctionName}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Creates a function URL with the specified configuration parameters. + /// - Parameter input: The request parameters. + /// - Returns: The function URL configuration. + @discardableResult + public func createFunctionUrlConfig( + _ input: CreateFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateFunctionUrlConfigResponse { + try await self.client.execute( + operation: "CreateFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Deletes a Lambda function URL. + /// - Parameter input: The request parameters. + public func deleteFunctionUrlConfig( + _ input: DeleteFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { + try await self.client.execute( + operation: "DeleteFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Returns the Function URL configuration for a Lambda function. + /// - Parameter input: The request parameters. + /// - Returns: The Function URL configuration including the URL endpoint. + @discardableResult + public func getFunctionUrlConfig( + _ input: GetFunctionUrlConfigRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> GetFunctionUrlConfigResponse { + try await self.client.execute( + operation: "GetFunctionUrlConfig", + path: "/2021-10-31/functions/{FunctionName}/url", + httpMethod: .GET, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Grants an AWS service, AWS account, or AWS organization permission to use a function. + /// - Parameter input: The request parameters. + /// - Returns: The permission statement added to the function policy. + @discardableResult + public func addPermission( + _ input: AddPermissionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> AddPermissionResponse { + try await self.client.execute( + operation: "AddPermission", + path: "/2015-03-31/functions/{FunctionName}/policy", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Revokes function-use permission from an AWS service or another AWS account. + /// - Parameter input: The request parameters. + public func removePermission( + _ input: RemovePermissionRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws { + try await self.client.execute( + operation: "RemovePermission", + path: "/2015-03-31/functions/{FunctionName}/policy/{StatementId}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift new file mode 100644 index 000000000..6393b98a2 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaErrors.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS Lambda service +@available(LambdaSwift 2.0, *) +public struct LambdaErrorType: AWSErrorType { + enum Code: String { + case invalidParameterValueException = "InvalidParameterValueException" + case resourceConflictException = "ResourceConflictException" + case resourceNotFoundException = "ResourceNotFoundException" + case serviceException = "ServiceException" + case tooManyRequestsException = "TooManyRequestsException" + case codeStorageExceededException = "CodeStorageExceededException" + case policyLengthExceededException = "PolicyLengthExceededException" + case preconditionFailedException = "PreconditionFailedException" + case resourceNotReadyException = "ResourceNotReadyException" + } + + private let error: Code + public let context: AWSErrorContext? + + /// Initialize from error code string + public init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + internal init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The error code returned by the service + public var errorCode: String { self.error.rawValue } + + /// Human-readable description + public var description: String { + "\(self.error.rawValue): \(self.message ?? "")" + } + + /// One of the parameters in the request is not valid. + public static var invalidParameterValueException: Self { .init(.invalidParameterValueException) } + /// The resource already exists, or another operation is in progress. + public static var resourceConflictException: Self { .init(.resourceConflictException) } + /// The resource specified in the request does not exist. + public static var resourceNotFoundException: Self { .init(.resourceNotFoundException) } + /// The AWS Lambda service encountered an internal error. + public static var serviceException: Self { .init(.serviceException) } + /// The request throughput limit was exceeded. + public static var tooManyRequestsException: Self { .init(.tooManyRequestsException) } + /// Your AWS Lambda function code size exceeded the maximum allowed size. + public static var codeStorageExceededException: Self { .init(.codeStorageExceededException) } + /// The permissions policy for the resource is too large. + public static var policyLengthExceededException: Self { .init(.policyLengthExceededException) } + /// The RevisionId provided does not match the latest RevisionId for the Lambda function or alias. + public static var preconditionFailedException: Self { .init(.preconditionFailedException) } + /// The function is not in a ready state. + public static var resourceNotReadyException: Self { .init(.resourceNotReadyException) } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift new file mode 100644 index 000000000..a1ed0f66d --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/Lambda/LambdaShapes.swift @@ -0,0 +1,597 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +@_spi(SotoInternal) import SotoCore + +// MARK: - Enums + +/// Lambda function architecture +@available(LambdaSwift 2.0, *) +public enum LambdaArchitecture: String, Codable, Sendable { + case x86_64 = "x86_64" + case arm64 = "arm64" +} + +/// Lambda function runtime +@available(LambdaSwift 2.0, *) +public enum LambdaRuntime: String, Codable, Sendable { + case providedAl2023 = "provided.al2023" + case providedAl2 = "provided.al2" +} + +/// Lambda function packaging type +@available(LambdaSwift 2.0, *) +public enum LambdaPackageType: String, Codable, Sendable { + case zip = "Zip" + case image = "Image" +} + +/// Function URL auth type +@available(LambdaSwift 2.0, *) +public enum FunctionUrlAuthType: String, Codable, Sendable { + case awsIam = "AWS_IAM" + case none = "NONE" +} + +// MARK: - GetFunction + +/// Request for GetFunction operation +@available(LambdaSwift 2.0, *) +public struct GetFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for GetFunction operation +@available(LambdaSwift 2.0, *) +public struct GetFunctionResponse: AWSDecodableShape, Sendable { + /// The configuration of the function. + public let configuration: FunctionConfiguration? + /// The deployment package of the function. + public let code: FunctionCodeLocation? + + private enum CodingKeys: String, CodingKey { + case configuration = "Configuration" + case code = "Code" + } +} + +/// Function configuration details +@available(LambdaSwift 2.0, *) +public struct FunctionConfiguration: Codable, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's description. + public let description: String? + /// The amount of time in seconds that Lambda allows a function to run before stopping it. + public let timeout: Int? + /// The amount of memory available to the function at runtime. + public let memorySize: Int? + /// The latest updated date. + public let lastModified: String? + /// The SHA256 hash of the deployment package. + public let codeSha256: String? + /// The version of the Lambda function. + public let version: String? + /// The function's state. + public let state: String? + /// The reason for the function's current state. + public let stateReason: String? + /// The instruction set architecture. + public let architectures: [String]? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case description = "Description" + case timeout = "Timeout" + case memorySize = "MemorySize" + case lastModified = "LastModified" + case codeSha256 = "CodeSha256" + case version = "Version" + case state = "State" + case stateReason = "StateReason" + case architectures = "Architectures" + } +} + +/// Function code location details +@available(LambdaSwift 2.0, *) +public struct FunctionCodeLocation: Codable, Sendable { + /// The service that hosts the deployment package. + public let repositoryType: String? + /// A presigned URL to download the deployment package. + public let location: String? + + private enum CodingKeys: String, CodingKey { + case repositoryType = "RepositoryType" + case location = "Location" + } +} + +// MARK: - CreateFunction + +/// Function code for CreateFunction +@available(LambdaSwift 2.0, *) +public struct FunctionCode: Codable, Sendable { + /// The base64-encoded contents of the deployment package. + public let zipFile: String? + /// An Amazon S3 bucket in the same AWS Region as your function. + public let s3Bucket: String? + /// The Amazon S3 key of the deployment package. + public let s3Key: String? + + public init(zipFile: String? = nil, s3Bucket: String? = nil, s3Key: String? = nil) { + self.zipFile = zipFile + self.s3Bucket = s3Bucket + self.s3Key = s3Key + } + + private enum CodingKeys: String, CodingKey { + case zipFile = "ZipFile" + case s3Bucket = "S3Bucket" + case s3Key = "S3Key" + } +} + +/// Request for CreateFunction operation +@available(LambdaSwift 2.0, *) +public struct CreateFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The Amazon Resource Name (ARN) of the function's execution role. + public let role: String + /// The identifier of the function's runtime. + public let runtime: LambdaRuntime + /// The name of the method within your code that Lambda calls to run your function. + public let handler: String + /// The code for the function. + public let code: FunctionCode + /// A description of the function. + public let description: String? + /// The amount of time (in seconds) that Lambda allows a function to run before stopping it. + public let timeout: Int? + /// The amount of memory available to the function at runtime. + public let memorySize: Int? + /// The instruction set architecture. + public let architectures: [LambdaArchitecture]? + /// The type of deployment package. + public let packageType: LambdaPackageType? + + public init( + functionName: String, + role: String, + runtime: LambdaRuntime, + handler: String, + code: FunctionCode, + description: String? = nil, + timeout: Int? = nil, + memorySize: Int? = nil, + architectures: [LambdaArchitecture]? = nil, + packageType: LambdaPackageType? = nil + ) { + self.functionName = functionName + self.role = role + self.runtime = runtime + self.handler = handler + self.code = code + self.description = description + self.timeout = timeout + self.memorySize = memorySize + self.architectures = architectures + self.packageType = packageType + } + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case role = "Role" + case runtime = "Runtime" + case handler = "Handler" + case code = "Code" + case description = "Description" + case timeout = "Timeout" + case memorySize = "MemorySize" + case architectures = "Architectures" + case packageType = "PackageType" + } +} + +/// Response for CreateFunction operation +@available(LambdaSwift 2.0, *) +public struct CreateFunctionResponse: AWSDecodableShape, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's description. + public let description: String? + /// The function's state. + public let state: String? + /// The instruction set architecture. + public let architectures: [String]? + /// The version of the Lambda function. + public let version: String? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case description = "Description" + case state = "State" + case architectures = "Architectures" + case version = "Version" + } +} + +// MARK: - UpdateFunctionCode + +/// Request for UpdateFunctionCode operation +@available(LambdaSwift 2.0, *) +public struct UpdateFunctionCodeRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The base64-encoded contents of the deployment package. + public let zipFile: String? + /// An Amazon S3 bucket in the same AWS Region as your function. + public let s3Bucket: String? + /// The Amazon S3 key of the deployment package. + public let s3Key: String? + /// The instruction set architecture. + public let architectures: [LambdaArchitecture]? + + public init( + functionName: String, + zipFile: String? = nil, + s3Bucket: String? = nil, + s3Key: String? = nil, + architectures: [LambdaArchitecture]? = nil + ) { + self.functionName = functionName + self.zipFile = zipFile + self.s3Bucket = s3Bucket + self.s3Key = s3Key + self.architectures = architectures + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.zipFile, forKey: .zipFile) + try container.encodeIfPresent(self.s3Bucket, forKey: .s3Bucket) + try container.encodeIfPresent(self.s3Key, forKey: .s3Key) + try container.encodeIfPresent(self.architectures, forKey: .architectures) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: String, CodingKey { + case zipFile = "ZipFile" + case s3Bucket = "S3Bucket" + case s3Key = "S3Key" + case architectures = "Architectures" + } +} + +/// Response for UpdateFunctionCode operation +@available(LambdaSwift 2.0, *) +public struct UpdateFunctionCodeResponse: AWSDecodableShape, Sendable { + /// The name of the function. + public let functionName: String? + /// The function's Amazon Resource Name (ARN). + public let functionArn: String? + /// The runtime environment. + public let runtime: String? + /// The function's execution role. + public let role: String? + /// The function's handler. + public let handler: String? + /// The size of the function's deployment package, in bytes. + public let codeSize: Int64? + /// The function's state. + public let state: String? + /// The instruction set architecture. + public let architectures: [String]? + /// The version of the Lambda function. + public let version: String? + + private enum CodingKeys: String, CodingKey { + case functionName = "FunctionName" + case functionArn = "FunctionArn" + case runtime = "Runtime" + case role = "Role" + case handler = "Handler" + case codeSize = "CodeSize" + case state = "State" + case architectures = "Architectures" + case version = "Version" + } +} + +// MARK: - DeleteFunction + +/// Request for DeleteFunction operation +@available(LambdaSwift 2.0, *) +public struct DeleteFunctionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - CreateFunctionUrlConfig + +/// Request for CreateFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) +public struct CreateFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// The type of authentication that your function URL uses. + public let authType: FunctionUrlAuthType + + public init(functionName: String, authType: FunctionUrlAuthType) { + self.functionName = functionName + self.authType = authType + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.authType, forKey: .authType) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: String, CodingKey { + case authType = "AuthType" + } +} + +/// Response for CreateFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) +public struct CreateFunctionUrlConfigResponse: AWSDecodableShape, Sendable { + /// The HTTP URL endpoint for the function. + public let functionUrl: String? + /// The Amazon Resource Name (ARN) of the function. + public let functionArn: String? + /// The type of authentication. + public let authType: String? + /// When the function URL was created. + public let creationTime: String? + + private enum CodingKeys: String, CodingKey { + case functionUrl = "FunctionUrl" + case functionArn = "FunctionArn" + case authType = "AuthType" + case creationTime = "CreationTime" + } +} + +// MARK: - DeleteFunctionUrlConfig + +/// Request for DeleteFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) +public struct DeleteFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - GetFunctionUrlConfig + +/// Request for GetFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) +public struct GetFunctionUrlConfigRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + + public init(functionName: String) { + self.functionName = functionName + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for GetFunctionUrlConfig operation +@available(LambdaSwift 2.0, *) +public struct GetFunctionUrlConfigResponse: AWSDecodableShape, Sendable { + /// The HTTP URL endpoint for the function. + public let functionUrl: String? + /// The type of authentication. + public let authType: String? + + private enum CodingKeys: String, CodingKey { + case functionUrl = "FunctionUrl" + case authType = "AuthType" + } +} + +// MARK: - AddPermission + +/// Request for AddPermission operation +@available(LambdaSwift 2.0, *) +public struct AddPermissionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// A statement identifier that differentiates the statement from others in the same policy. + public let statementId: String + /// The action that the principal can use on the function. + public let action: String + /// The AWS service or AWS account that invokes the function. + public let principal: String + /// The identifier for your organization in AWS Organizations. + public let principalOrgID: String? + /// For AWS service principals, the ARN of the AWS resource that invokes the function. + public let sourceArn: String? + /// For Amazon S3 only, the AWS account ID of the bucket owner. + public let sourceAccount: String? + /// Specify a version or alias to add permissions to a published version of the function. + public let qualifier: String? + /// Only update the policy if the revision ID matches the ID that's specified. + public let revisionId: String? + /// The type of authentication that your function URL uses. + public let functionUrlAuthType: FunctionUrlAuthType? + + public init( + functionName: String, + statementId: String, + action: String, + principal: String, + principalOrgID: String? = nil, + sourceArn: String? = nil, + sourceAccount: String? = nil, + qualifier: String? = nil, + revisionId: String? = nil, + functionUrlAuthType: FunctionUrlAuthType? = nil + ) { + self.functionName = functionName + self.statementId = statementId + self.action = action + self.principal = principal + self.principalOrgID = principalOrgID + self.sourceArn = sourceArn + self.sourceAccount = sourceAccount + self.qualifier = qualifier + self.revisionId = revisionId + self.functionUrlAuthType = functionUrlAuthType + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.statementId, forKey: .statementId) + try container.encode(self.action, forKey: .action) + try container.encode(self.principal, forKey: .principal) + try container.encodeIfPresent(self.principalOrgID, forKey: .principalOrgID) + try container.encodeIfPresent(self.sourceArn, forKey: .sourceArn) + try container.encodeIfPresent(self.sourceAccount, forKey: .sourceAccount) + try container.encodeIfPresent(self.revisionId, forKey: .revisionId) + try container.encodeIfPresent(self.functionUrlAuthType, forKey: .functionUrlAuthType) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + if let qualifier = self.qualifier { + requestContainer.encodeQuery(qualifier, key: "Qualifier") + } + } + + private enum CodingKeys: String, CodingKey { + case statementId = "StatementId" + case action = "Action" + case principal = "Principal" + case principalOrgID = "PrincipalOrgID" + case sourceArn = "SourceArn" + case sourceAccount = "SourceAccount" + case revisionId = "RevisionId" + case functionUrlAuthType = "FunctionUrlAuthType" + } +} + +/// Response for AddPermission operation +@available(LambdaSwift 2.0, *) +public struct AddPermissionResponse: AWSDecodableShape, Sendable { + /// The permission statement that's added to the function policy. + public let statement: String? + + private enum CodingKeys: String, CodingKey { + case statement = "Statement" + } +} + +// MARK: - RemovePermission + +/// Request for RemovePermission operation +@available(LambdaSwift 2.0, *) +public struct RemovePermissionRequest: AWSEncodableShape, Sendable { + /// The name of the Lambda function. + public let functionName: String + /// Statement ID of the permission to remove. + public let statementId: String + + public init(functionName: String, statementId: String) { + self.functionName = functionName + self.statementId = statementId + } + + public func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.functionName, key: "FunctionName") + requestContainer.encodePath(self.statementId, key: "StatementId") + } + + private enum CodingKeys: CodingKey {} +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift new file mode 100644 index 000000000..f22b4c250 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Client.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import Logging +import SotoCore + +/// Client for Amazon Simple Storage Service (S3). +/// +/// S3 uses the REST-XML protocol with path-style addressing. +/// Endpoint: `https://s3..amazonaws.com` +@available(LambdaSwift 2.0, *) +struct S3Client: AWSService { + let client: AWSClient + let config: AWSServiceConfig + + /// Initialize the S3 client. + /// - Parameters: + /// - client: The AWSClient used for request signing and transport. + /// - region: The AWS region to send requests to. + init(client: AWSClient, region: Region) { + self.client = client + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "S3", + serviceIdentifier: "s3", + signingName: "s3", + serviceProtocol: .restxml, + apiVersion: "2006-03-01", + errorType: S3ErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + init(from: S3Client, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Creates a new S3 bucket. + /// + /// PUT /{Bucket} + /// + /// When the region is not `us-east-1`, a `CreateBucketConfiguration` XML body + /// with the `LocationConstraint` element is included. + /// + /// - Parameter input: The request parameters. + /// - Returns: The response containing the bucket location. + @discardableResult + func createBucket( + _ input: CreateBucketRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> CreateBucketResponse { + try await self.client.execute( + operation: "CreateBucket", + path: "/{Bucket}", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Determines if a bucket exists and you have permission to access it. + /// + /// HEAD /{Bucket} + /// + /// Returns an empty response on success. Throws `NotFound` if the bucket + /// does not exist or you do not have permission to access it. + /// + /// - Parameter input: The request parameters. + func headBucket(_ input: HeadBucketRequest, logger: Logger = AWSClient.loggingDisabled) async throws { + try await self.client.execute( + operation: "HeadBucket", + path: "/{Bucket}", + httpMethod: .HEAD, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Adds an object to a bucket. + /// + /// PUT /{Bucket}/{Key+} + /// + /// - Parameter input: The request parameters including the object body data. + /// - Returns: The response containing ETag and version information. + @discardableResult + func putObject( + _ input: PutObjectRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> PutObjectResponse { + try await self.client.execute( + operation: "PutObject", + path: "/{Bucket}/{Key+}", + httpMethod: .PUT, + serviceConfig: self.config, + input: input, + logger: logger + ) + } + + /// Removes an object from a bucket. + /// + /// DELETE /{Bucket}/{Key+} + /// + /// - Parameter input: The request parameters. + /// - Returns: The response containing delete marker and version information. + @discardableResult + func deleteObject( + _ input: DeleteObjectRequest, + logger: Logger = AWSClient.loggingDisabled + ) async throws -> DeleteObjectResponse { + try await self.client.execute( + operation: "DeleteObject", + path: "/{Bucket}/{Key+}", + httpMethod: .DELETE, + serviceConfig: self.config, + input: input, + logger: logger + ) + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift new file mode 100644 index 000000000..833727709 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Errors.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +import SotoCore + +/// Error type for AWS S3 service +@available(LambdaSwift 2.0, *) +struct S3ErrorType: AWSErrorType { + enum Code: String { + case bucketAlreadyExists = "BucketAlreadyExists" + case bucketAlreadyOwnedByYou = "BucketAlreadyOwnedByYou" + case noSuchBucket = "NoSuchBucket" + case noSuchKey = "NoSuchKey" + case notFound = "NotFound" + case invalidBucketName = "InvalidBucketName" + case invalidObjectState = "InvalidObjectState" + } + + private let error: Code + let context: AWSErrorContext? + + /// The error code returned by S3. + var errorCode: String { self.error.rawValue } + + init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + private init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The requested bucket name already exists. Select a different name and try again. + static var bucketAlreadyExists: S3ErrorType { .init(.bucketAlreadyExists) } + + /// The bucket you tried to create already exists, and you own it. + static var bucketAlreadyOwnedByYou: S3ErrorType { .init(.bucketAlreadyOwnedByYou) } + + /// The specified bucket does not exist. + static var noSuchBucket: S3ErrorType { .init(.noSuchBucket) } + + /// The specified key does not exist. + static var noSuchKey: S3ErrorType { .init(.noSuchKey) } + + /// The resource was not found (used by HeadBucket/HeadObject). + static var notFound: S3ErrorType { .init(.notFound) } + + /// The specified bucket name is not valid. + static var invalidBucketName: S3ErrorType { .init(.invalidBucketName) } + + /// The operation is not valid for the object's storage class. + static var invalidObjectState: S3ErrorType { .init(.invalidObjectState) } + + var description: String { + "\(self.errorCode): \(self.context?.message ?? "")" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift new file mode 100644 index 000000000..0d06b7417 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/S3/S3Shapes.swift @@ -0,0 +1,191 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT + +@_spi(SotoInternal) import SotoCore + +// MARK: - CreateBucket + +/// Configuration for the bucket location constraint. +@available(LambdaSwift 2.0, *) +struct CreateBucketConfiguration: AWSEncodableShape, Sendable { + static let _xmlRootNodeName: String? = "CreateBucketConfiguration" + static let _xmlNamespace: String? = "http://s3.amazonaws.com/doc/2006-03-01/" + + /// The Region where the bucket will be created. If you don't specify a Region, + /// the bucket is created in US East (N. Virginia) Region (us-east-1). + let locationConstraint: String? + + init(locationConstraint: String? = nil) { + self.locationConstraint = locationConstraint + } + + private enum CodingKeys: String, CodingKey { + case locationConstraint = "LocationConstraint" + } +} + +/// Request for CreateBucket operation. +@available(LambdaSwift 2.0, *) +struct CreateBucketRequest: AWSEncodableShape, Sendable { + /// The name of the bucket to create. + let bucket: String + /// The configuration information for the bucket, including the location constraint. + let createBucketConfiguration: CreateBucketConfiguration? + + init(bucket: String, createBucketConfiguration: CreateBucketConfiguration? = nil) { + self.bucket = bucket + self.createBucketConfiguration = createBucketConfiguration + } + + func encode(to encoder: Encoder) throws { + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + if let config = self.createBucketConfiguration { + try config.encode(to: encoder) + } else { + _ = encoder.container(keyedBy: CodingKeys.self) + } + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for CreateBucket operation. +@available(LambdaSwift 2.0, *) +struct CreateBucketResponse: AWSDecodableShape, Sendable { + /// The URI that identifies the bucket. + let location: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.location = try? responseContainer.decodeHeaderIfPresent(String.self, key: "Location") + } +} + +// MARK: - HeadBucket + +/// Request for HeadBucket operation. +@available(LambdaSwift 2.0, *) +struct HeadBucketRequest: AWSEncodableShape, Sendable { + /// The bucket name. + let bucket: String + + init(bucket: String) { + self.bucket = bucket + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + } + + private enum CodingKeys: CodingKey {} +} + +// MARK: - PutObject + +/// Request for PutObject operation. +@available(LambdaSwift 2.0, *) +struct PutObjectRequest: AWSEncodableShape, Sendable { + static let _options: AWSShapeOptions = [.allowStreaming] + + /// The bucket name. + let bucket: String + /// Object key for which the PUT action was initiated. + let key: String + /// Object data. + let body: AWSHTTPBody + + init(bucket: String, key: String, body: AWSHTTPBody) { + self.bucket = bucket + self.key = key + self.body = body + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + requestContainer.encodePath(self.key, key: "Key") + try self.body.encode(to: encoder) + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for PutObject operation. +@available(LambdaSwift 2.0, *) +struct PutObjectResponse: AWSDecodableShape, Sendable { + /// Entity tag for the uploaded object. + let eTag: String? + /// Version of the object. + let versionId: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.eTag = try? responseContainer.decodeHeaderIfPresent(String.self, key: "ETag") + self.versionId = try? responseContainer.decodeHeaderIfPresent(String.self, key: "x-amz-version-id") + } +} + +// MARK: - DeleteObject + +/// Request for DeleteObject operation. +@available(LambdaSwift 2.0, *) +struct DeleteObjectRequest: AWSEncodableShape, Sendable { + /// The bucket name. + let bucket: String + /// Key name of the object to delete. + let key: String + /// The version ID used to reference a specific version of the object. + let versionId: String? + + init(bucket: String, key: String, versionId: String? = nil) { + self.bucket = bucket + self.key = key + self.versionId = versionId + } + + func encode(to encoder: Encoder) throws { + _ = encoder.container(keyedBy: CodingKeys.self) + let requestContainer = encoder.userInfo[.awsRequest]! as! RequestEncodingContainer + requestContainer.encodePath(self.bucket, key: "Bucket") + requestContainer.encodePath(self.key, key: "Key") + if let versionId = self.versionId { + requestContainer.encodeQuery(versionId, key: "versionId") + } + } + + private enum CodingKeys: CodingKey {} +} + +/// Response for DeleteObject operation. +@available(LambdaSwift 2.0, *) +struct DeleteObjectResponse: AWSDecodableShape, Sendable { + /// Indicates whether the specified object version that was permanently deleted + /// was a delete marker. + let deleteMarker: Bool? + /// Returns the version ID of the delete marker. + let versionId: String? + + init(from decoder: Decoder) throws { + let responseContainer = decoder.userInfo[.awsResponse]! as! ResponseDecodingContainer + self.deleteMarker = try? responseContainer.decodeHeaderIfPresent(Bool.self, key: "x-amz-delete-marker") + self.versionId = try? responseContainer.decodeHeaderIfPresent(String.self, key: "x-amz-version-id") + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift new file mode 100644 index 000000000..86455cbaf --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSClient.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +/// Client for AWS Security Token Service (STS). +/// +/// STS uses the AWS Query protocol. The endpoint is regional: +/// `https://sts..amazonaws.com` +/// +/// Operations use POST with `Action=&Version=2011-06-15` +/// URL-encoded form body. Responses are XML. +@available(LambdaSwift 2.0, *) +struct STSClient: AWSService { + let client: AWSClient + let config: AWSServiceConfig + + /// Initialize the STS client. + /// - Parameters: + /// - client: The AWSClient used for request signing and transport. + /// - region: The AWS region to send requests to. + init(client: AWSClient, region: Region) { + self.client = client + self.config = AWSServiceConfig( + region: region, + partition: region.partition, + serviceName: "STS", + serviceIdentifier: "sts", + signingName: "sts", + serviceProtocol: .query, + apiVersion: "2011-06-15", + errorType: STSErrorType.self + ) + } + + /// Create a new version of the service with a patch applied. + init(from: STSClient, patch: AWSServiceConfig.Patch) { + self.client = from.client + self.config = from.config.with(patch: patch) + } + + // MARK: - Operations + + /// Returns details about the IAM user or role whose credentials are used + /// to call the operation. + /// + /// No permissions are required to perform this operation. If an administrator + /// attaches a policy to your identity that explicitly denies access to the + /// `sts:GetCallerIdentity` action, you can still perform this operation. + /// + /// - Returns: A ``STSGetCallerIdentityResponse`` containing the account, ARN, + /// and user ID of the calling entity. + func getCallerIdentity() async throws -> STSGetCallerIdentityResponse { + let input = STSGetCallerIdentityRequest() + return try await self.client.execute( + operation: "GetCallerIdentity", + path: "/", + httpMethod: .POST, + serviceConfig: self.config, + input: input, + logger: Self.logger + ) + } + + // MARK: - Private + + private static let logger = Logger(label: "STSClient") +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift new file mode 100644 index 000000000..7d2bffd22 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSErrors.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +/// Error type for STS service operations. +@available(LambdaSwift 2.0, *) +struct STSErrorType: AWSErrorType { + enum Code: String { + case expiredTokenException = "ExpiredTokenException" + case malformedPolicyDocumentException = "MalformedPolicyDocument" + case packedPolicyTooLargeException = "PackedPolicyTooLarge" + case regionDisabledException = "RegionDisabledException" + case idpRejectedClaimException = "IDPRejectedClaim" + case invalidIdentityTokenException = "InvalidIdentityToken" + case idpCommunicationErrorException = "IDPCommunicationError" + } + + private let error: Code + let context: AWSErrorContext? + + /// The error code returned by STS. + var errorCode: String { self.error.rawValue } + + init?(errorCode: String, context: AWSErrorContext) { + guard let error = Code(rawValue: errorCode) else { return nil } + self.error = error + self.context = context + } + + private init(_ error: Code) { + self.error = error + self.context = nil + } + + /// The security token included in the request is expired. + static var expiredTokenException: STSErrorType { .init(.expiredTokenException) } + + /// The request was rejected because the policy document was malformed. + static var malformedPolicyDocumentException: STSErrorType { .init(.malformedPolicyDocumentException) } + + /// The request was rejected because the total packed size of the session policies is too large. + static var packedPolicyTooLargeException: STSErrorType { .init(.packedPolicyTooLargeException) } + + /// STS is not activated in the requested region. + static var regionDisabledException: STSErrorType { .init(.regionDisabledException) } + + /// The identity provider rejected the request because the identity claim is invalid. + static var idpRejectedClaimException: STSErrorType { .init(.idpRejectedClaimException) } + + /// The web identity token is invalid or expired. + static var invalidIdentityTokenException: STSErrorType { .init(.invalidIdentityTokenException) } + + /// The request could not be fulfilled because the identity provider (IDP) is not accessible. + static var idpCommunicationErrorException: STSErrorType { .init(.idpCommunicationErrorException) } + + var description: String { + "\(self.errorCode): \(self.context?.message ?? "")" + } +} diff --git a/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift new file mode 100644 index 000000000..01a5a2cee --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/GeneratedClients/STS/STSShapes.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// Generated by scripts/generate-aws-clients.sh — DO NOT EDIT +// +//===----------------------------------------------------------------------===// + +import SotoCore + +// MARK: - Input Shapes + +/// Request shape for the GetCallerIdentity operation. +/// This operation requires no input parameters. +@available(LambdaSwift 2.0, *) +struct STSGetCallerIdentityRequest: AWSEncodableShape { + init() {} +} + +// MARK: - Output Shapes + +/// Response shape for the GetCallerIdentity operation. +@available(LambdaSwift 2.0, *) +struct STSGetCallerIdentityResponse: AWSDecodableShape { + /// The unique identifier of the calling entity. + /// The exact value depends on the type of entity that is making the call. + let userId: String? + + /// The AWS account ID number of the account that owns or contains the calling entity. + let account: String? + + /// The AWS ARN associated with the calling entity. + let arn: String? + + private enum CodingKeys: String, CodingKey { + case userId = "UserId" + case account = "Account" + case arn = "Arn" + } +} diff --git a/Plugins/AWSLambdaPackager/PluginUtils.swift b/Sources/AWSLambdaPluginHelper/Process.swift similarity index 99% rename from Plugins/AWSLambdaPackager/PluginUtils.swift rename to Sources/AWSLambdaPluginHelper/Process.swift index 8cb7e22ed..c996f9960 100644 --- a/Plugins/AWSLambdaPackager/PluginUtils.swift +++ b/Sources/AWSLambdaPluginHelper/Process.swift @@ -15,10 +15,9 @@ import Dispatch import Foundation -import PackagePlugin import Synchronization -@available(macOS 15.0, *) +@available(LambdaSwift 2.0, *) struct Utils { @discardableResult static func execute( diff --git a/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift new file mode 100644 index 000000000..c0e44ecd8 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-build/Builder.swift @@ -0,0 +1,690 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@available(LambdaSwift 2.0, *) +struct Builder { + func build(arguments: [String]) async throws { + let configuration = try BuilderConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + // display informational warning only when user explicitly selects an AL2 image + if configuration.explicitAL2Image { + self.displayAL2Warning() + } + + let builtProducts: [String: URL] + + if self.isAmazonLinux(.al2) || self.isAmazonLinux(.al2023) { + // native build on Amazon Linux + builtProducts = try self.buildNative( + packageIdentity: configuration.packageID, + products: configuration.products, + buildConfiguration: configuration.buildConfiguration, + noStrip: configuration.noStrip, + verboseLogging: configuration.verboseLogging + ) + } else { + // build with docker/container + builtProducts = try self.buildInDocker( + packageIdentity: configuration.packageID, + packageDirectory: configuration.packageDirectory, + products: configuration.products, + containerCLIPath: configuration.dockerToolPath, + containerCLI: configuration.crossCompileMethod, + outputDirectory: configuration.outputDirectory, + baseImage: configuration.baseDockerImage, + disableDockerImageUpdate: configuration.disableDockerImageUpdate, + buildConfiguration: configuration.buildConfiguration, + noStrip: configuration.noStrip, + verboseLogging: configuration.verboseLogging + ) + } + + // create the archive + let archives = try self.package( + packageName: configuration.packageDisplayName, + products: builtProducts, + zipToolPath: configuration.zipToolPath, + outputDirectory: configuration.outputDirectory, + verboseLogging: configuration.verboseLogging + ) + + print( + "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" + ) + for (product, archivePath) in archives { + print(" * \(product) at \(archivePath.path())") + } + } + + private func buildNative( + packageIdentity: String, + products: [String], + buildConfiguration: BuildConfiguration, + noStrip: Bool, + verboseLogging: Bool + ) throws -> [String: URL] { + print("-------------------------------------------------------------------------") + print("building \"\(packageIdentity)\"") + print("-------------------------------------------------------------------------") + + var results = [String: URL]() + for product in products { + print("building \"\(product)\"") + var buildArguments = [ + "build", "-c", buildConfiguration.rawValue, + "--product", product, + "--static-swift-stdlib", + ] + if !noStrip { + buildArguments += ["-Xlinker", "-s"] + } + try Utils.execute( + executable: URL(fileURLWithPath: "/usr/bin/swift"), + arguments: buildArguments, + logLevel: verboseLogging ? .debug : .output + ) + + // get the build output path + let showBinPathArguments = ["build", "-c", buildConfiguration.rawValue, "--show-bin-path"] + let binPath = try Utils.execute( + executable: URL(fileURLWithPath: "/usr/bin/swift"), + arguments: showBinPathArguments, + logLevel: .silent + ).trimmingCharacters(in: .whitespacesAndNewlines) + + let productPath = URL(fileURLWithPath: binPath).appending(path: product) + guard FileManager.default.fileExists(atPath: productPath.path()) else { + print("expected '\(product)' binary at \"\(productPath.path())\"") + throw BuilderErrors.productExecutableNotFound(product) + } + results[product] = productPath + } + return results + } + + private func buildInDocker( + packageIdentity: String, + packageDirectory: URL, + products: [String], + containerCLIPath: URL, + containerCLI: CrossCompileMethod, + outputDirectory: URL, + baseImage: String, + disableDockerImageUpdate: Bool, + buildConfiguration: BuildConfiguration, + noStrip: Bool, + verboseLogging: Bool + ) throws -> [String: URL] { + + // verify the container CLI binary exists at the resolved path + guard FileManager.default.fileExists(atPath: containerCLIPath.path()) else { + throw BuilderErrors.containerCLINotFound(containerCLI) + } + + print("-------------------------------------------------------------------------") + print("building \"\(packageIdentity)\" in \(containerCLI)") + print("-------------------------------------------------------------------------") + + if !disableDockerImageUpdate { + // update the underlying image, if necessary + print("updating \"\(baseImage)\" image") + try Utils.execute( + executable: containerCLIPath, + arguments: containerCLI.pullArguments(image: baseImage), + logLevel: verboseLogging ? .debug : .output + ) + } + + // get the build output path + let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" + let dockerBuildOutputPath = try Utils.execute( + executable: containerCLIPath, + arguments: containerCLI.runArguments( + baseImage: baseImage, + workingDirectory: "/workspace", + mounts: ["\(packageDirectory.path()):/workspace"], + env: nil, + command: buildOutputPathCommand + ), + logLevel: verboseLogging ? .debug : .silent + ) + guard let buildPathOutput = dockerBuildOutputPath.split(separator: "\n").last else { + throw BuilderErrors.failedParsingDockerOutput(dockerBuildOutputPath) + } + let buildOutputPath = URL( + string: buildPathOutput.replacingOccurrences(of: "/workspace/", with: packageDirectory.description) + )! + + // build the products + var builtProducts = [String: URL]() + for product in products { + print("building \"\(product)\"") + var buildCommand = + "swift build -c \(buildConfiguration.rawValue) --product \(product) --static-swift-stdlib" + if !noStrip { + buildCommand += " -Xlinker -s" + } + if let localPath = ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] { + // when developing locally, we must have the full swift-aws-lambda-runtime project in the container + // because Examples' Package.swift have a dependency on ../.. + // just like Package.swift's examples assume ../.., we assume we are two levels below the root project + let slice = packageDirectory.pathComponents.suffix(2) + try Utils.execute( + executable: containerCLIPath, + arguments: containerCLI.runArguments( + baseImage: baseImage, + workingDirectory: "/workspace/\(slice.joined(separator: "/"))", + mounts: ["\(packageDirectory.path())../..:/workspace"], + env: ["LAMBDA_USE_LOCAL_DEPS": localPath], + command: buildCommand + ), + logLevel: verboseLogging ? .debug : .output + ) + } else { + try Utils.execute( + executable: containerCLIPath, + arguments: containerCLI.runArguments( + baseImage: baseImage, + workingDirectory: "/workspace", + mounts: ["\(packageDirectory.path()):/workspace"], + env: nil, + command: buildCommand + ), + logLevel: verboseLogging ? .debug : .output + ) + } + let productPath = buildOutputPath.appending(path: product) + + guard FileManager.default.fileExists(atPath: productPath.path()) else { + print("expected '\(product)' binary at \"\(productPath.path())\"") + throw BuilderErrors.productExecutableNotFound(product) + } + builtProducts[product] = productPath + } + return builtProducts + } + + // TODO: explore using ziplib or similar instead of shelling out + private func package( + packageName: String, + products: [String: URL], + zipToolPath: URL, + outputDirectory: URL, + verboseLogging: Bool + ) throws -> [String: URL] { + + var archives = [String: URL]() + for (product, artifactPath) in products { + print("-------------------------------------------------------------------------") + print("archiving \"\(product)\"") + print("-------------------------------------------------------------------------") + + // prep zipfile location + let workingDirectory = outputDirectory.appending(path: product) + let zipfilePath = workingDirectory.appending(path: "\(product).zip") + if FileManager.default.fileExists(atPath: workingDirectory.path()) { + try FileManager.default.removeItem(atPath: workingDirectory.path()) + } + try FileManager.default.createDirectory(atPath: workingDirectory.path(), withIntermediateDirectories: true) + + // rename artifact to "bootstrap" + let relocatedArtifactPath = workingDirectory.appending(path: "bootstrap") + try FileManager.default.copyItem(atPath: artifactPath.path(), toPath: relocatedArtifactPath.path()) + + var arguments: [String] = [] + #if os(macOS) || os(Linux) + arguments = [ + "--recurse-paths", + "--symlinks", + zipfilePath.lastPathComponent, + relocatedArtifactPath.lastPathComponent, + ] + #else + throw BuilderErrors.unsupportedPlatform("can't or don't know how to create a zip file on this platform") + #endif + + // add resources + var artifactPathComponents = artifactPath.pathComponents + _ = artifactPathComponents.removeFirst() // Get rid of beginning "/" + _ = artifactPathComponents.removeLast() // Get rid of the name of the package + let artifactDirectory = "/\(artifactPathComponents.joined(separator: "/"))" + for fileInArtifactDirectory in try FileManager.default.contentsOfDirectory(atPath: artifactDirectory) { + guard let artifactURL = URL(string: "\(artifactDirectory)/\(fileInArtifactDirectory)") else { + continue + } + + guard artifactURL.pathExtension == "resources" else { + continue // Not resources, so don't copy + } + let resourcesDirectoryName = artifactURL.lastPathComponent + let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) + if FileManager.default.fileExists(atPath: artifactURL.path()) { + do { + arguments.append(resourcesDirectoryName) + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + } catch let error as CocoaError { + + // On Linux, when the build has been done with Docker, + // the source file are owned by root + // this causes a permission error **after** the files have been copied + // see https://github.com/awslabs/swift-aws-lambda-runtime/issues/449 + // see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282 + + // because this error happens after the files have been copied, we can ignore it + // this code checks if the destination file exists + // if they do, just ignore error, otherwise throw it up to the caller. + if !(error.code == CocoaError.Code.fileWriteNoPermission + && FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path())) + { + throw error + } // else just ignore it + } + } + } + + // run the zip tool + try Utils.execute( + executable: zipToolPath, + arguments: arguments, + customWorkingDirectory: workingDirectory, + logLevel: verboseLogging ? .debug : .silent + ) + + archives[product] = zipfilePath + } + return archives + } + + private enum AmazonLinuxVersion { + case al2 + case al2023 + } + + private func isAmazonLinux(_ version: AmazonLinuxVersion) -> Bool { + guard let data = FileManager.default.contents(atPath: "/etc/system-release"), + let release = String(data: data, encoding: .utf8) + else { + return false + } + switch version { + case .al2023: + return release.hasPrefix("Amazon Linux release 2023") + case .al2: + return release.hasPrefix("Amazon Linux release 2") + && !release.hasPrefix("Amazon Linux release 2023") + } + } + + private func displayAL2Warning() { + let yellow = "\u{001b}[33m" + let reset = "\u{001b}[0m" + print( + "\(yellow)warning: Amazon Linux 2 is deprecated. " + + "Consider migrating to Amazon Linux 2023 (--base-docker-image swift:-amazonlinux2023).\(reset)" + ) + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to build and package your lambda function. + + REQUIREMENTS: To use this plugin, you must have docker or container installed and started. + + USAGE: swift package --allow-network-connections docker lambda-build + [--help] [--verbose] + [--output-path ] + [--products ] + [--configuration debug | release] + [--swift-version ] + [--base-docker-image ] + [--disable-docker-image-update] + [--cross-compile ] + [--no-strip] + + + OPTIONS: + --verbose Produce verbose output for debugging. + --output-path The path of the binary package. + (default is `.build/plugins/AWSLambdaPackager/outputs/...`) + --products The list of executable targets to build. + (default is taken from Package.swift) + --configuration The build configuration (debug or release) + (default is release) + --swift-version The swift version to use for building. + (default is latest) + This parameter cannot be used when --base-docker-image is specified. + --base-docker-image The name of the base docker image to use for the build. + (default: swift:-amazonlinux2023) + Visit Docker Hub for all available swift tags: + https://hub.docker.com/_/swift/tags?name=amazonlinux + This parameter cannot be used when --swift-version is specified. + --disable-docker-image-update Do not attempt to update the docker image. + --cross-compile The cross-compilation method to use. + Values: docker, container, swift-static-sdk, custom-sdk + (default is docker) + Note: swift-static-sdk and custom-sdk are not yet supported. + --no-strip Do not strip debug symbols from the binary. + --help Show help information. + """ + ) + } +} + +@available(LambdaSwift 2.0, *) +enum CrossCompileMethod: String, CustomStringConvertible { + case docker + case container + case swiftStaticSdk = "swift-static-sdk" + case customSdk = "custom-sdk" + + var isSupported: Bool { + switch self { + case .docker, .container: return true + case .swiftStaticSdk, .customSdk: return false + } + } + + static func parse(_ value: String?) throws -> Self { + guard let value else { + return .docker + } + + guard let method = CrossCompileMethod(rawValue: value.lowercased()) else { + throw BuilderErrors.invalidArgument( + "invalid cross-compile method '\(value)'. Use 'docker', 'container', 'swift-static-sdk', or 'custom-sdk'." + ) + } + + guard method.isSupported else { + throw BuilderErrors.unsupportedCrossCompileMethod(method) + } + + return method + } + + /// Returns the container CLI pull arguments for the given image. + func pullArguments(image: String) -> [String] { + switch self { + case .docker: + return ["pull", image] + case .container: + return ["image", "pull", image] + case .swiftStaticSdk, .customSdk: + fatalError("pullArguments should not be called for unsupported cross-compile methods") + } + } + + /// Returns the container CLI run arguments for the given configuration. + func runArguments( + baseImage: String, + workingDirectory: String, + mounts: [String], + env: [String: String]?, + command: String + ) -> [String] { + switch self { + case .docker, .container: + var args: [String] = ["run", "--rm"] + for mount in mounts { + args += ["-v", mount] + } + if let env { + for (key, value) in env.sorted(by: { $0.key < $1.key }) { + args += ["--env", "\(key)=\(value)"] + } + } + args += ["-w", workingDirectory, baseImage, "bash", "-cl", command] + return args + case .swiftStaticSdk, .customSdk: + fatalError("runArguments should not be called for unsupported cross-compile methods") + } + } + + var description: String { + self.rawValue + } +} + +@available(LambdaSwift 2.0, *) +struct BuilderConfiguration: CustomStringConvertible { + + // passed by the user + public let help: Bool + public let outputDirectory: URL + public let products: [String] + public let buildConfiguration: BuildConfiguration + public let verboseLogging: Bool + public let baseDockerImage: String + public let disableDockerImageUpdate: Bool + public let crossCompileMethod: CrossCompileMethod + public let noStrip: Bool + public let explicitAL2Image: Bool + + // passed by the plugin + public let packageID: String + public let packageDisplayName: String + public let packageDirectory: URL + public let dockerToolPath: URL + public let zipToolPath: URL + + public init(arguments: [String]) throws { + var argumentExtractor = ArgumentExtractor(arguments) + + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let outputPathArgument = argumentExtractor.extractOption(named: "output-path") + let outputDirectoryArgument = argumentExtractor.extractOption(named: "output-directory") + let packageIDArgument = argumentExtractor.extractOption(named: "package-id") + let packageDisplayNameArgument = argumentExtractor.extractOption(named: "package-display-name") + let packageDirectoryArgument = argumentExtractor.extractOption(named: "package-directory") + let dockerToolPathArgument = argumentExtractor.extractOption(named: "docker-tool-path") + let zipToolPathArgument = argumentExtractor.extractOption(named: "zip-tool-path") + let productsArgument = argumentExtractor.extractOption(named: "products") + let configurationArgument = argumentExtractor.extractOption(named: "configuration") + let swiftVersionArgument = argumentExtractor.extractOption(named: "swift-version") + let baseDockerImageArgument = argumentExtractor.extractOption(named: "base-docker-image") + let disableDockerImageUpdateArgument = argumentExtractor.extractFlag(named: "disable-docker-image-update") > 0 + let crossCompileArgument = argumentExtractor.extractOption(named: "cross-compile") + let containerCliArgument = argumentExtractor.extractOption(named: "container-cli") // deprecated alias + let noStripArgument = argumentExtractor.extractFlag(named: "no-strip") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + + // help required ? + self.help = helpArgument + + // verbose logging required ? + self.verboseLogging = verboseArgument + + // package id + guard !packageIDArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-id argument is required") + } + self.packageID = packageIDArgument.first! + + // package display name + guard !packageDisplayNameArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-display-name argument is required") + } + self.packageDisplayName = packageDisplayNameArgument.first! + + // package directory + guard !packageDirectoryArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--package-directory argument is required") + } + self.packageDirectory = URL(fileURLWithPath: packageDirectoryArgument.first!) + + // docker tool path + guard !dockerToolPathArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--docker-tool-path argument is required") + } + self.dockerToolPath = URL(fileURLWithPath: dockerToolPathArgument.first!) + + // zip tool path + guard !zipToolPathArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--zip-tool-path argument is required") + } + self.zipToolPath = URL(fileURLWithPath: zipToolPathArgument.first!) + + // output directory + // --output-directory is a deprecated alias for --output-path (backward compatibility) + let resolvedOutputPath: String + if let outputPath = outputPathArgument.first { + resolvedOutputPath = outputPath + } else if let outputDirectory = outputDirectoryArgument.first { + print("warning: '--output-directory' is deprecated, use '--output-path' instead.") + resolvedOutputPath = outputDirectory + } else { + throw BuilderErrors.invalidArgument("--output-path is required") + } + self.outputDirectory = URL(fileURLWithPath: resolvedOutputPath) + + // products + guard !productsArgument.isEmpty else { + throw BuilderErrors.invalidArgument("--products argument is required") + } + self.products = productsArgument.flatMap { $0.split(separator: ",").map(String.init) } + + // build configuration + guard let buildConfigurationName = configurationArgument.first else { + throw BuilderErrors.invalidArgument("--configuration argument is required") + } + guard let _buildConfiguration = BuildConfiguration(rawValue: buildConfigurationName) else { + throw BuilderErrors.invalidArgument("invalid build configuration named '\(buildConfigurationName)'") + } + self.buildConfiguration = _buildConfiguration + + guard !(!swiftVersionArgument.isEmpty && !baseDockerImageArgument.isEmpty) else { + throw BuilderErrors.invalidArgument("--swift-version and --base-docker-image are mutually exclusive") + } + + let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image + + self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2023" + + self.disableDockerImageUpdate = disableDockerImageUpdateArgument + // --container-cli is a deprecated alias for --cross-compile (backward compatibility) + let resolvedCrossCompile = crossCompileArgument.first ?? containerCliArgument.first + self.crossCompileMethod = try CrossCompileMethod.parse(resolvedCrossCompile) + self.noStrip = noStripArgument + + // detect when user explicitly provides an AL2 (not AL2023) base image + if let explicitImage = baseDockerImageArgument.first { + self.explicitAL2Image = + explicitImage.contains("amazonlinux2") + && !explicitImage.contains("amazonlinux2023") + } else { + self.explicitAL2Image = false + } + + if self.verboseLogging { + print("-------------------------------------------------------------------------") + print("configuration") + print("-------------------------------------------------------------------------") + print(self) + } + } + + var description: String { + """ + { + outputDirectory: \(self.outputDirectory) + products: \(self.products) + buildConfiguration: \(self.buildConfiguration) + dockerToolPath: \(self.dockerToolPath) + baseDockerImage: \(self.baseDockerImage) + disableDockerImageUpdate: \(self.disableDockerImageUpdate) + crossCompileMethod: \(self.crossCompileMethod) + zipToolPath: \(self.zipToolPath) + packageID: \(self.packageID) + packageDisplayName: \(self.packageDisplayName) + packageDirectory: \(self.packageDirectory) + } + """ + } +} + +@available(LambdaSwift 2.0, *) +enum BuilderErrors: Error, CustomStringConvertible { + case invalidArgument(String) + case unsupportedPlatform(String) + case unknownProduct(String) + case productExecutableNotFound(String) + case unsupportedCrossCompileMethod(CrossCompileMethod) + case containerCLINotFound(CrossCompileMethod) + case failedWritingDockerfile + case failedParsingDockerOutput(String) + case processFailed([String], Int32) + + var description: String { + switch self { + case .invalidArgument(let description): + return description + case .unsupportedPlatform(let description): + return description + case .unknownProduct(let description): + return description + case .productExecutableNotFound(let product): + return "product executable not found '\(product)'" + case .unsupportedCrossCompileMethod(let method): + return + "The '\(method)' cross-compilation method is not yet supported. " + + "For information on how to install and use Swift cross-compilation SDKs, visit: " + + "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + case .containerCLINotFound(let method): + switch method { + case .docker: + return + "Docker is not installed or not found at the expected path. " + + "Install Docker from https://docs.docker.com/get-docker/" + case .container: + return + "Apple's 'container' CLI is not installed or not found at the expected path. " + + "Install it from https://github.com/apple/container" + case .swiftStaticSdk, .customSdk: + return + "The '\(method)' cross-compilation method is not yet supported. " + + "For information on how to install and use Swift cross-compilation SDKs, visit: " + + "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + } + case .failedWritingDockerfile: + return "failed writing dockerfile" + case .failedParsingDockerOutput(let output): + return "failed parsing docker output: '\(output)'" + case .processFailed(let arguments, let code): + return "\(arguments.joined(separator: " ")) failed with code \(code)" + } + } +} + +@available(LambdaSwift 2.0, *) +enum BuildConfiguration: String { + case debug + case release +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift new file mode 100644 index 000000000..6bf086b3e --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-deploy/Deployer.swift @@ -0,0 +1,1319 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Logging +import SotoCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@available(LambdaSwift 2.0, *) +enum DeploymentAction: Equatable { + /// Create a new function (function does not exist yet). + case create + /// Update an existing function's code. + case update + /// Delete an existing function. + case delete +} + +@available(LambdaSwift 2.0, *) +struct Deployer { + + // MARK: - Account ID and Function Existence + + /// Resolves the AWS account ID by calling STS GetCallerIdentity. + func resolveAccountId(using stsClient: STSClient) async throws -> String { + do { + let response = try await stsClient.getCallerIdentity() + guard let accountId = response.account else { + throw DeployerErrors.credentialResolutionFailed( + "STS GetCallerIdentity returned no account ID" + ) + } + return accountId + } catch let error as DeployerErrors { + throw error + } catch { + throw DeployerErrors.awsAPIError( + service: "STS", + operation: "GetCallerIdentity", + message: "\(error)" + ) + } + } + + /// Checks whether a Lambda function with the given name already exists. + func functionExists( + _ functionName: String, + using lambdaClient: LambdaClient + ) async throws -> Bool { + do { + _ = try await lambdaClient.getFunction( + GetFunctionRequest(functionName: functionName) + ) + return true + } catch { + // If the error indicates the resource was not found, the function doesn't exist + let errorDescription = "\(error)" + if errorDescription.contains("ResourceNotFoundException") + || errorDescription.contains("Function not found") + { + return false + } + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "GetFunction", + message: errorDescription + ) + } + } + + /// Determines the deployment action based on function existence and the `--delete` flag. + func determineDeploymentAction( + functionExists: Bool, + delete: Bool + ) throws -> DeploymentAction { + if delete { + guard functionExists else { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "DeleteFunction", + message: "cannot delete function: function does not exist" + ) + } + return .delete + } + return functionExists ? .update : .create + } + + // MARK: - S3 Staging + + /// AWS Lambda direct upload limit (50 MB compressed). + /// Archives larger than this must be staged through S3. + static let directUploadLimit: Int64 = 50 * 1024 * 1024 + + /// Constructs the deployment bucket name per the naming convention. + /// Format: `swift-aws-lambda-runtime--` + static func deploymentBucketName(region: String, accountId: String) -> String { + "swift-aws-lambda-runtime-\(region)-\(accountId)" + } + + /// Ensures the S3 deployment bucket exists. If the bucket does not exist, it is created. + /// - Parameters: + /// - bucket: The bucket name. + /// - region: The AWS region for the bucket. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func ensureBucketExists(bucket: String, region: Region, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Checking if deployment bucket '\(bucket)' exists...") + } + + do { + try await s3Client.headBucket(HeadBucketRequest(bucket: bucket)) + if verbose { + print("[verbose] Deployment bucket '\(bucket)' exists") + } + } catch let error as S3ErrorType where error.context?.responseCode == .notFound { + // Bucket does not exist, create it + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } catch let error as AWSResponseError where error.context?.responseCode == .notFound { + // Bucket does not exist (fallback for unrecognized error codes) + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } catch let error as AWSRawError where error.context.responseCode == .notFound { + // Bucket does not exist (fallback for HEAD responses with no body) + try await createBucket(bucket: bucket, region: region, using: s3Client, verbose: verbose) + } + } + + /// Creates an S3 bucket. Includes `LocationConstraint` when the region is not `us-east-1`. + private func createBucket(bucket: String, region: Region, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Creating deployment bucket '\(bucket)' in region '\(region.rawValue)'...") + } + + let request: CreateBucketRequest + if region == .useast1 { + request = CreateBucketRequest(bucket: bucket) + } else { + let locationConstraint = CreateBucketConfiguration(locationConstraint: region.rawValue) + request = CreateBucketRequest(bucket: bucket, createBucketConfiguration: locationConstraint) + } + + do { + try await s3Client.createBucket(request) + if verbose { + print("[verbose] Deployment bucket '\(bucket)' created successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "CreateBucket", + message: error.message ?? error.errorCode + ) + } + } + + /// Uploads a ZIP archive to S3 for deployment staging. + /// - Parameters: + /// - bucket: The bucket to upload to. + /// - key: The object key. + /// - data: The ZIP archive data. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func uploadToS3(bucket: String, key: String, data: Data, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + let sizeMB = Double(data.count) / (1024 * 1024) + print("[verbose] Uploading archive to s3://\(bucket)/\(key) (\(String(format: "%.1f", sizeMB)) MB)...") + } + + let request = PutObjectRequest(bucket: bucket, key: key, body: AWSHTTPBody(bytes: data)) + + do { + try await s3Client.putObject(request) + if verbose { + print("[verbose] Upload to S3 completed successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "PutObject", + message: error.message ?? error.errorCode + ) + } + } + + /// Deletes a staged S3 object after deployment completes. + /// The bucket is retained for reuse by future deployments. + /// - Parameters: + /// - bucket: The bucket containing the object. + /// - key: The object key to delete. + /// - s3Client: The S3 client to use. + /// - verbose: Whether to emit verbose progress output. + func deleteFromS3(bucket: String, key: String, using s3Client: S3Client, verbose: Bool) async throws { + if verbose { + print("[verbose] Cleaning up staged object s3://\(bucket)/\(key)...") + } + + let request = DeleteObjectRequest(bucket: bucket, key: key) + + do { + try await s3Client.deleteObject(request) + if verbose { + print("[verbose] Staged object deleted successfully") + } + } catch let error as S3ErrorType { + throw DeployerErrors.awsAPIError( + service: "S3", + operation: "DeleteObject", + message: error.message ?? error.errorCode + ) + } + } + + // MARK: - Function Orchestration + + /// Maps the deployer architecture enum to the Lambda API architecture enum. + private static func lambdaArchitecture( + from architecture: DeployerConfiguration.Architecture + ) -> LambdaArchitecture { + switch architecture { + case .x64: return .x86_64 + case .arm64: return .arm64 + } + } + + /// Determines the upload strategy based on archive size. + /// - Parameter archiveSize: The size of the ZIP archive in bytes. + /// - Returns: `true` if the archive should be uploaded directly (base64), `false` if S3 staging is required. + static func shouldUploadDirectly(archiveSize: Int64) -> Bool { + archiveSize <= directUploadLimit + } + + /// Creates a new Lambda function with the `provided.al2023` runtime. + /// + /// The function code is provided either as a base64-encoded ZIP payload (direct upload) + /// or as an S3 bucket/key reference (for archives exceeding the direct upload limit). + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - architecture: The target architecture (x64 or arm64). + /// - role: The IAM role ARN for the function's execution role. + /// - zipData: The ZIP archive data for direct upload (mutually exclusive with bucket/key). + /// - bucket: The S3 bucket containing the deployment package (mutually exclusive with zipData). + /// - key: The S3 key of the deployment package (mutually exclusive with zipData). + /// - lambdaClient: The Lambda client to use for the API call. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The response from the CreateFunction API including the function ARN. + @discardableResult + func createFunction( + name: String, + architecture: DeployerConfiguration.Architecture, + role: String, + zipData: Data? = nil, + bucket: String? = nil, + key: String? = nil, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> CreateFunctionResponse { + if verbose { + if zipData != nil { + let sizeMB = Double(zipData!.count) / (1024 * 1024) + print( + "[verbose] Creating Lambda function '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)..." + ) + } else { + print( + "[verbose] Creating Lambda function '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")..." + ) + } + } + + // Build the function code — either direct ZIP or S3 reference + let code: FunctionCode + if let zipData { + code = FunctionCode(zipFile: zipData.base64EncodedString()) + } else { + code = FunctionCode(s3Bucket: bucket, s3Key: key) + } + + let request = CreateFunctionRequest( + functionName: name, + role: role, + runtime: .providedAl2023, + handler: "bootstrap", + code: code, + architectures: [Self.lambdaArchitecture(from: architecture)], + packageType: .zip + ) + + do { + let response = try await lambdaClient.createFunction(request) + if verbose { + print("[verbose] Lambda function '\(name)' created successfully") + if let arn = response.functionArn { + print("[verbose] Function ARN: \(arn)") + } + } + return response + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "CreateFunction", + message: error.message ?? error.errorCode + ) + } + } + + /// Updates an existing Lambda function's code. + /// + /// The function code is provided either as a base64-encoded ZIP payload (direct upload) + /// or as an S3 bucket/key reference (for archives exceeding the direct upload limit). + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - zipData: The ZIP archive data for direct upload (mutually exclusive with bucket/key). + /// - bucket: The S3 bucket containing the deployment package (mutually exclusive with zipData). + /// - key: The S3 key of the deployment package (mutually exclusive with zipData). + /// - lambdaClient: The Lambda client to use for the API call. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The response from the UpdateFunctionCode API. + @discardableResult + func updateFunctionCode( + name: String, + zipData: Data? = nil, + bucket: String? = nil, + key: String? = nil, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> UpdateFunctionCodeResponse { + if verbose { + if zipData != nil { + let sizeMB = Double(zipData!.count) / (1024 * 1024) + print( + "[verbose] Updating function code for '\(name)' with direct upload (\(String(format: "%.1f", sizeMB)) MB)..." + ) + } else { + print( + "[verbose] Updating function code for '\(name)' with S3 reference s3://\(bucket ?? "")/\(key ?? "")..." + ) + } + } + + let request: UpdateFunctionCodeRequest + if let zipData { + request = UpdateFunctionCodeRequest( + functionName: name, + zipFile: zipData.base64EncodedString() + ) + } else { + request = UpdateFunctionCodeRequest( + functionName: name, + s3Bucket: bucket, + s3Key: key + ) + } + + do { + let response = try await lambdaClient.updateFunctionCode(request) + if verbose { + print("[verbose] Function code for '\(name)' updated successfully") + if let arn = response.functionArn { + print("[verbose] Function ARN: \(arn)") + } + } + return response + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "UpdateFunctionCode", + message: error.message ?? error.errorCode + ) + } + } + + /// Deletes a Lambda function and its associated IAM role. + /// + /// This first deletes the Lambda function using the DeleteFunction API, + /// then cleans up the IAM role and its attached policies. + /// + /// - Parameters: + /// - name: The Lambda function name. + /// - lambdaClient: The Lambda client to use for the API call. + /// - iamClient: The IAM client to use for role cleanup. + /// - verbose: Whether to emit verbose progress output. + func deleteFunction( + name: String, + using lambdaClient: LambdaClient, + iamClient: IAMClient, + verbose: Bool + ) async throws { + if verbose { + print("[verbose] Deleting Lambda function '\(name)'...") + } + + // Delete the function URL config first (ignore errors if not configured) + do { + try await lambdaClient.deleteFunctionUrlConfig( + DeleteFunctionUrlConfigRequest(functionName: name) + ) + if verbose { + print("[verbose] Deleted Function URL configuration for '\(name)'") + } + } catch { + if verbose { + print("[verbose] No Function URL to delete (or already deleted)") + } + } + + // Delete the Lambda function + let request = DeleteFunctionRequest(functionName: name) + do { + try await lambdaClient.deleteFunction(request) + if verbose { + print("[verbose] Lambda function '\(name)' deleted successfully") + } + } catch let error as LambdaErrorType { + throw DeployerErrors.awsAPIError( + service: "Lambda", + operation: "DeleteFunction", + message: error.message ?? error.errorCode + ) + } + + print("Deleted Lambda function '\(name)'") + + // Delete the associated IAM role and its policies + try await deleteIAMRole(functionName: name, using: iamClient, verbose: verbose) + } + + // MARK: - Function URL + + /// Configures a Function URL for the Lambda function with IAM authentication + /// and adds a resource-based permission allowing Function URL invocation. + /// + /// - Parameters: + /// - functionName: The Lambda function name. + /// - lambdaClient: The Lambda client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The Function URL string (HTTPS endpoint). + @discardableResult + func setupFunctionURL( + functionName: String, + accountId: String, + using lambdaClient: LambdaClient, + verbose: Bool + ) async throws -> String { + if verbose { + print("[verbose] Creating Function URL for '\(functionName)' with AWS_IAM auth type...") + } + + // Create the Function URL configuration with IAM authentication + let createUrlRequest = CreateFunctionUrlConfigRequest( + functionName: functionName, + authType: .awsIam + ) + + let createUrlResponse: CreateFunctionUrlConfigResponse + do { + createUrlResponse = try await lambdaClient.createFunctionUrlConfig(createUrlRequest) + } catch let error as LambdaErrorType { + throw DeployerErrors.functionURLCreationFailed( + "CreateFunctionUrlConfig failed: \(error.message ?? error.errorCode)" + ) + } + + guard let functionUrl = createUrlResponse.functionUrl else { + throw DeployerErrors.functionURLCreationFailed( + "CreateFunctionUrlConfig succeeded but no URL was returned" + ) + } + + if verbose { + print("[verbose] Function URL created: \(functionUrl)") + } + + // Add resource-based permission for Function URL invocation + // Scoped to the account to avoid overly-permissive resource policy + let addPermissionRequest = AddPermissionRequest( + functionName: functionName, + statementId: "FunctionURLAllowAccountAccess", + action: "lambda:InvokeFunctionUrl", + principal: accountId, + functionUrlAuthType: .awsIam + ) + + do { + try await lambdaClient.addPermission(addPermissionRequest) + if verbose { + print("[verbose] Added resource-based permission for Function URL invocation") + } + } catch let error as LambdaErrorType { + throw DeployerErrors.functionURLCreationFailed( + "AddPermission failed: \(error.message ?? error.errorCode)" + ) + } + + return functionUrl + } + + // MARK: - Source Code Detection + + /// Scans the Sources directory for usage of `FunctionURLRequest`, indicating + /// the project was scaffolded with `lambda-init --with-url` and needs a Function URL. + /// This allows `lambda-deploy` to auto-detect the need for `--with-url`. + private func detectFunctionURLUsage() -> Bool { + let sourcesDir = URL(fileURLWithPath: "Sources") + guard + let enumerator = FileManager.default.enumerator( + at: sourcesDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + else { + return false + } + + for case let fileURL as URL in enumerator { + guard fileURL.pathExtension == "swift" else { continue } + guard let contents = try? String(contentsOf: fileURL, encoding: .utf8) else { continue } + if contents.contains("FunctionURLRequest") || contents.contains("FunctionURLResponse") { + return true + } + } + return false + } + + // MARK: - Deploy + + func deploy(arguments: [String]) async throws { + let configuration = try DeployerConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + if configuration.verboseLogging { + print("-------------------------------------------------------------------------") + print("configuration") + print("-------------------------------------------------------------------------") + print(configuration) + } + + // Check for AWS configuration files and emit non-blocking warning if absent + self.checkAWSConfigurationFiles(verbose: configuration.verboseLogging) + + // Initialize AWSClient with the appropriate credential provider. + // When --profile is specified, build a selector chain that passes the + // profile name to each provider that understands profiles (configFile, + // sso, login) — mirroring the structure of .default so profiles using + // login_session or sso_session resolve, not just static / assume-role. + // Otherwise, use the default credential provider chain. + let clientLogger: Logger = { + var logger = Logger(label: "AWSLambdaDeployer") + logger.logLevel = configuration.verboseLogging ? .debug : .info + return logger + }() + + let credentialProvider: CredentialProviderFactory + if let profile = configuration.profile { + if configuration.verboseLogging { + print("[verbose] Using AWS profile: \(profile)") + } + credentialProvider = .selector( + .configFile(profile: profile), + .sso(profileName: profile), + .login(profileName: profile) + ) + } else { + credentialProvider = .default + } + + let awsClient = AWSClient( + credentialProvider: credentialProvider, + logger: clientLogger + ) + + do { + // Resolve the AWS region: use --region override, or fall through to environment/config resolution + let region: Region + if let regionOverride = configuration.region { + region = Region(rawValue: regionOverride) + if configuration.verboseLogging { + print("[verbose] Using region override: \(regionOverride)") + } + } else { + // Resolve region from environment variables (AWS_REGION or AWS_DEFAULT_REGION) + if let envRegion = ProcessInfo.processInfo.environment["AWS_REGION"] + ?? ProcessInfo.processInfo.environment["AWS_DEFAULT_REGION"] + { + region = Region(rawValue: envRegion) + if configuration.verboseLogging { + print("[verbose] Using region from environment: \(envRegion)") + } + } else { + // Default to us-east-1 if no region can be resolved + region = .useast1 + if configuration.verboseLogging { + print("[verbose] No region specified or found in environment, defaulting to us-east-1") + } + } + } + + // Verify credentials can be resolved by attempting to get them + do { + _ = try await awsClient.getCredential() + if configuration.verboseLogging { + print("[verbose] AWS credentials resolved successfully") + } + } catch { + throw DeployerErrors.credentialResolutionFailed( + "Unable to resolve AWS credentials. " + + "If your session has expired, run 'aws sso login' or 'aws login' to refresh it. " + + "Otherwise, ensure credentials are configured via " + + "environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), " + + "~/.aws/credentials file, ECS/EKS container credentials, or EC2 instance metadata. " + + "Error: \(error)" + ) + } + + // Initialize service clients + let stsClient = STSClient(client: awsClient, region: region) + let lambdaClient = LambdaClient(client: awsClient, region: region) + let iamClient = IAMClient(client: awsClient, region: region) + let s3Client = S3Client(client: awsClient, region: region) + + // Resolve account ID via STS + print("Resolving AWS account ID...") + let accountId = try await resolveAccountId(using: stsClient) + if configuration.verboseLogging { + print("[verbose] AWS account ID: \(accountId)") + } + + // Determine function name from products + guard let functionName = configuration.products.first else { + throw DeployerErrors.missingProduct + } + if configuration.delete { + print("Deleting function '\(functionName)' from \(region.rawValue)...") + } else { + print("Deploying function '\(functionName)' to \(region.rawValue)...") + } + + // Check if function already exists + print("Checking if function '\(functionName)' exists...") + let exists = try await functionExists(functionName, using: lambdaClient) + let action = try determineDeploymentAction(functionExists: exists, delete: configuration.delete) + + if configuration.verboseLogging { + print("[verbose] Function '\(functionName)' exists: \(exists), action: \(action)") + } + + switch action { + case .delete: + print("Deleting function '\(functionName)'...") + try await deleteFunction( + name: functionName, + using: lambdaClient, + iamClient: iamClient, + verbose: configuration.verboseLogging + ) + print("🗑️ Function '\(functionName)' deleted successfully.") + + case .create, .update: + // Resolve the ZIP archive path + let archiveURL: URL + if let inputDir = configuration.inputDirectory { + archiveURL = inputDir.appendingPathComponent("\(functionName)/\(functionName).zip") + } else { + // Default build output path. + // Check both the current Builder plugin path and the legacy Packager plugin path. + // The legacy AWSLambdaPackager path can be removed when the archive plugin is retired. + let builderPath = URL( + fileURLWithPath: + ".build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip" + ) + let packagerPath = URL( + fileURLWithPath: + ".build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/\(functionName)/\(functionName).zip" + ) + + if FileManager.default.fileExists(atPath: builderPath.path) { + archiveURL = builderPath + } else { + // Fallback to legacy packager path (used by `swift package archive`) + // TODO: remove this fallback when the AWSLambdaPackager plugin is retired + archiveURL = packagerPath + } + } + + guard FileManager.default.fileExists(atPath: archiveURL.path) else { + throw DeployerErrors.archiveNotFound(archiveURL) + } + + let zipData = try Data(contentsOf: archiveURL) + let archiveSize = Int64(zipData.count) + + if configuration.verboseLogging { + let sizeMB = Double(archiveSize) / (1024 * 1024) + print("[verbose] Archive: \(archiveURL.path) (\(String(format: "%.1f", sizeMB)) MB)") + print( + "[verbose] Upload strategy: \(Self.shouldUploadDirectly(archiveSize: archiveSize) ? "direct" : "S3 staging")" + ) + } + + // Determine upload strategy + var s3Bucket: String? = nil + var s3Key: String? = nil + + if !Self.shouldUploadDirectly(archiveSize: archiveSize) { + // Stage to S3 + print("Archive exceeds 50 MB, staging to S3...") + let bucketName = Self.deploymentBucketName(region: region.rawValue, accountId: accountId) + s3Key = "\(functionName)/\(functionName).zip" + try await ensureBucketExists( + bucket: bucketName, + region: region, + using: s3Client, + verbose: configuration.verboseLogging + ) + try await uploadToS3( + bucket: bucketName, + key: s3Key!, + data: zipData, + using: s3Client, + verbose: configuration.verboseLogging + ) + s3Bucket = bucketName + } + + let functionArn: String? + + if action == .create { + // Resolve IAM role + print("Resolving IAM role...") + let roleArn = try await resolveIAMRole( + functionName: functionName, + iamRole: configuration.iamRole, + using: iamClient, + verbose: configuration.verboseLogging + ) + + // Create the function + print("Creating Lambda function '\(functionName)'...") + let response: CreateFunctionResponse + if let bucket = s3Bucket, let key = s3Key { + response = try await createFunction( + name: functionName, + architecture: configuration.architecture, + role: roleArn, + bucket: bucket, + key: key, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + response = try await createFunction( + name: functionName, + architecture: configuration.architecture, + role: roleArn, + zipData: zipData, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + functionArn = response.functionArn + } else { + // Update the function code + print("Updating Lambda function '\(functionName)'...") + let response: UpdateFunctionCodeResponse + if let bucket = s3Bucket, let key = s3Key { + response = try await updateFunctionCode( + name: functionName, + bucket: bucket, + key: key, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + response = try await updateFunctionCode( + name: functionName, + zipData: zipData, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + functionArn = response.functionArn + } + + // Clean up S3 staged object + if let bucket = s3Bucket, let key = s3Key { + try await deleteFromS3( + bucket: bucket, + key: key, + using: s3Client, + verbose: configuration.verboseLogging + ) + } + + // Set up Function URL if requested (or auto-detected from source code) + var functionURL: String? = nil + let shouldSetupURL = configuration.withURL || detectFunctionURLUsage() + if shouldSetupURL { + if action == .create { + print("Configuring Function URL...") + functionURL = try await setupFunctionURL( + functionName: functionName, + accountId: accountId, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } else { + // On update, retrieve the existing Function URL + do { + let urlConfig = try await lambdaClient.getFunctionUrlConfig( + GetFunctionUrlConfigRequest(functionName: functionName) + ) + functionURL = urlConfig.functionUrl + } catch { + // No URL configured yet — set it up + print("Configuring Function URL...") + functionURL = try await setupFunctionURL( + functionName: functionName, + accountId: accountId, + using: lambdaClient, + verbose: configuration.verboseLogging + ) + } + } + } + + // Report success + reportDeploymentSuccess( + functionName: functionName, + functionArn: functionArn + ?? "arn:aws:lambda:\(region.rawValue):\(accountId):function:\(functionName)", + region: region.rawValue, + functionURL: functionURL + ) + } + + try await awsClient.shutdown() + } catch { + try await awsClient.shutdown() + throw error + } + } + + /// Check for the presence of AWS configuration files and emit an informational + /// warning if they are absent. This is non-blocking — deployment continues + /// regardless, because credentials may be available from other sources in the + /// credential provider chain (environment variables, ECS/EKS, EC2 IMDS). + private func checkAWSConfigurationFiles(verbose: Bool) { + let homeDirectory: String + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + homeDirectory = NSHomeDirectory() + #else + homeDirectory = ProcessInfo.processInfo.environment["HOME"] ?? "~" + #endif + + let configPath = "\(homeDirectory)/.aws/config" + let credentialsPath = "\(homeDirectory)/.aws/credentials" + + let configExists = FileManager.default.fileExists(atPath: configPath) + let credentialsExists = FileManager.default.fileExists(atPath: credentialsPath) + + if !configExists && !credentialsExists { + print( + """ + ⚠️ AWS configuration files not found (~/.aws/config and ~/.aws/credentials). + If you are running on a developer machine, install the AWS CLI and run + 'aws configure' to set up your credentials and default region. + On EC2, ECS, or EKS, credentials are typically provided automatically + by the instance or task role. + """ + ) + } else if verbose { + print("[verbose] AWS configuration files found:") + if configExists { print(" - \(configPath)") } + if credentialsExists { print(" - \(credentialsPath)") } + } + } + + // MARK: - IAM Role Management + + /// The ARN of the AWS managed policy for basic Lambda execution (CloudWatch Logs access). + private static let lambdaBasicExecutionRolePolicyARN = + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + + /// Constructs the IAM role name for a Lambda function. + /// Format: `swift-lambda--role` + static func iamRoleName(for functionName: String) -> String { + "swift-lambda-\(functionName)-role" + } + + /// The trust policy document that allows Lambda to assume the role. + private static let lambdaTrustPolicy = """ + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]} + """ + + /// Creates a new IAM role for the Lambda function with the Lambda trust policy, + /// attaches the AWSLambdaBasicExecutionRole managed policy, and waits for + /// role propagation before returning. + /// + /// - Parameters: + /// - functionName: The Lambda function name used to derive the role name. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The ARN of the created role. + @discardableResult + func createIAMRole(functionName: String, using iamClient: IAMClient, verbose: Bool) async throws -> String { + let roleName = Self.iamRoleName(for: functionName) + + if verbose { + print("[verbose] Creating IAM role '\(roleName)' with Lambda trust policy...") + } + + // Create the role with the Lambda assume-role trust policy + let createRoleRequest = IAMCreateRoleRequest( + roleName: roleName, + assumeRolePolicyDocument: Self.lambdaTrustPolicy, + path: "/", + description: + "Execution role for Lambda function '\(functionName)' created by swift-aws-lambda-runtime deploy plugin" + ) + + let createRoleResponse: IAMCreateRoleResponse + do { + createRoleResponse = try await iamClient.createRole(createRoleRequest) + } catch { + throw DeployerErrors.iamRoleCreationFailed( + "CreateRole failed for '\(roleName)': \(error)" + ) + } + + guard let roleARN = createRoleResponse.role?.arn else { + throw DeployerErrors.iamRoleCreationFailed( + "CreateRole succeeded but no ARN was returned for '\(roleName)'" + ) + } + + if verbose { + print("[verbose] IAM role created: \(roleARN)") + } + + // Attach the AWSLambdaBasicExecutionRole managed policy + let attachPolicyRequest = IAMAttachRolePolicyRequest( + roleName: roleName, + policyArn: Self.lambdaBasicExecutionRolePolicyARN + ) + + do { + try await iamClient.attachRolePolicy(attachPolicyRequest) + } catch { + throw DeployerErrors.iamRoleCreationFailed( + "AttachRolePolicy failed for '\(roleName)': \(error)" + ) + } + + if verbose { + print("[verbose] Attached AWSLambdaBasicExecutionRole policy to '\(roleName)'") + } + + // Wait for role propagation — IAM is eventually consistent and the role + // may not be usable by Lambda immediately after creation. + if verbose { + print("[verbose] Waiting 10 seconds for IAM role propagation...") + } + try await Task.sleep(for: .seconds(10)) + + if verbose { + print("[verbose] IAM role '\(roleName)' is ready") + } + + return roleARN + } + + /// Deletes the IAM role associated with a Lambda function, including + /// detaching managed policies and deleting inline policies. + /// + /// - Parameters: + /// - functionName: The Lambda function name used to derive the role name. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + func deleteIAMRole(functionName: String, using iamClient: IAMClient, verbose: Bool) async throws { + let roleName = Self.iamRoleName(for: functionName) + + if verbose { + print("[verbose] Deleting IAM role '\(roleName)'...") + } + + // Detach the AWSLambdaBasicExecutionRole managed policy + let detachPolicyRequest = IAMDetachRolePolicyRequest( + roleName: roleName, + policyArn: Self.lambdaBasicExecutionRolePolicyARN + ) + + do { + try await iamClient.detachRolePolicy(detachPolicyRequest) + if verbose { + print("[verbose] Detached AWSLambdaBasicExecutionRole from '\(roleName)'") + } + } catch { + // If the policy is not attached, ignore the error and continue + if verbose { + print("[verbose] Note: detaching managed policy failed (may not be attached): \(error)") + } + } + + // Delete any inline policies that may have been added + // We use a known inline policy name pattern for cleanup + let inlinePolicyName = "\(roleName)-inline-policy" + do { + let deleteInlinePolicyRequest = IAMDeleteRolePolicyRequest( + roleName: roleName, + policyName: inlinePolicyName + ) + try await iamClient.deleteRolePolicy(deleteInlinePolicyRequest) + if verbose { + print("[verbose] Deleted inline policy '\(inlinePolicyName)' from '\(roleName)'") + } + } catch { + // Inline policy may not exist, which is fine + if verbose { + print("[verbose] Note: deleting inline policy failed (may not exist): \(error)") + } + } + + // Delete the role itself + let deleteRoleRequest = IAMDeleteRoleRequest(roleName: roleName) + do { + try await iamClient.deleteRole(deleteRoleRequest) + if verbose { + print("[verbose] IAM role '\(roleName)' deleted successfully") + } + } catch { + throw DeployerErrors.awsAPIError( + service: "IAM", + operation: "DeleteRole", + message: "Failed to delete role '\(roleName)': \(error)" + ) + } + + print("Deleted IAM role '\(roleName)'") + } + + /// Resolves the IAM role for a Lambda function deployment. + /// + /// If an IAM role ARN is provided via `--iam-role`, it is returned directly. + /// Otherwise, a new role is created with the Lambda trust policy and the + /// AWSLambdaBasicExecutionRole managed policy attached. + /// + /// - Parameters: + /// - functionName: The Lambda function name. + /// - iamRole: An optional user-specified IAM role ARN. + /// - iamClient: The IAM client to use for API calls. + /// - verbose: Whether to emit verbose progress output. + /// - Returns: The IAM role ARN to use for the Lambda function. + func resolveIAMRole( + functionName: String, + iamRole: String?, + using iamClient: IAMClient, + verbose: Bool + ) async throws -> String { + // If the user specified an IAM role, use it directly + if let iamRole { + if verbose { + print("[verbose] Using user-specified IAM role: \(iamRole)") + } + return iamRole + } + + // Check if the role already exists + let roleName = Self.iamRoleName(for: functionName) + do { + let getRoleResponse = try await iamClient.getRole( + IAMGetRoleRequest(roleName: roleName) + ) + if let existingARN = getRoleResponse.role?.arn { + if verbose { + print("[verbose] Found existing IAM role: \(existingARN)") + } + return existingARN + } + } catch { + // Role does not exist — we will create it + if verbose { + print("[verbose] IAM role '\(roleName)' not found, creating a new one...") + } + } + + // Create a new role + return try await createIAMRole(functionName: functionName, using: iamClient, verbose: verbose) + } + + // MARK: - Success Reporting + + /// Reports a successful deployment to the developer, including the function ARN, + /// deployment region, and a ready-to-use invocation command. + /// + /// - Parameters: + /// - functionName: The deployed Lambda function name. + /// - functionArn: The ARN of the deployed Lambda function. + /// - region: The AWS region where the function was deployed. + /// - functionURL: The Function URL if `--with-url` was used, or `nil` otherwise. + func reportDeploymentSuccess( + functionName: String, + functionArn: String, + region: String, + functionURL: String? + ) { + print("") + print("🚀 Deployment complete!") + print(" Function ARN: \(functionArn)") + print(" Region: \(region)") + + if let functionURL { + print(" Function URL: \(functionURL)") + print("") + print("Invoke your function with:") + print("") + print( + " (eval $(aws configure export-credentials --format env) && curl --aws-sigv4 \"aws:amz:\(region):lambda\" --user \"$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY\" -H \"x-amz-security-token: $AWS_SESSION_TOKEN\" \"\(functionURL)?name=World\" )" + ) + } else { + print("") + print("Invoke your function with:") + print( + #" aws lambda invoke --function-name \#(functionName) --region \#(region) --payload $(echo '{"name":"World","age":30}' | base64) /tmp/out.json > /dev/null && cat /tmp/out.json"# + ) + } + print("") + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to deploy a Lambda function to AWS. + + USAGE: swift package --allow-network-connections all:443 lambda-deploy + [--help] [--verbose] + [--with-url] + [--delete] + [--region ] + [--profile ] + [--iam-role ] + [--input-directory ] + [--architecture ] + [--products ] + + OPTIONS: + --verbose Produce verbose output for debugging. + --with-url Create a Function URL for the Lambda function. + The URL uses AWS_IAM authentication, restricted to + authenticated principals in your AWS account. + --delete Delete the Lambda function, its IAM role, and + Function URL (if any). + --region The AWS region to deploy to. + (default: resolved from AWS configuration) + --profile The named AWS profile to use for credentials and region. + (default: default credential provider chain) + --iam-role The ARN of an existing IAM role for the Lambda function. + (default: create a new role) + --input-directory The path to the directory containing the deployment + ZIP archive produced by lambda-build. + (default: .build/plugins/AWSLambdaBuilder/outputs/...) + --architecture The Lambda function architecture (x64 or arm64). + (default: host architecture - \(DeployerConfiguration.Architecture.host.rawValue)) + --products The list of executable targets to deploy. + (default is taken from Package.swift) + --help Show help information. + """ + ) + } +} + +@available(LambdaSwift 2.0, *) +struct DeployerConfiguration: CustomStringConvertible { + let help: Bool + let verboseLogging: Bool + let withURL: Bool + let delete: Bool + let region: String? + let profile: String? + let iamRole: String? + let inputDirectory: URL? + let architecture: Architecture + let products: [String] + + enum Architecture: String { + case x64 + case arm64 + + static var host: Architecture { + #if arch(x86_64) + return .x64 + #else + return .arm64 + #endif + } + } + + init(arguments: [String]) throws { + var argumentExtractor = ArgumentExtractor(arguments) + + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let withURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 + let deleteArgument = argumentExtractor.extractFlag(named: "delete") > 0 + let regionArgument = argumentExtractor.extractOption(named: "region") + let profileArgument = argumentExtractor.extractOption(named: "profile") + let iamRoleArgument = argumentExtractor.extractOption(named: "iam-role") + let inputDirectoryArgument = argumentExtractor.extractOption(named: "input-directory") + let architectureArgument = argumentExtractor.extractOption(named: "architecture") + let productsArgument = argumentExtractor.extractOption(named: "products") + + // help required? + self.help = helpArgument + + // verbose logging required? + self.verboseLogging = verboseArgument + + // create a Function URL? + self.withURL = withURLArgument + + // delete the function? + self.delete = deleteArgument + + // AWS region (nil means Soto resolves it) + self.region = regionArgument.first + + // AWS profile from ~/.aws/config (nil means default credential chain) + self.profile = profileArgument.first + + // IAM role ARN (nil means create a new role) + self.iamRole = iamRoleArgument.first + + // input directory for the ZIP archive + if let inputDir = inputDirectoryArgument.first { + self.inputDirectory = URL(fileURLWithPath: inputDir) + } else { + self.inputDirectory = nil + } + + // architecture + if let archString = architectureArgument.first { + guard let arch = Architecture(rawValue: archString) else { + throw DeployerErrors.invalidArchitecture(archString) + } + self.architecture = arch + } else { + self.architecture = .host + } + + // products + self.products = productsArgument.flatMap { $0.split(separator: ",").map(String.init) } + } + + var description: String { + """ + { + verboseLogging: \(self.verboseLogging) + withURL: \(self.withURL) + delete: \(self.delete) + region: \(self.region ?? "") + profile: \(self.profile ?? "") + iamRole: \(self.iamRole ?? "") + inputDirectory: \(self.inputDirectory?.path() ?? "") + architecture: \(self.architecture.rawValue) + products: \(self.products) + } + """ + } +} + +@available(LambdaSwift 2.0, *) +enum DeployerErrors: Error, CustomStringConvertible { + case invalidArchitecture(String) + case credentialResolutionFailed(String) + case awsAPIError(service: String, operation: String, message: String) + case archiveNotFound(URL) + case functionURLCreationFailed(String) + case iamRoleCreationFailed(String) + case missingProduct + + var description: String { + switch self { + case .invalidArchitecture(let value): + return "invalid architecture '\(value)'. Use 'x64' or 'arm64'." + case .credentialResolutionFailed(let message): + return "AWS credential resolution failed: \(message)" + case .awsAPIError(let service, let operation, let message): + return "AWS \(service) \(operation) error: \(message)" + case .archiveNotFound(let url): + return "deployment archive not found at '\(url.path())'" + case .functionURLCreationFailed(let message): + return "failed to create Function URL: \(message)" + case .iamRoleCreationFailed(let message): + return "failed to create IAM role: \(message)" + case .missingProduct: + return "no product specified. Use --products or define an executable target in Package.swift." + } + } +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift new file mode 100644 index 000000000..4bda99c08 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Initializer.swift @@ -0,0 +1,214 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@available(LambdaSwift 2.0, *) +struct Initializer { + + func initialize(arguments: [String]) async throws { + + let configuration = try InitializerConfiguration(arguments: arguments) + + if configuration.help { + self.displayHelpMessage() + return + } + + // Find the main entry point file in the Sources directory + let sourcesDir = configuration.destinationDir.appendingPathComponent("Sources") + let entryPoint = try findEntryPoint(in: sourcesDir) + + // Back up the original file + let backupURL = entryPoint.appendingPathExtension("bak") + if FileManager.default.fileExists(atPath: entryPoint.path) { + try? FileManager.default.copyItem(at: entryPoint, to: backupURL) + if configuration.verboseLogging { + print("Backed up original file to: \(backupURL.path)") + } + } + + // Overwrite with the Lambda template + do { + let template = TemplateType.template(for: configuration.templateType) + try template.write(to: entryPoint, atomically: true, encoding: .utf8) + + if configuration.verboseLogging { + print("File written at: \(entryPoint.path)") + } + + let relativePath = entryPoint.path.replacingOccurrences( + of: configuration.destinationDir.path + "/", + with: "" + ) + print("✅ Lambda function written to \(relativePath)") + print("📦 You can now package with: 'swift package lambda-build'") + } catch { + print("🛑 Failed to create the Lambda function file: \(error)") + } + } + + /// Finds the main entry point Swift file in the Sources directory. + /// + /// Strategy: + /// 1. Look for a file containing `@main` or a `main.swift` + /// 2. If Sources has a single subdirectory, look for `.swift` in it + /// 3. Fall back to `Sources/main.swift` + private func findEntryPoint(in sourcesDir: URL) throws -> URL { + guard FileManager.default.fileExists(atPath: sourcesDir.path) else { + // No Sources directory yet — use the classic path + return sourcesDir.appendingPathComponent("main.swift") + } + + // List immediate children of Sources/ + let contents = try FileManager.default.contentsOfDirectory( + at: sourcesDir, + includingPropertiesForKeys: [.isDirectoryKey], + options: [.skipsHiddenFiles] + ) + + // Find subdirectories (typical Swift package layout: Sources//) + let subdirs = contents.filter { url in + (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true + } + + // If there's exactly one subdirectory, look inside it + if let targetDir = subdirs.first, subdirs.count == 1 { + let targetName = targetDir.lastPathComponent + + // Check for main.swift first + let mainSwift = targetDir.appendingPathComponent("main.swift") + if FileManager.default.fileExists(atPath: mainSwift.path) { + return mainSwift + } + + // Check for .swift (what `swift package init --type executable` creates) + let namedFile = targetDir.appendingPathComponent("\(targetName).swift") + if FileManager.default.fileExists(atPath: namedFile.path) { + return namedFile + } + + // Look for any .swift file containing @main + let swiftFiles = try FileManager.default.contentsOfDirectory( + at: targetDir, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ).filter { $0.pathExtension == "swift" } + + for file in swiftFiles { + if let content = try? String(contentsOf: file, encoding: .utf8), + content.contains("@main") + { + return file + } + } + + // No match found — default to .swift (will be created) + return namedFile + } + + // No subdirectory or multiple subdirectories — check for main.swift directly in Sources/ + let mainSwift = sourcesDir.appendingPathComponent("main.swift") + if FileManager.default.fileExists(atPath: mainSwift.path) { + return mainSwift + } + + // Check for any .swift file in Sources/ containing @main + let topLevelSwiftFiles = contents.filter { $0.pathExtension == "swift" } + for file in topLevelSwiftFiles { + if let content = try? String(contentsOf: file, encoding: .utf8), + content.contains("@main") + { + return file + } + } + + // Fall back to Sources/main.swift + return mainSwift + } + + private func displayHelpMessage() { + print( + """ + OVERVIEW: A SwiftPM plugin to scaffold a HelloWorld Lambda function. + By default, it creates a Lambda function that receives a JSON + document and responds with another JSON document. + + USAGE: swift package lambda-init + [--help] [--verbose] + [--with-url] + [--allow-writing-to-package-directory] + + OPTIONS: + --with-url Create a Lambda function exposed with an URL + --allow-writing-to-package-directory Don't ask for permissions to write files. + --verbose Produce verbose output for debugging. + --help Show help information. + """ + ) + } +} + +private enum TemplateType { + case `default` + case url + + static func template(for type: TemplateType) -> String { + switch type { + case .default: return functionWithJSONTemplate + case .url: return functionWithUrlTemplate + } + } +} + +private struct InitializerConfiguration: CustomStringConvertible { + public let help: Bool + public let verboseLogging: Bool + public let destinationDir: URL + public let templateType: TemplateType + + public init(arguments: [String]) throws { + var argumentExtractor = ArgumentExtractor(arguments) + let verboseArgument = argumentExtractor.extractFlag(named: "verbose") > 0 + let helpArgument = argumentExtractor.extractFlag(named: "help") > 0 + let destDirArgument = argumentExtractor.extractOption(named: "dest-dir") + let templateURLArgument = argumentExtractor.extractFlag(named: "with-url") > 0 + + // help required ? + self.help = helpArgument + + // verbose logging required ? + self.verboseLogging = verboseArgument + + // dest dir + self.destinationDir = URL(fileURLWithPath: destDirArgument[0]) + + // template type. Default is the JSON one + self.templateType = templateURLArgument ? .url : .default + } + + var description: String { + """ + { + verboseLogging: \(self.verboseLogging) + destinationDir: \(self.destinationDir) + templateType: \(self.templateType) + } + """ + } +} diff --git a/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift new file mode 100644 index 000000000..3d5f057b8 --- /dev/null +++ b/Sources/AWSLambdaPluginHelper/lambda-init/Template.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +let functionWithUrlTemplate = #""" + import AWSLambdaRuntime + import AWSLambdaEvents + + // in this example we receive a FunctionURLRequest and we return a FunctionURLResponse + // https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + + let runtime = LambdaRuntime { + (event: FunctionURLRequest, context: LambdaContext) -> FunctionURLResponse in + + guard let name = event.queryStringParameters?["name"] else { + return FunctionURLResponse(statusCode: .badRequest) + } + + return FunctionURLResponse(statusCode: .ok, body: #"{ "message" : "Hello \#\#(name)" } "#) + } + + try await runtime.run() + """# + +let functionWithJSONTemplate = #""" + import AWSLambdaRuntime + + // the data structure to represent the input parameter + struct HelloRequest: Decodable { + let name: String + let age: Int + } + + // the data structure to represent the output response + struct HelloResponse: Encodable { + let greetings: String + } + + // in this example we receive a HelloRequest JSON and we return a HelloResponse JSON + + // the Lambda runtime + let runtime = LambdaRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) + } + + // start the loop + try await runtime.run() + """# diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md index d9bbc1510..52b46faf1 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md @@ -11,9 +11,7 @@ Learn how to deploy your Swift Lambda functions to AWS. ### Overview -There are multiple ways to deploy your Swift code to AWS Lambda. The very first time, you'll probably use the AWS Console to create a new Lambda function and upload your code as a zip file. However, as you iterate on your code, you'll want to automate the deployment process. - -To take full advantage of the cloud, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. +There are multiple ways to deploy your Swift code to AWS Lambda. The simplest way is to use the `lambda-deploy` plugin that handles IAM role creation, code upload, and function management automatically. For more complex deployments, we recommend using Infrastructure as Code (IaC) tools like the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) or [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/). These tools allow you to define your infrastructure and deployment process as code, which can be version-controlled and automated. In this section, we show you how to deploy your Swift Lambda functions using different AWS Tools. Alternatively, you might also consider using popular third-party tools like [Serverless Framework](https://www.serverless.com/), [Terraform](https://www.terraform.io/), or [Pulumi](https://www.pulumi.com/) to deploy Lambda functions and create and manage AWS infrastructure. @@ -22,8 +20,8 @@ Here is the content of this guide: * [Prerequisites](#prerequisites) * [Choosing the AWS Region where to deploy](#choosing-the-aws-region-where-to-deploy) * [The Lambda execution IAM role](#the-lambda-execution-iam-role) + * [Deploy your Lambda function with the lambda-deploy plugin](#deploy-your-lambda-function-with-the-lambda-deploy-plugin) * [Deploy your Lambda function with the AWS Console](#deploy-your-lambda-function-with-the-aws-console) - * [Deploy your Lambda function with the AWS Command Line Interface (CLI)](#deploy-your-lambda-function-with-the-aws-command-line-interface-cli) * [Deploy your Lambda function with AWS Serverless Application Model (SAM)](#deploy-your-lambda-function-with-aws-serverless-application-model-sam) * [Deploy your Lambda function with AWS Cloud Development Kit (CDK)](#deploy-your-lambda-function-with-aws-cloud-development-kit-cdk) * [Third-party tools](#third-party-tools) @@ -63,16 +61,30 @@ Here is the content of this guide: } ``` -3. A Swift Lambda function to deploy. +3. AWS CLI and credentials configuration. + + To deploy with the `lambda-deploy` plugin from your local machine, install the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and run `aws configure` to create the AWS configuration files (`~/.aws/config` and `~/.aws/credentials`). This stores your default region and credentials so the deploy plugin can access AWS. + + ```sh + aws configure + ``` + + > On EC2, ECS, or EKS, credentials are typically provided automatically by the instance or task role, so running `aws configure` is not required in those environments. - You need a Swift Lambda function to deploy. If you don't have one yet, you can use one of the examples in the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. +4. A Swift Lambda function to deploy. - Compile and package the function using the following command + You need a Swift Lambda function to deploy. If you don't have one yet, you can scaffold one using the `lambda-init` plugin: ```sh - swift package archive \ - --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 + swift package lambda-init --allow-writing-to-package-directory + ``` + + Or use one of the examples in the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. + + Compile and package the function using the following command: + + ```sh + swift package --allow-network-connections docker lambda-build ``` This command creates a ZIP file with the compiled Swift code. The ZIP file is located in the `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip` folder. @@ -101,6 +113,75 @@ A Lambda execution role is an AWS Identity and Access Management (IAM) role that When you create a Lambda function, you must specify an execution role. This role contains two main components: a trust policy that allows the Lambda service itself to assume the role, and permission policies that determine what AWS resources the function can access. By default, Lambda functions get basic permissions to write logs to CloudWatch Logs, but any additional permissions (like accessing S3 buckets or sending messages to SQS queues) must be explicitly added to the role's policies. Following the principle of least privilege, it's recommended to grant only the minimum permissions necessary for your function to operate, helping maintain the security of your serverless applications. +### Deploy your Lambda function with the lambda-deploy plugin + +The `lambda-deploy` plugin provides the simplest way to deploy your Lambda function from the command line. It handles IAM role creation, code upload, and function creation or update automatically. + +In this example, we're building the HelloWorld example from the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. + +#### Prerequisites + +Ensure you have configured your AWS credentials by running `aws configure`. This creates the `~/.aws/config` and `~/.aws/credentials` files that the deploy plugin reads to authenticate with AWS. + +```sh +aws configure +``` + +#### Create or update the function + +The `lambda-deploy` plugin automatically detects whether the function exists. If the function does not exist, it creates a new one (including the IAM role). If the function already exists, it updates the code. + +The command assumes you've already built the ZIP file with `swift package lambda-build`, as described in the [Prerequisites](#prerequisites) section. + +```sh +swift package --allow-network-connections all:443 lambda-deploy +``` + +When the deployment succeeds, the plugin reports the function ARN, region, and a ready-to-use invocation command. + +#### Invoke the function + +Use the command displayed by the plugin after deployment: + +```sh +aws lambda invoke \ + --function-name MyLambda \ + --payload $(echo '{"name":"World","age":30}' | base64) \ + /dev/stdout +``` + +#### Deploy with a Function URL + +To expose the function as an HTTPS endpoint, add the `--with-url` option: + +```sh +swift package --allow-network-connections all:443 lambda-deploy --with-url +``` + +> **Security:** The Function URL uses IAM authentication (`AWS_IAM`) and the resource policy restricts access to authenticated IAM principals in your AWS account only. Unauthenticated requests and requests from other accounts are rejected. Callers must sign requests with AWS Signature Version 4. See [Lambda Function URL security and auth model](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html) for details. + +> **Note:** Function URLs deliver a `FunctionURLRequest` and expect a `FunctionURLResponse`. Your Lambda function code must use these types (from `AWSLambdaEvents`) instead of plain JSON structs. Use `swift package lambda-init --with-url` to scaffold a function with the correct request/response pattern, or see the [Streaming+FunctionUrl](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/Streaming+FunctionUrl) example. + +The plugin reports the Function URL and a ready-to-use `curl` command. Invoke it with: + +```sh +(eval $(aws configure export-credentials --format env) && \ + curl --aws-sigv4 "aws:amz:us-east-1:lambda" \ + --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ + -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ + "https://.lambda-url..on.aws/") +``` + +> The `eval $(aws configure export-credentials --format env)` command exports your AWS credentials as environment variables from whatever credential source you have configured (SSO, config file, assumed role, etc.). + +#### Delete the function + +Remove the Lambda function and its associated IAM role: + +```sh +swift package --allow-network-connections all:443 lambda-deploy --delete +``` + ### Deploy your Lambda function with the AWS Console In this section, we deploy the HelloWorld example function using the AWS Console. The HelloWorld function is a simple function that takes a `String` as input and returns a `String`. @@ -126,7 +207,7 @@ On the right side, select **Upload from** and select **.zip file**. ![Console - select zip file](console-40-select-zip-file) -Select the zip file created with the `swift package archive` command as described in the [Prerequisites](#prerequisites) section. +Select the zip file created with the `swift package lambda-build` command as described in the [Prerequisites](#prerequisites) section. Select **Save** @@ -180,120 +261,6 @@ Select the `HelloWorld-role-xxxx` role and select **Delete**. Confirm the deleti ![Console - delete IAM role](console-80-delete-role) -### Deploy your Lambda function with the AWS Command Line Interface (CLI) - -You can deploy your Lambda function using the AWS Command Line Interface (CLI). The CLI is a unified tool to manage your AWS services from the command line and automate your operations through scripts. The CLI is available for Windows, macOS, and Linux. Follow the [installation](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configuration](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) instructions in the AWS CLI User Guide. - -In this example, we're building the HelloWorld example from the [Examples](https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples) directory. - -#### Create the function - -To create a function, you must first create the function execution role and define the permission. Then, you create the function with the `create-function` command. - -The command assumes you've already created the ZIP file with the `swift package archive` command, as described in the [Prerequisites](#prerequisites) section. - -```sh -# enter your AWS Account ID -export AWS_ACCOUNT_ID=123456789012 - -# Allow the Lambda service to assume the execution role -cat < assume-role-policy.json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] -} -EOF - -# Create the execution role -aws iam create-role \ ---role-name lambda_basic_execution \ ---assume-role-policy-document file://assume-role-policy.json - -# create permissions to associate with the role -cat < permissions.json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": "arn:aws:logs:*:*:*" - } - ] -} -EOF - -# Attach the permissions to the role -aws iam put-role-policy \ ---role-name lambda_basic_execution \ ---policy-name lambda_basic_execution_policy \ ---policy-document file://permissions.json - -# Create the Lambda function -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution -``` - -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -To update the function, use the `update-function-code` command after you've recompiled and archived your code again with the `swift package archive` command. - -```sh -aws lambda update-function-code \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip -``` - -#### Invoke the function - -Use the `invoke-function` command to invoke the function. You can pass a well-formed JSON payload as input to the function. The payload must be encoded in base64. The CLI returns the status code and stores the response in a file. - -```sh -# invoke the function -aws lambda invoke \ ---function-name MyLambda \ ---payload $(echo \"Swift Lambda function\" | base64) \ -out.txt - -# show the response -cat out.txt - -# delete the response file -rm out.txt -``` - -#### Delete the function - -To cleanup, first delete the Lambda funtion, then delete the IAM role. - -```sh -# delete the Lambda function -aws lambda delete-function --function-name MyLambda - -# delete the IAM policy attached to the role -aws iam delete-role-policy --role-name lambda_basic_execution --policy-name lambda_basic_execution_policy - -# delete the IAM role -aws iam delete-role --role-name lambda_basic_execution -``` - ### Deploy your Lambda function with AWS Serverless Application Model (SAM) AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. It provides a simplified way to define the Amazon API Gateway APIs, AWS Lambda functions, and Amazon DynamoDB tables needed by your serverless application. You can define your serverless application in a single file, and SAM will use it to deploy your function and all its dependencies. @@ -512,7 +479,7 @@ export class LambdaApiStack extends cdk.Stack { } } ``` -The code assumes you already built and packaged the APIGateway Lambda function with the `swift package archive` command, as described in the [Prerequisites](#prerequisites) section. +The code assumes you already built and packaged the APIGateway Lambda function with the `swift package lambda-build` command, as described in the [Prerequisites](#prerequisites) section. You can write code to add an API Gateway to invoke your Lambda function. The following code creates an HTTP API Gateway that triggers the Lambda function. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh index cd0126aa4..7d5086cdd 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh @@ -1 +1 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh index c230f61cb..fc6e56308 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh @@ -1,9 +1,9 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "palindrome" in docker ------------------------------------------------------------------------- -updating "swift:amazonlinux2023" docker image +updating "swift:6.1-amazonlinux2023" docker image amazonlinux2023: Pulling from library/swift Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 Status: Image is up to date for swift:amazonlinux2023 diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh index a3ab15611..7e1205aa7 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh @@ -1,9 +1,9 @@ -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "palindrome" in docker ------------------------------------------------------------------------- -updating "swift:amazonlinux2023" docker image +updating "swift:6.1-amazonlinux2023" docker image amazonlinux2023: Pulling from library/swift Digest: sha256:df06a50f70e2e87f237bd904d2fc48195742ebda9f40b4a821c4d39766434009 Status: Image is up to date for swift:amazonlinux2023 diff --git a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md index a32724e1d..1b2dc069b 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md @@ -113,7 +113,7 @@ AWS Lambda runtime runs on Amazon Linux. You must compile your code for Amazon L > Be sure to have [Docker](https://docs.docker.com/desktop/install/mac-install/) installed for this step. ```sh -swift package archive --allow-network-connections docker --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ------------------------------------------------------------------------- building "MyFirstLambdaFunction" in docker @@ -130,27 +130,34 @@ building "MyFirstLambdaFunction" archiving "MyFirstLambdaFunction" ------------------------------------------------------------------------- 1 archive created - * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip + * MyFirstLambdaFunction at /Users/YourUserName/MyFirstLambdaFunction/.build/plugins/AWSLambdaBuilder/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip +``` +6. Deploy on AWS Lambda -cp .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyFirstLambdaFunction/MyFirstLambdaFunction.zip ~/Desktop -``` +> Be sure [to have an AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and configured (`aws configure`) to follow these steps. -> Note: The archive command currently defaults to Amazon Linux 2 (`swift:amazonlinux2`) as the build environment. Amazon Linux 2 reaches End of Life on June 30, 2026 and the default will change to Amazon Linux 2023 after that date. To migrate early, re-run the archive command with `--base-docker-image swift:amazonlinux2023`. When deploying a function built on Amazon Linux 2023, you must use the `provided.al2023` Lambda runtime instead of `provided.al2`. +Deploy your function using the `lambda-deploy` plugin: -6. Deploy on AWS Lambda +```sh +swift package --allow-network-connections all:443 lambda-deploy +``` -> Be sure [to have an AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) to follow these steps. +The plugin creates the IAM role, uploads the code, and creates the Lambda function automatically. When the deployment succeeds, it reports the function ARN and a ready-to-use `aws lambda invoke` command. -- Connect to the [AWS Console](https://console.aws.amazon.com) -- Navigate to Lambda -- Create a function -- Select **Provide your own bootstrap on Amazon Linux 2** as **Runtime** -- Select an **Architecture** that matches the one of the machine where you build the code. Select **x86_64** when you build on Intel-based Macs or **arm64** for Apple Silicon-based Macs. -- Upload the ZIP create during step 5 -- Select the **Test** tab, enter a test event such as `{"name": "Seb", "age": 50}` and select **Test** +Invoke your function: -If the test succeeds, you will see the result: `{"greetings":"Hello Seb. You look younger than your age."}`. +```sh +aws lambda invoke \ + --function-name MyFirstLambdaFunction \ + --payload $(echo '{"name":"World","age":30}' | base64) \ + /dev/stdout +``` + +When you're done, clean up the function and its IAM role: +```sh +swift package --allow-network-connections all:443 lambda-deploy --delete +``` -Congratulations 🎉! You just wrote, test, build, and deployed a Lambda function written in Swift. +Congratulations 🎉! You just wrote, tested, built, and deployed a Lambda function written in Swift. diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial index 45a850049..2681147fd 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial @@ -63,7 +63,7 @@ @Step { Add the `platform` section. - It defines on which Apple platforms the code can be executed. Since Lambda functions are supposed to be run on Linux servers with Amazon Linux 2, it is reasonable to make them run only on macOS, for debugging for example. It does not make sense to run this code on iOS, iPadOS, tvOS, and watchOS. + It defines on which Apple platforms the code can be executed. Since Lambda functions are supposed to be run on Linux servers with Amazon Linux 2023, it is reasonable to make them run only on macOS, for debugging for example. It does not make sense to run this code on iOS, iPadOS, tvOS, and watchOS. @Code(name: "Package.swift", file: 03-02-02-package.swift) } @Step { diff --git a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial index 702135322..4d8a81e07 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial +++ b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial @@ -12,7 +12,7 @@ @Steps { - AWS Lambda runs on top of [Amazon Linux 2](https://aws.amazon.com/amazon-linux-2/). You must therefore compile your code for Linux. The AWS Lambda Runtime for Swift uses Docker to do so. Once the code is compiled, it must be assembled in a ZIP file before being deployed in the cloud. + AWS Lambda runs on top of [Amazon Linux 2023](https://aws.amazon.com/linux/amazon-linux-2023/). You must therefore compile your code for Linux. The AWS Lambda Runtime for Swift uses Docker to do so. Once the code is compiled, it must be assembled in a ZIP file before being deployed in the cloud. The AWS Lambda Runtime for Swift provides a [Swift Package Manager plugin](https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md) to compile and zip your Lambda function in one simple step. @Step { @@ -22,13 +22,13 @@ } @Step { - In a terminal, invoke the `archive` command to build and zip your Lambda function. + In a terminal, invoke the `lambda-build` command to build and zip your Lambda function. @Code(name: "Commands in a Terminal", file: 04-01-02-plugin-archive.sh) } @Step { - The plugin starts a Docker container running Amazon Linux 2 and compile your Lambda function code. It then creates a zip file. When everything goes well, you should see an output similar to this one. + The plugin starts a Docker container running Amazon Linux 2023 and compiles your Lambda function code. It then creates a zip file. When everything goes well, you should see an output similar to this one. @Code(name: "Commands in a Terminal", file: 04-01-03-plugin-archive.sh) } @@ -87,7 +87,7 @@ } @Step { - Enter a **Function name**. I choose `PalindromeLambda`. Select `Provide your own bootstrap on Amazon Linux 2` as **Runtime**. And select `arm64` as **Architecture** when you build on a Mac with Apple Silicon. Leave all other parameter as default, and select **Create function** on the bottom right part. + Enter a **Function name**. I choose `PalindromeLambda`. Select `Amazon Linux 2023` as **Runtime**. And select `arm64` as **Architecture** when you build on a Mac with Apple Silicon. Leave all other parameter as default, and select **Create function** on the bottom right part. > The runtime architecture for Lambda (`arm64` or `x86_64`) must match the one of the machine where you compiled the code. When you compiled on an Intel-based Mac, use `x86_64`. When compiling on an Apple Silicon-based Mac select `arm64`. diff --git a/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift new file mode 100644 index 000000000..9e54887f5 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/BuilderConfigurationTests.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaPluginHelper + +@Suite("BuilderConfiguration argument parsing") +struct BuilderConfigurationTests { + + // MARK: - Helper + + /// Provides the mandatory arguments required for BuilderConfiguration to parse successfully. + private func defaultArgs( + outputPath: String = "/tmp/output", + products: String = "MyLambda", + configuration: String = "release" + ) -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", outputPath, + "--products", products, + "--configuration", configuration, + ] + } + + // MARK: - Cross-compile parsing (Requirement 2.7) + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile with valid value 'docker'") + func crossCompileDocker() throws { + let args = defaultArgs() + ["--cross-compile", "docker"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .docker) + } + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile with valid value 'container'") + func crossCompileContainer() throws { + let args = defaultArgs() + ["--cross-compile", "container"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .container) + } + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile with 'swift-static-sdk' throws unsupported error") + func crossCompileSwiftStaticSdk() throws { + let args = defaultArgs() + ["--cross-compile", "swift-static-sdk"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile with 'custom-sdk' throws unsupported error") + func crossCompileCustomSdk() throws { + let args = defaultArgs() + ["--cross-compile", "custom-sdk"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile with invalid value throws error") + func crossCompileInvalidValue() throws { + let args = defaultArgs() + ["--cross-compile", "invalid-method"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--cross-compile defaults to docker when omitted") + func crossCompileDefaultsToDocker() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.crossCompileMethod == .docker) + } + + // MARK: - No-strip flag (Requirements 2.5, 2.6) + + @available(LambdaSwift 2.0, *) + @Test("--no-strip flag is detected when present") + func noStripFlagPresent() throws { + let args = defaultArgs() + ["--no-strip"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.noStrip == true) + } + + @available(LambdaSwift 2.0, *) + @Test("--no-strip flag defaults to false when omitted") + func noStripFlagAbsent() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.noStrip == false) + } + + // MARK: - Output directory deprecated alias (Requirement 7.5) + + @available(LambdaSwift 2.0, *) + @Test("--output-directory deprecated alias maps to outputDirectory") + func outputDirectoryAlias() throws { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-directory", "/custom/output/path", + "--products", "MyLambda", + "--configuration", "release", + ] + let config = try BuilderConfiguration(arguments: args) + #expect(config.outputDirectory.path().hasSuffix("custom/output/path")) + } + + @available(LambdaSwift 2.0, *) + @Test("--output-path takes precedence when both are provided") + func outputPathTakesPrecedence() throws { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/primary/path", + "--output-directory", "/deprecated/path", + "--products", "MyLambda", + "--configuration", "release", + ] + let config = try BuilderConfiguration(arguments: args) + #expect(config.outputDirectory.path().hasSuffix("primary/path")) + } + + // MARK: - Mutual exclusion of --swift-version and --base-docker-image (Requirement 2.17) + + @available(LambdaSwift 2.0, *) + @Test("--swift-version and --base-docker-image together throws error") + func mutualExclusionSwiftVersionAndBaseImage() throws { + let args = defaultArgs() + ["--swift-version", "6.0", "--base-docker-image", "swift:6.0-amazonlinux2023"] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } + + @available(LambdaSwift 2.0, *) + @Test("--swift-version alone is accepted") + func swiftVersionAlone() throws { + let args = defaultArgs() + ["--swift-version", "6.0"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:6.0-amazonlinux2023") + } + + @available(LambdaSwift 2.0, *) + @Test("--base-docker-image alone is accepted") + func baseDockerImageAlone() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2023"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:5.10-amazonlinux2023") + } + + // MARK: - Default base image is amazonlinux2023 (Requirement 6.1) + + @available(LambdaSwift 2.0, *) + @Test("Default base image contains amazonlinux2023") + func defaultBaseImageIsAL2023() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage.contains("amazonlinux2023")) + } + + @available(LambdaSwift 2.0, *) + @Test("Default base image format without swift-version is swift:amazonlinux2023") + func defaultBaseImageFormatNoVersion() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:amazonlinux2023") + } + + @available(LambdaSwift 2.0, *) + @Test("Base image with --swift-version includes version prefix") + func baseImageWithSwiftVersion() throws { + let args = defaultArgs() + ["--swift-version", "6.1"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.baseDockerImage == "swift:6.1-amazonlinux2023") + } + + // MARK: - Explicit AL2 image detection + + @available(LambdaSwift 2.0, *) + @Test("Explicit AL2 image is detected") + func explicitAL2ImageDetected() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:5.10-amazonlinux2"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == true) + } + + @available(LambdaSwift 2.0, *) + @Test("AL2023 image is not flagged as explicit AL2") + func al2023ImageNotFlaggedAsAL2() throws { + let args = defaultArgs() + ["--base-docker-image", "swift:6.0-amazonlinux2023"] + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } + + @available(LambdaSwift 2.0, *) + @Test("Default image (no --base-docker-image) is not flagged as explicit AL2") + func defaultImageNotFlaggedAsAL2() throws { + let args = defaultArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift new file mode 100644 index 000000000..9ff2710ef --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/DeployerConfigurationTests.swift @@ -0,0 +1,270 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaPluginHelper + +@Suite("DeployerConfiguration argument parsing") +struct DeployerConfigurationTests { + + // MARK: - Architecture parsing (Requirement 3.14) + + @available(LambdaSwift 2.0, *) + @Test("Valid architecture x64 is parsed correctly") + func architectureX64() throws { + let config = try DeployerConfiguration(arguments: ["--architecture", "x64"]) + #expect(config.architecture == .x64) + } + + @available(LambdaSwift 2.0, *) + @Test("Valid architecture arm64 is parsed correctly") + func architectureArm64() throws { + let config = try DeployerConfiguration(arguments: ["--architecture", "arm64"]) + #expect(config.architecture == .arm64) + } + + @available(LambdaSwift 2.0, *) + @Test("Invalid architecture throws error") + func invalidArchitectureThrows() throws { + #expect(throws: DeployerErrors.self) { + _ = try DeployerConfiguration(arguments: ["--architecture", "mips"]) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Invalid architecture value produces descriptive error") + func invalidArchitectureMessage() throws { + do { + _ = try DeployerConfiguration(arguments: ["--architecture", "sparc"]) + Issue.record("Expected an error to be thrown") + } catch let error as DeployerErrors { + let description = error.description + #expect(description.contains("sparc")) + #expect(description.contains("x64") || description.contains("arm64")) + } + } + + // MARK: - Default architecture matches host (Requirement 3.13) + + @available(LambdaSwift 2.0, *) + @Test("Default architecture matches host when --architecture is omitted") + func defaultArchitectureMatchesHost() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.architecture == .host) + // Verify .host resolves to a valid value on this machine + #if arch(x86_64) + #expect(config.architecture == .x64) + #else + #expect(config.architecture == .arm64) + #endif + } + + // MARK: - Region parsing (Requirement 3.25) + + @available(LambdaSwift 2.0, *) + @Test("Region is parsed from arguments") + func regionParsing() throws { + let config = try DeployerConfiguration(arguments: ["--region", "eu-west-1"]) + #expect(config.region == "eu-west-1") + } + + @available(LambdaSwift 2.0, *) + @Test("Region is nil when not specified") + func regionDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.region == nil) + } + + @available(LambdaSwift 2.0, *) + @Test("Region with equals syntax is parsed") + func regionEqualsSyntax() throws { + let config = try DeployerConfiguration(arguments: ["--region=us-west-2"]) + #expect(config.region == "us-west-2") + } + + // MARK: - IAM role parsing + + @available(LambdaSwift 2.0, *) + @Test("IAM role is parsed from arguments") + func iamRoleParsing() throws { + let roleArn = "arn:aws:iam::123456789012:role/my-role" + let config = try DeployerConfiguration(arguments: ["--iam-role", roleArn]) + #expect(config.iamRole == roleArn) + } + + @available(LambdaSwift 2.0, *) + @Test("IAM role is nil when not specified") + func iamRoleDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.iamRole == nil) + } + + // MARK: - Input directory parsing + + @available(LambdaSwift 2.0, *) + @Test("Input directory is parsed from arguments") + func inputDirectoryParsing() throws { + let config = try DeployerConfiguration(arguments: ["--input-directory", "/tmp/build/output"]) + #expect(config.inputDirectory != nil) + #expect(config.inputDirectory?.path().contains("/tmp/build/output") == true) + } + + @available(LambdaSwift 2.0, *) + @Test("Input directory is nil when not specified") + func inputDirectoryDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.inputDirectory == nil) + } + + // MARK: - With URL flag parsing + + @available(LambdaSwift 2.0, *) + @Test("--with-url flag is detected") + func withURLFlag() throws { + let config = try DeployerConfiguration(arguments: ["--with-url"]) + #expect(config.withURL == true) + } + + @available(LambdaSwift 2.0, *) + @Test("--with-url defaults to false") + func withURLDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.withURL == false) + } + + // MARK: - Delete flag parsing + + @available(LambdaSwift 2.0, *) + @Test("--delete flag is detected") + func deleteFlag() throws { + let config = try DeployerConfiguration(arguments: ["--delete"]) + #expect(config.delete == true) + } + + @available(LambdaSwift 2.0, *) + @Test("--delete defaults to false") + func deleteDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.delete == false) + } + + // MARK: - Help flag (Requirement 3.25) + + @available(LambdaSwift 2.0, *) + @Test("--help flag is detected") + func helpFlag() throws { + let config = try DeployerConfiguration(arguments: ["--help"]) + #expect(config.help == true) + } + + @available(LambdaSwift 2.0, *) + @Test("--help defaults to false") + func helpDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.help == false) + } + + // MARK: - Verbose flag + + @available(LambdaSwift 2.0, *) + @Test("--verbose flag is detected") + func verboseFlag() throws { + let config = try DeployerConfiguration(arguments: ["--verbose"]) + #expect(config.verboseLogging == true) + } + + @available(LambdaSwift 2.0, *) + @Test("--verbose defaults to false") + func verboseDefaultFalse() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.verboseLogging == false) + } + + // MARK: - Products parsing + + @available(LambdaSwift 2.0, *) + @Test("Products are parsed from arguments") + func productsParsing() throws { + let config = try DeployerConfiguration(arguments: ["--products", "MyLambda"]) + #expect(config.products == ["MyLambda"]) + } + + @available(LambdaSwift 2.0, *) + @Test("Multiple comma-separated products are parsed") + func multipleProductsParsing() throws { + let config = try DeployerConfiguration(arguments: ["--products", "FuncA,FuncB,FuncC"]) + #expect(config.products == ["FuncA", "FuncB", "FuncC"]) + } + + @available(LambdaSwift 2.0, *) + @Test("Products default to empty array") + func productsDefaultEmpty() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.products.isEmpty) + } + + // MARK: - Profile parsing + + @available(LambdaSwift 2.0, *) + @Test("Profile is parsed from arguments") + func profileParsing() throws { + let config = try DeployerConfiguration(arguments: ["--profile", "staging"]) + #expect(config.profile == "staging") + } + + @available(LambdaSwift 2.0, *) + @Test("Profile is nil when not specified") + func profileDefaultNil() throws { + let config = try DeployerConfiguration(arguments: []) + #expect(config.profile == nil) + } + + // MARK: - Combined arguments + + @available(LambdaSwift 2.0, *) + @Test("Multiple options parsed together") + func combinedArguments() throws { + let config = try DeployerConfiguration(arguments: [ + "--region", "ap-southeast-1", + "--architecture", "arm64", + "--with-url", + "--verbose", + "--iam-role", "arn:aws:iam::123456789012:role/test", + "--input-directory", "/tmp/output", + "--products", "MyFunc", + ]) + #expect(config.region == "ap-southeast-1") + #expect(config.architecture == .arm64) + #expect(config.withURL == true) + #expect(config.verboseLogging == true) + #expect(config.iamRole == "arn:aws:iam::123456789012:role/test") + #expect(config.inputDirectory?.path().contains("/tmp/output") == true) + #expect(config.products == ["MyFunc"]) + } + + @available(LambdaSwift 2.0, *) + @Test("Delete with region and products") + func deleteWithOptions() throws { + let config = try DeployerConfiguration(arguments: [ + "--delete", + "--region", "us-east-1", + "--products", "MyFunc", + ]) + #expect(config.delete == true) + #expect(config.region == "us-east-1") + #expect(config.products == ["MyFunc"]) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift new file mode 100644 index 000000000..13a63a9d3 --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/DeployerS3Tests.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaPluginHelper + +// MARK: - Deployment Bucket Name Construction Tests + +@Suite("Deployment bucket name construction") +struct DeploymentBucketNameTests { + + @available(LambdaSwift 2.0, *) + @Test("Bucket name has correct format: swift-aws-lambda-runtime--") + func bucketNameFormat() { + let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") + #expect(name == "swift-aws-lambda-runtime-us-east-1-123456789012") + } + + @available(LambdaSwift 2.0, *) + @Test("Bucket name with eu-west-1 region") + func bucketNameEuWest1() { + let name = Deployer.deploymentBucketName(region: "eu-west-1", accountId: "987654321098") + #expect(name == "swift-aws-lambda-runtime-eu-west-1-987654321098") + } + + @available(LambdaSwift 2.0, *) + @Test("Bucket name with ap-southeast-2 region") + func bucketNameApSoutheast2() { + let name = Deployer.deploymentBucketName(region: "ap-southeast-2", accountId: "111222333444") + #expect(name == "swift-aws-lambda-runtime-ap-southeast-2-111222333444") + } + + @available(LambdaSwift 2.0, *) + @Test("Bucket name with us-west-2 region and different account") + func bucketNameUsWest2() { + let name = Deployer.deploymentBucketName(region: "us-west-2", accountId: "000000000000") + #expect(name == "swift-aws-lambda-runtime-us-west-2-000000000000") + } + + @available(LambdaSwift 2.0, *) + @Test("Bucket name is always lowercase") + func bucketNameIsLowercase() { + let name = Deployer.deploymentBucketName(region: "us-east-1", accountId: "123456789012") + #expect(name == name.lowercased()) + } + + @available(LambdaSwift 2.0, *) + @Test( + "Bucket name length is between 3 and 63 characters (valid S3 name)", + arguments: [ + ("us-east-1", "123456789012"), + ("eu-west-1", "987654321098"), + ("ap-southeast-2", "111222333444"), + ("us-gov-west-1", "555666777888"), + ("me-south-1", "000000000001"), + ] + ) + func bucketNameLengthIsValid(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + #expect(name.count >= 3, "Bucket name must be at least 3 characters") + #expect(name.count <= 63, "Bucket name must be at most 63 characters") + } + + @available(LambdaSwift 2.0, *) + @Test( + "Bucket name contains only valid S3 characters (lowercase, digits, hyphens)", + arguments: [ + ("us-east-1", "123456789012"), + ("ap-northeast-1", "999888777666"), + ("eu-central-1", "012345678901"), + ] + ) + func bucketNameContainsOnlyValidCharacters(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + let validCharacters = "abcdefghijklmnopqrstuvwxyz0123456789-" + let allValid = name.allSatisfy { validCharacters.contains($0) } + #expect(allValid, "Bucket name must contain only lowercase letters, digits, and hyphens") + } +} + +// MARK: - Archive Size Threshold Tests + +@Suite("Archive size threshold and upload strategy") +struct ArchiveSizeThresholdTests { + + @available(LambdaSwift 2.0, *) + @Test("directUploadLimit is exactly 50 MB") + func directUploadLimitValue() { + let expectedLimit: Int64 = 50 * 1024 * 1024 + #expect(Deployer.directUploadLimit == expectedLimit) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of exactly 50 MB should upload directly") + func exactly50MBUploadsDirectly() { + let fiftyMB: Int64 = 50 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMB) == true) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 50 MB + 1 byte should use S3 staging") + func fiftyMBPlusOneUsesS3() { + let fiftyMBPlusOne: Int64 = 50 * 1024 * 1024 + 1 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyMBPlusOne) == false) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 0 bytes should upload directly") + func zeroBytesUploadsDirectly() { + #expect(Deployer.shouldUploadDirectly(archiveSize: 0) == true) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 1 byte should upload directly") + func oneByteUploadsDirectly() { + #expect(Deployer.shouldUploadDirectly(archiveSize: 1) == true) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 49 MB should upload directly") + func fortyNineMBUploadsDirectly() { + let fortyNineMB: Int64 = 49 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fortyNineMB) == true) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 51 MB should use S3 staging") + func fiftyOneMBUsesS3() { + let fiftyOneMB: Int64 = 51 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: fiftyOneMB) == false) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 100 MB should use S3 staging") + func hundredMBUsesS3() { + let hundredMB: Int64 = 100 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: hundredMB) == false) + } + + @available(LambdaSwift 2.0, *) + @Test("Archive of 250 MB (Lambda max) should use S3 staging") + func lambdaMaxSizeUsesS3() { + let lambdaMax: Int64 = 250 * 1024 * 1024 + #expect(Deployer.shouldUploadDirectly(archiveSize: lambdaMax) == false) + } +} diff --git a/Tests/AWSLambdaPluginHelperTests/Placeholder.swift b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift new file mode 100644 index 000000000..e808efbbb --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/Placeholder.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Placeholder file to keep the test target valid when no other test files are present. diff --git a/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift new file mode 100644 index 000000000..93bd0c41e --- /dev/null +++ b/Tests/AWSLambdaPluginHelperTests/PropertyTests.swift @@ -0,0 +1,629 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaPluginHelper + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// MARK: - Property 2: Deprecated option alias equivalence + +/// **Validates: Requirements 7.5, 7.6** +/// +/// For any path value, `--output-directory ` produces the same `outputDirectory` +/// as `--output-path `. +@Suite("Property 2: Deprecated option alias equivalence") +struct DeprecatedAliasEquivalencePropertyTests { + + static let samplePaths: [String] = [ + "/tmp/output", + "/usr/local/build", + "/home/user/projects/my-lambda/output", + "/var/folders/abc/xyz", + "/a", + "/output-with-dashes", + "/path/with spaces", + "/path_with_underscores/nested/deep/dir", + "/simple", + "/very/long/path/that/goes/deep/into/the/filesystem/structure/for/testing", + "/tmp", + "/usr", + "/build", + "/opt/lambda/out", + "/Users/developer/Desktop/project/dist", + "/root/deploy", + "/mnt/data/builds/release", + "/srv/app/output", + "/home/ci/workspace/artifacts", + "/tmp/swift-build-output", + "/Volumes/External/builds", + "/private/tmp/xcode-build", + "/workspace/output", + "/code/bin", + "/artifacts/v1.0", + "/release/arm64", + "/debug/output", + "/home/user/.build/output", + "/tmp/output-2024", + "/project/dist/lambda", + "/builds/nightly/latest", + "/ci/artifacts/staging", + "/deploy/packages", + "/lambda/archives", + "/swift/build/products", + "/output123", + "/tmp/a/b/c/d/e/f/g", + "/data/out", + "/results/final", + "/packages/compiled", + "/snapshots/build-42", + "/Users/test/output", + "/tmp/build-output-1", + "/tmp/build-output-2", + "/tmp/build-output-3", + "/var/output/lambda", + "/opt/builds/release-2", + "/srv/builds/debug-1", + "/home/dev/out", + "/workspace/dist", + "/project/target", + "/builds/latest", + "/artifacts/snapshot", + "/deploy/staging", + "/lambda/output", + "/archive/dir", + "/compiled/bins", + "/packaged/zips", + "/release/packages", + "/debug/bins", + "/test/output", + "/ci/output", + "/cd/output", + "/dev/output", + "/staging/output", + "/prod/output", + "/alpha/output", + "/beta/output", + "/gamma/output", + "/delta/output", + "/epsilon/output", + "/zeta/output", + "/eta/output", + "/theta/output", + "/iota/output", + "/kappa/output", + "/lambda-out", + "/mu/output", + "/nu/output", + "/xi/output", + "/omicron/output", + "/pi/output", + "/rho/output", + "/sigma/output", + "/tau/output", + "/upsilon/output", + "/phi/output", + "/chi/output", + "/psi/output", + "/omega/output", + "/final/output", + "/last/output", + "/end/output", + "/done/output", + "/complete/output", + "/finished/output", + "/ready/output", + "/built/output", + "/assembled/output", + "/crafted/output", + "/forged/output", + "/made/output", + "/created/output", + "/generated/output", + "/produced/output", + ] + + private func baseArgs(excludingOutputArgs: Bool = true) -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @available(LambdaSwift 2.0, *) + @Test("--output-directory produces same outputDirectory as --output-path", arguments: samplePaths) + func deprecatedAliasEquivalence(path: String) throws { + let argsWithOutputPath = baseArgs() + ["--output-path", path] + let configWithOutputPath = try BuilderConfiguration(arguments: argsWithOutputPath) + + let argsWithOutputDirectory = baseArgs() + ["--output-directory", path] + let configWithOutputDirectory = try BuilderConfiguration(arguments: argsWithOutputDirectory) + + #expect( + configWithOutputPath.outputDirectory == configWithOutputDirectory.outputDirectory, + "--output-directory '\(path)' should produce same outputDirectory as --output-path '\(path)'" + ) + } +} + +// MARK: - Property 3: Cross-compile method parsing round-trip + +/// **Validates: Requirements 2.7** +/// +/// For any valid `CrossCompileMethod` enum case, `rawValue` → parse → original case. +/// Note: swift-static-sdk and custom-sdk throw "unsupported" on `parse()`, but their +/// rawValue round-trips through the enum initializer correctly. +@Suite("Property 3: Cross-compile method parsing round-trip") +struct CrossCompileMethodRoundTripPropertyTests { + + @available(LambdaSwift 2.0, *) + static var allCases: [CrossCompileMethod] { + [ + .docker, + .container, + .swiftStaticSdk, + .customSdk, + ] + } + + @available(LambdaSwift 2.0, *) + @Test("rawValue → init(rawValue:) round-trips for all CrossCompileMethod cases", arguments: allCases) + func rawValueRoundTrip(method: CrossCompileMethod) { + let rawValue = method.rawValue + let parsed = CrossCompileMethod(rawValue: rawValue) + #expect(parsed == method, "CrossCompileMethod(rawValue: \"\(rawValue)\") should produce \(method)") + } +} + +// MARK: - Property 4: Mutual exclusion of --swift-version and --base-docker-image + +/// **Validates: Requirements 2.17** +/// +/// For any non-empty swift-version and any non-empty base-docker-image, parsing throws an error. +@Suite("Property 4: Mutual exclusion of --swift-version and --base-docker-image") +struct MutualExclusionPropertyTests { + + static let swiftVersions: [String] = [ + "5.9", "5.10", "6.0", "6.1", "6.2", + "5.9.1", "5.9.2", "5.10.1", "6.0.1", "6.0.2", + "6.1.0", "6.1.1", "6.2.0", "7.0", "8.0", + "5.0", "5.1", "5.2", "5.3", "5.4", + "5.5", "5.6", "5.7", "5.8", "4.2", + ] + + static let dockerImages: [String] = [ + "swift:5.9-amazonlinux2023", + "swift:6.0-amazonlinux2023", + "swift:6.1-amazonlinux2023", + "swift:latest-amazonlinux2023", + "swift:5.10-amazonlinux2", + "myregistry/swift:6.0-al2023", + "custom-image:latest", + "ubuntu:22.04", + "swift:nightly-amazonlinux2023", + "ghcr.io/swift/swift:6.0", + ] + + static let combinations: [(String, String)] = { + var result: [(String, String)] = [] + for version in swiftVersions { + for image in dockerImages { + result.append((version, image)) + } + } + // Return first 100 + return Array(result.prefix(100)) + }() + + private func baseArgs() -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @available(LambdaSwift 2.0, *) + @Test( + "Both --swift-version and --base-docker-image throws error", + arguments: combinations + ) + func mutualExclusionThrows(version: String, image: String) { + let args = baseArgs() + ["--swift-version", version, "--base-docker-image", image] + #expect(throws: (any Error).self) { + _ = try BuilderConfiguration(arguments: args) + } + } +} + +// MARK: - Property 5: Deployment bucket name construction + +/// **Validates: Requirements 3.17, 3.18** +/// +/// For any valid region and 12-digit account ID, result matches +/// "swift-aws-lambda-runtime--" and is a valid S3 bucket name. +@Suite("Property 5: Deployment bucket name construction") +struct DeploymentBucketNamePropertyTests { + + static let regions: [String] = [ + "us-east-1", "us-east-2", "us-west-1", "us-west-2", + "eu-west-1", "eu-west-2", "eu-west-3", "eu-central-1", "eu-central-2", + "eu-north-1", "eu-south-1", "eu-south-2", + "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "ap-south-1", "ap-south-2", "ap-east-1", + "sa-east-1", "ca-central-1", "ca-west-1", + "me-south-1", "me-central-1", + "af-south-1", "il-central-1", + "us-gov-west-1", "us-gov-east-1", + ] + + static let accountIds: [String] = [ + "123456789012", "000000000000", "999999999999", + "111111111111", "222222222222", "333333333333", + "444444444444", "555555555555", "666666666666", + "777777777777", "888888888888", "012345678901", + "109876543210", "100200300400", "001002003004", + ] + + static let combinations: [(String, String)] = { + var result: [(String, String)] = [] + for region in regions { + for accountId in accountIds { + result.append((region, accountId)) + } + } + // Return first 100 + return Array(result.prefix(100)) + }() + + @available(LambdaSwift 2.0, *) + @Test( + "Bucket name matches expected format and is a valid S3 name", + arguments: combinations + ) + func bucketNameConstruction(region: String, accountId: String) { + let name = Deployer.deploymentBucketName(region: region, accountId: accountId) + + // Verify format + let expectedName = "swift-aws-lambda-runtime-\(region)-\(accountId)" + #expect(name == expectedName, "Expected '\(expectedName)' but got '\(name)'") + + // Verify it's lowercase + #expect(name == name.lowercased(), "Bucket name must be all lowercase") + + // Verify length is between 3 and 63 characters (valid S3 bucket name) + #expect(name.count >= 3, "Bucket name must be at least 3 characters, got \(name.count)") + #expect(name.count <= 63, "Bucket name must be at most 63 characters, got \(name.count)") + + // Verify only valid S3 bucket name characters (lowercase letters, digits, hyphens) + let validChars = "abcdefghijklmnopqrstuvwxyz0123456789-" + let allValid = name.allSatisfy { validChars.contains($0) } + #expect(allValid, "Bucket name must contain only lowercase letters, digits, and hyphens") + + // Verify doesn't start or end with hyphen + #expect(!name.hasPrefix("-"), "Bucket name must not start with a hyphen") + #expect(!name.hasSuffix("-"), "Bucket name must not end with a hyphen") + } +} + +// MARK: - Property 6: Archive size determines upload strategy + +/// **Validates: Requirements 3.15, 3.19** +/// +/// For any size ≤ 50 MB → direct upload; for any size > 50 MB → S3 staging. +@Suite("Property 6: Archive size determines upload strategy") +struct ArchiveSizeUploadStrategyPropertyTests { + + static let fiftyMB: Int64 = 50 * 1024 * 1024 + + static let sizesAtOrBelowLimit: [Int64] = { + var sizes: [Int64] = [0, 1, 100, 1024, 10_000, 100_000, 1_000_000] + // Add sizes approaching the boundary + let limit: Int64 = fiftyMB + for offset: Int64 in stride(from: 50, through: 0, by: -1) { + sizes.append(limit - offset) + } + // Add various sizes below limit + let moreSizes: [Int64] = [ + Int64(1024 * 1024), + Int64(5 * 1024 * 1024), + Int64(10 * 1024 * 1024), + Int64(20 * 1024 * 1024), + Int64(25 * 1024 * 1024), + Int64(30 * 1024 * 1024), + Int64(40 * 1024 * 1024), + Int64(45 * 1024 * 1024), + Int64(49 * 1024 * 1024), + ] + sizes.append(contentsOf: moreSizes) + return Array(Set(sizes).sorted().prefix(100)) + }() + + static let sizesAboveLimit: [Int64] = { + var sizes: [Int64] = [] + let limit: Int64 = fiftyMB + // Add sizes just above the boundary + for offset: Int64 in 1...51 { + sizes.append(limit + offset) + } + // Add various sizes above limit + let moreSizes: [Int64] = [ + Int64(51 * 1024 * 1024), + Int64(55 * 1024 * 1024), + Int64(60 * 1024 * 1024), + Int64(75 * 1024 * 1024), + Int64(100 * 1024 * 1024), + Int64(150 * 1024 * 1024), + Int64(200 * 1024 * 1024), + Int64(250 * 1024 * 1024), + Int64(300 * 1024 * 1024), + Int64(500 * 1024 * 1024), + Int64(1024 * 1024 * 1024), + ] + sizes.append(contentsOf: moreSizes) + return Array(Set(sizes).sorted().prefix(100)) + }() + + @available(LambdaSwift 2.0, *) + @Test("Sizes at or below 50 MB should upload directly", arguments: sizesAtOrBelowLimit) + func sizeAtOrBelowLimitUploadsDirect(size: Int64) { + #expect( + Deployer.shouldUploadDirectly(archiveSize: size) == true, + "Archive of \(size) bytes (≤ 50 MB) should upload directly" + ) + } + + @available(LambdaSwift 2.0, *) + @Test("Sizes above 50 MB should use S3 staging", arguments: sizesAboveLimit) + func sizeAboveLimitUsesS3(size: Int64) { + #expect( + Deployer.shouldUploadDirectly(archiveSize: size) == false, + "Archive of \(size) bytes (> 50 MB) should use S3 staging" + ) + } +} + +// MARK: - Property 8: AL2 warning logic + +/// **Validates: Requirements 6.2, 6.3** +/// +/// For any image containing "amazonlinux2" but NOT "amazonlinux2023" → explicitAL2Image is true. +/// For "amazonlinux2023" or default → explicitAL2Image is false. +@Suite("Property 8: AL2 warning emitted only for explicit AL2 image selection") +struct AL2WarningLogicPropertyTests { + + static let al2Images: [String] = [ + "swift:5.9-amazonlinux2", + "swift:5.10-amazonlinux2", + "swift:6.0-amazonlinux2", + "swift:6.1-amazonlinux2", + "swift:latest-amazonlinux2", + "myregistry/swift:5.9-amazonlinux2", + "custom-amazonlinux2-image:v1", + "swift:nightly-amazonlinux2", + "ghcr.io/swift:6.0-amazonlinux2", + "amazonlinux2-swift:6.0", + "swift:5.8-amazonlinux2", + "swift:5.7-amazonlinux2", + "swift:5.6-amazonlinux2", + "registry.example.com/swift:6.0-amazonlinux2", + "my-amazonlinux2-build:latest", + "swift-amazonlinux2:6.1", + "public.ecr.aws/swift:6.0-amazonlinux2", + "test-amazonlinux2-image", + "swift:5.5-amazonlinux2", + "dev-amazonlinux2:v2", + "swift:5.4-amazonlinux2", + "swift:5.3-amazonlinux2", + "base-amazonlinux2:latest", + "swift-runtime-amazonlinux2:6.0", + "ci-amazonlinux2-builder:1.0", + "swift:5.2-amazonlinux2", + "custom-swift-amazonlinux2:dev", + "swift:5.1-amazonlinux2", + "staging-amazonlinux2:v3", + "swift:5.0-amazonlinux2", + "swift:4.2-amazonlinux2", + "build-amazonlinux2:release", + "swift:amazonlinux2", + "my/repo/amazonlinux2:tag", + "test:amazonlinux2-latest", + "swift:6.2-amazonlinux2", + "registry/amazonlinux2-swift:v1", + "local-amazonlinux2:dev", + "swift:nightly-main-amazonlinux2", + "swift:6.0.1-amazonlinux2", + "swift:6.0.2-amazonlinux2", + "swift:6.1.0-amazonlinux2", + "builder-amazonlinux2:prod", + "deploy-amazonlinux2:staging", + "lambda-amazonlinux2:latest", + "swift-lambda-amazonlinux2:6.0", + "amazonlinux2-builder:v4", + "ci/cd-amazonlinux2:latest", + "swift:release-amazonlinux2", + "swift:dev-amazonlinux2", + ] + + static let nonAL2Images: [String] = [ + "swift:5.9-amazonlinux2023", + "swift:6.0-amazonlinux2023", + "swift:6.1-amazonlinux2023", + "swift:latest-amazonlinux2023", + "swift:nightly-amazonlinux2023", + "myregistry/swift:6.0-amazonlinux2023", + "custom-amazonlinux2023:v1", + "ubuntu:22.04", + "debian:bookworm", + "alpine:3.18", + "fedora:39", + "centos:stream9", + "swift:6.0", + "swift:latest", + "ghcr.io/swift:6.0-amazonlinux2023", + "registry.example.com/swift:6.0-amazonlinux2023", + "public.ecr.aws/swift:6.0-amazonlinux2023", + "swift:5.10-amazonlinux2023", + "swift:6.2-amazonlinux2023", + "custom-image:latest", + "my-build-image:v1", + "swift-builder:6.0", + "lambda-base:latest", + "ci-runner:2.0", + "dev-env:latest", + "swift:nightly-main-amazonlinux2023", + "swift:6.0.1-amazonlinux2023", + "swift:6.0.2-amazonlinux2023", + "swift:6.1.0-amazonlinux2023", + "amazonlinux2023-swift:6.0", + "my-amazonlinux2023-image:v1", + "swift-amazonlinux2023:latest", + "ci-amazonlinux2023:prod", + "builder-amazonlinux2023:v2", + "deploy-amazonlinux2023:staging", + "swift:release-amazonlinux2023", + "swift:dev-amazonlinux2023", + "registry/amazonlinux2023-swift:v1", + "local-amazonlinux2023:dev", + "staging-amazonlinux2023:v3", + "base-amazonlinux2023:latest", + "swift-runtime-amazonlinux2023:6.0", + "ci-amazonlinux2023-builder:1.0", + "lambda-amazonlinux2023:latest", + "swift-lambda-amazonlinux2023:6.0", + "amazonlinux2023-builder:v4", + "test-amazonlinux2023-image", + "prod-amazonlinux2023:release", + "nightly-amazonlinux2023:latest", + "snapshot-amazonlinux2023:v5", + ] + + private func baseArgs() -> [String] { + [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + ] + } + + @available(LambdaSwift 2.0, *) + @Test("AL2 images (not AL2023) set explicitAL2Image to true", arguments: al2Images) + func al2ImageDetected(image: String) throws { + let args = baseArgs() + ["--base-docker-image", image] + let config = try BuilderConfiguration(arguments: args) + #expect( + config.explicitAL2Image == true, + "Image '\(image)' contains 'amazonlinux2' but not 'amazonlinux2023', should set explicitAL2Image=true" + ) + } + + @available(LambdaSwift 2.0, *) + @Test("AL2023 or non-AL2 images set explicitAL2Image to false", arguments: nonAL2Images) + func nonAL2ImageNotDetected(image: String) throws { + let args = baseArgs() + ["--base-docker-image", image] + let config = try BuilderConfiguration(arguments: args) + #expect( + config.explicitAL2Image == false, + "Image '\(image)' should set explicitAL2Image=false" + ) + } + + @available(LambdaSwift 2.0, *) + @Test("Default image (no --base-docker-image) sets explicitAL2Image to false") + func defaultImageNotFlagged() throws { + let args = baseArgs() + let config = try BuilderConfiguration(arguments: args) + #expect(config.explicitAL2Image == false) + } +} + +// MARK: - Property 9: Unsupported cross-compile methods report error with link + +/// **Validates: Requirements 2.14** +/// +/// For swift-static-sdk and custom-sdk, CrossCompileMethod.parse throws error +/// containing the SDK guide URL. +@Suite("Property 9: Unsupported cross-compile methods report error with link") +struct UnsupportedCrossCompileMethodsPropertyTests { + + static let unsupportedMethods: [String] = [ + "swift-static-sdk", + "custom-sdk", + ] + + static let sdkGuideURL = "https://www.swift.org/documentation/articles/static-linux-getting-started.html" + + @available(LambdaSwift 2.0, *) + @Test("Unsupported methods throw error with SDK guide URL", arguments: unsupportedMethods) + func unsupportedMethodThrowsWithLink(method: String) { + do { + _ = try CrossCompileMethod.parse(method) + Issue.record("Expected CrossCompileMethod.parse(\"\(method)\") to throw, but it succeeded") + } catch { + let errorDescription = String(describing: error) + #expect( + errorDescription.contains(Self.sdkGuideURL), + "Error for '\(method)' should contain SDK guide URL '\(Self.sdkGuideURL)', got: \(errorDescription)" + ) + } + } + + @available(LambdaSwift 2.0, *) + @Test("Unsupported methods via BuilderConfiguration throw error with SDK guide URL", arguments: unsupportedMethods) + func unsupportedMethodInBuilderConfigThrowsWithLink(method: String) { + let args: [String] = [ + "--package-id", "my-package", + "--package-display-name", "MyPackage", + "--package-directory", "/tmp/project", + "--docker-tool-path", "/usr/local/bin/docker", + "--zip-tool-path", "/usr/bin/zip", + "--output-path", "/tmp/output", + "--products", "MyLambda", + "--configuration", "release", + "--cross-compile", method, + ] + do { + _ = try BuilderConfiguration(arguments: args) + Issue.record("Expected BuilderConfiguration to throw for --cross-compile \(method)") + } catch { + let errorDescription = String(describing: error) + #expect( + errorDescription.contains(Self.sdkGuideURL), + "Error for '--cross-compile \(method)' should contain SDK guide URL, got: \(errorDescription)" + ) + } + } +} diff --git a/readme.md b/readme.md index b6daabfe6..3ac49ee95 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift - To build and archive your Lambda function, you need to install [docker](https://docs.docker.com/desktop/install/mac-install/) or Apple [container](https://github.com/apple/container). -- To deploy the Lambda function and invoke it, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). +- To deploy the Lambda function and invoke it, you must have [an AWS account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) and [install and configure the `aws` command line](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (run `aws configure` to set up your credentials in `~/.aws/`). - Some examples are using [AWS SAM](https://aws.amazon.com/serverless/sam/). Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) before deploying these examples. @@ -58,7 +58,7 @@ For the fastest path, just type: ```bash cd Examples/_MyFirstFunction -./create_and_deploy_function.sh +./create_function.sh ``` Otherwise, continue reading. @@ -117,19 +117,35 @@ The runtime comes with a plugin to generate the code of a simple AWS Lambda func swift package lambda-init --allow-writing-to-package-directory ``` -Your `Sources/main.swift` file should look like this. +Your `Sources/MyLambda/MyLambda.swift` file should look like this. ```swift import AWSLambdaRuntime -// in this example we are receiving and responding with strings +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// in this example we receive a HelloRequest JSON and we return a HelloResponse JSON +// the Lambda runtime let runtime = LambdaRuntime { - (event: String, context: LambdaContext) in - return String(event.reversed()) + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) } -try await runtime.run() +// start the loop +try await runtime.run() ``` 4. Build & archive the package @@ -137,9 +153,7 @@ try await runtime.run() The runtime comes with a plugin to compile on Amazon Linux and create a ZIP archive: ```bash -swift package archive \ - --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 +swift package --allow-network-connections docker lambda-build ``` By default, it runs on `docker` but it also allows you to build with [Apple container](https://github.com/apple/container) (it requires disabling the sandbox): @@ -150,9 +164,8 @@ By default, it runs on `docker` but it also allows you to build with [Apple cont # until https://github.com/swiftlang/swift-package-manager/issues/9763 is fixed swift package --disable-sandbox \ --allow-network-connections docker \ - --base-docker-image swift:amazonlinux2023 \ - archive \ - --container-cli container + lambda-build \ + --cross-compile container ``` If there is no error, the ZIP archive is ready to deploy. @@ -161,59 +174,38 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa > [!NOTE] > If you encounter Docker credential store errors during the build, remove the `credsStore` entry from your `~/.docker/config.json` file or disable the plugin sandbox with `--disable-sandbox`. See [issue #609](https://github.com/awslabs/swift-aws-lambda-runtime/issues/609) for details. -> [!NOTE] -> The archive plugin currently defaults to Amazon Linux 2 as the build environment. After June 30, 2026, the default will change to Amazon Linux 2023. To migrate early, add the `--base-docker-image swift:amazonlinux2023` flag to the archive command: -> ```bash -> swift package archive \ -> --allow-network-connections docker \ -> --base-docker-image swift:amazonlinux2023 -> ``` -> When deploying functions built on Amazon Linux 2023, you must use the `provided.al2023` runtime instead of `provided.al2` in the `aws lambda create-function` command. - 5. Deploy to AWS There are multiple ways to deploy to AWS ([SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html), [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started), [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html), [AWS Console](https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html)) that are covered later in this doc. -Here is how to deploy using the `aws` command line. +The fastest way to deploy is using the `lambda-deploy` plugin. It handles IAM role creation, function creation, and code upload automatically. + +> [!IMPORTANT] +> Before deploying, ensure you have the AWS CLI installed and have run `aws configure` to set up your credentials in `~/.aws/`. On EC2, ECS, or EKS, credentials are typically provided automatically by the instance or task role, so running `aws configure` is not required in those environments. ```bash -aws lambda create-function \ ---function-name MyLambda \ ---zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/MyLambda/MyLambda.zip \ ---runtime provided.al2023 \ ---handler provided \ ---architectures arm64 \ ---role arn:aws:iam:::role/lambda_basic_execution +swift package --allow-network-connections all:443 lambda-deploy ``` -The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. - -Replace `` with your actual AWS account ID (for example: 012345678901). - -> [!IMPORTANT] -> Before creating a function, you need to have a `lambda_basic_execution` IAM role in your AWS account. -> -> You can create this role in two ways: -> 1. Using AWS Console -> 2. Running the commands in the `create_lambda_execution_role()` function in [`Examples/_MyFirstFunction/create_iam_role.sh`](https://github.com/awslabs/swift-aws-lambda-runtime/blob/8dff649920ab0c66bb039d15ae48d9d5764db71a/Examples/_MyFirstFunction/create_and_deploy_function.sh#L40C1-L40C31) +This creates the Lambda function, provisions the necessary IAM role, and uploads the deployment package. 6. Invoke your Lambda function ```bash aws lambda invoke \ --function-name MyLambda \ ---payload $(echo \"Hello World\" | base64) \ -out.txt && cat out.txt && rm out.txt +--payload $(echo '{"name":"World","age":30}' | base64) \ +/dev/stdout ``` This should print ``` +{"greetings":"Hello World. You look older than your age."} { "StatusCode": 200, "ExecutedVersion": "$LATEST" } -"dlroW olleH" ``` ## Developing your Swift Lambda functions diff --git a/scripts/generate-aws-clients.sh b/scripts/generate-aws-clients.sh new file mode 100755 index 000000000..c2e44b4a5 --- /dev/null +++ b/scripts/generate-aws-clients.sh @@ -0,0 +1,364 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# ============================================================================= +# generate-aws-clients.sh +# +# Maintainer-run script to generate AWS service clients for the Lambda deploy +# plugin. This script is NOT part of the build process. It uses the Soto Code +# Generator to produce lightweight Swift clients for Lambda, IAM, S3, and STS +# with only the operations required by the deployer. +# +# Prerequisites: +# - Swift toolchain installed +# - Git installed +# - Internet access (to clone repos and download models) +# +# Usage: +# ./scripts/generate-aws-clients.sh +# +# The generated files are written to: +# Sources/AWSLambdaPluginHelper/GeneratedClients/ +# ============================================================================= + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +OUTPUT_DIR="${PROJECT_ROOT}/Sources/AWSLambdaPluginHelper/GeneratedClients" + +# Soto Code Generator repository and version +SOTO_CODEGEN_REPO="https://github.com/soto-project/soto-codegenerator.git" +SOTO_CODEGEN_BRANCH="main" + +# AWS SDK Smithy models repository +AWS_MODELS_REPO="https://github.com/aws/aws-sdk-go-v2.git" +AWS_MODELS_BRANCH="main" + +# Working directory for generation +WORK_DIR="${PROJECT_ROOT}/.build/codegen-work" + +# Services and their required operations +declare -A SERVICE_OPERATIONS +SERVICE_OPERATIONS=( + ["Lambda"]="CreateFunction,UpdateFunctionCode,DeleteFunction,GetFunction,CreateFunctionUrlConfig,GetFunctionUrlConfig,DeleteFunctionUrlConfig,AddPermission,RemovePermission" + ["IAM"]="CreateRole,DeleteRole,AttachRolePolicy,DetachRolePolicy,GetRole,PutRolePolicy,DeleteRolePolicy" + ["S3"]="CreateBucket,HeadBucket,PutObject,DeleteObject" + ["STS"]="GetCallerIdentity" +) + +# Map service names to their Smithy model directory names in aws-sdk-go-v2 +declare -A SERVICE_MODEL_DIRS +SERVICE_MODEL_DIRS=( + ["Lambda"]="lambda" + ["IAM"]="iam" + ["S3"]="s3" + ["STS"]="sts" +) + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +check_prerequisites() { + log "Checking prerequisites..." + + if ! command -v swift &> /dev/null; then + fatal "Swift toolchain not found. Please install Swift." + fi + + if ! command -v git &> /dev/null; then + fatal "Git not found. Please install git." + fi + + log "Prerequisites satisfied." +} + +setup_work_dir() { + log "Setting up working directory at ${WORK_DIR}..." + rm -rf "${WORK_DIR}" + mkdir -p "${WORK_DIR}" +} + +clone_codegen() { + log "Cloning Soto Code Generator..." + if [ -d "${WORK_DIR}/soto-codegenerator" ]; then + log "Soto Code Generator already cloned, pulling latest..." + git -C "${WORK_DIR}/soto-codegenerator" pull --quiet + else + git clone --quiet --depth 1 --branch "${SOTO_CODEGEN_BRANCH}" \ + "${SOTO_CODEGEN_REPO}" "${WORK_DIR}/soto-codegenerator" + fi + log "Soto Code Generator ready." +} + +download_models() { + log "Downloading AWS service model files..." + + local models_dir="${WORK_DIR}/aws-models" + mkdir -p "${models_dir}" + + # Clone aws-sdk-go-v2 sparsely to get only the service model directories we need + if [ ! -d "${WORK_DIR}/aws-sdk-go-v2" ]; then + git clone --quiet --depth 1 --filter=blob:none --sparse \ + --branch "${AWS_MODELS_BRANCH}" \ + "${AWS_MODELS_REPO}" "${WORK_DIR}/aws-sdk-go-v2" + + pushd "${WORK_DIR}/aws-sdk-go-v2" > /dev/null + local sparse_paths="" + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + sparse_paths="${sparse_paths} codegen/sdk-codegen/aws-models/${SERVICE_MODEL_DIRS[$service]}" + done + # shellcheck disable=SC2086 + git sparse-checkout set ${sparse_paths} + popd > /dev/null + fi + + # Copy model files to our working models directory + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local model_dir_name="${SERVICE_MODEL_DIRS[$service]}" + local src_dir="${WORK_DIR}/aws-sdk-go-v2/codegen/sdk-codegen/aws-models/${model_dir_name}" + if [ -d "${src_dir}" ]; then + cp -r "${src_dir}" "${models_dir}/" + log " Copied model for ${service} (${model_dir_name})" + else + # Try alternative model locations + local alt_src="${WORK_DIR}/aws-sdk-go-v2/codegen/sdk-codegen/aws-models" + local smithy_file + smithy_file=$(find "${alt_src}" -name "${model_dir_name}.json" -o -name "${model_dir_name}.smithy" 2>/dev/null | head -1) + if [ -n "${smithy_file}" ]; then + mkdir -p "${models_dir}/${model_dir_name}" + cp "${smithy_file}" "${models_dir}/${model_dir_name}/" + log " Copied model file for ${service}" + else + fatal "Could not find Smithy model for ${service} (looked in ${src_dir})" + fi + fi + done + + log "AWS service models ready." +} + +generate_config() { + log "Generating code generator configuration..." + + local config_file="${WORK_DIR}/codegen-config.json" + + # Build the configuration JSON with only the operations we need + cat > "${config_file}" << 'CONFIGEOF' +{ + "services": { + "Lambda": { + "operations": [ + "CreateFunction", + "UpdateFunctionCode", + "DeleteFunction", + "GetFunction", + "CreateFunctionUrlConfig", + "GetFunctionUrlConfig", + "DeleteFunctionUrlConfig", + "AddPermission", + "RemovePermission" + ] + }, + "IAM": { + "operations": [ + "CreateRole", + "DeleteRole", + "AttachRolePolicy", + "DetachRolePolicy", + "GetRole", + "PutRolePolicy", + "DeleteRolePolicy" + ] + }, + "S3": { + "operations": [ + "CreateBucket", + "HeadBucket", + "PutObject", + "DeleteObject" + ] + }, + "STS": { + "operations": [ + "GetCallerIdentity" + ] + } + } +} +CONFIGEOF + + log "Configuration written to ${config_file}" +} + +run_codegen() { + log "Building Soto Code Generator..." + pushd "${WORK_DIR}/soto-codegenerator" > /dev/null + swift build --configuration release 2>&1 | tail -5 + popd > /dev/null + + log "Running code generation for each service..." + + local codegen_bin="${WORK_DIR}/soto-codegenerator/.build/release/soto-codegenerator" + local models_dir="${WORK_DIR}/aws-models" + local generated_dir="${WORK_DIR}/generated" + mkdir -p "${generated_dir}" + + # If the code generator binary doesn't exist, try the default executable name + if [ ! -f "${codegen_bin}" ]; then + codegen_bin=$(find "${WORK_DIR}/soto-codegenerator/.build/release" -type f -perm +111 -name "*codegen*" | head -1) + if [ -z "${codegen_bin}" ]; then + # Fall back to running via swift run + log "Using 'swift run' to invoke the code generator..." + codegen_bin="SWIFT_RUN" + fi + fi + + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local model_dir_name="${SERVICE_MODEL_DIRS[$service]}" + local model_path="${models_dir}/${model_dir_name}" + local service_output="${generated_dir}/${service}" + mkdir -p "${service_output}" + + log " Generating ${service} client..." + + local operations="${SERVICE_OPERATIONS[$service]}" + + if [ "${codegen_bin}" = "SWIFT_RUN" ]; then + pushd "${WORK_DIR}/soto-codegenerator" > /dev/null + swift run soto-codegenerator \ + --model-path "${model_path}" \ + --output-path "${service_output}" \ + --operations "${operations}" \ + --module "${service}" \ + 2>&1 || log " Warning: Code generation for ${service} returned non-zero (may need manual review)" + popd > /dev/null + else + "${codegen_bin}" \ + --model-path "${model_path}" \ + --output-path "${service_output}" \ + --operations "${operations}" \ + --module "${service}" \ + 2>&1 || log " Warning: Code generation for ${service} returned non-zero (may need manual review)" + fi + done + + log "Code generation complete." +} + +copy_output() { + log "Copying generated clients to ${OUTPUT_DIR}..." + + local generated_dir="${WORK_DIR}/generated" + + # Clean previous generated output + rm -rf "${OUTPUT_DIR}" + mkdir -p "${OUTPUT_DIR}" + + for service in "${!SERVICE_MODEL_DIRS[@]}"; do + local service_dir="${generated_dir}/${service}" + local dest_dir="${OUTPUT_DIR}/${service}" + + if [ -d "${service_dir}" ] && [ "$(ls -A "${service_dir}" 2>/dev/null)" ]; then + mkdir -p "${dest_dir}" + cp -r "${service_dir}/"* "${dest_dir}/" + log " Copied ${service} → ${dest_dir}" + else + log " Warning: No generated files found for ${service} in ${service_dir}" + log " You may need to create the client files manually." + fi + done + + log "Generated clients installed at ${OUTPUT_DIR}" +} + +add_availability_annotations() { + log "Adding @available(LambdaSwift 2.0, *) annotations..." + + # Add @available(LambdaSwift 2.0, *) before every top-level struct/enum declaration + # in the generated files. This is required because soto-core uses availability + # annotations on its types (AWSClient, AWSServiceConfig, etc.) and this package + # does not declare a platforms: minimum. + find "${OUTPUT_DIR}" -name "*.swift" -print0 | while IFS= read -r -d '' file; do + perl -i -pe 's/^((?:public )?(?:struct|enum) \w+)/\@available(LambdaSwift 2.0, *)\n$1/' "$file" + done + + log "Availability annotations added." +} + +cleanup() { + log "Cleaning up working directory..." + rm -rf "${WORK_DIR}" + log "Cleanup complete." +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + log "==========================================" + log "AWS Service Client Generation Script" + log "==========================================" + log "" + log "This script generates lightweight AWS service clients" + log "for the Lambda deploy plugin using the Soto Code Generator." + log "" + log "Services: Lambda, IAM, S3, STS" + log "Output: ${OUTPUT_DIR}" + log "" + + check_prerequisites + setup_work_dir + clone_codegen + download_models + generate_config + run_codegen + copy_output + add_availability_annotations + + # Uncomment the following line to clean up after successful generation: + # cleanup + + log "" + log "==========================================" + log "Generation complete!" + log "==========================================" + log "" + log "Generated files are at:" + log " ${OUTPUT_DIR}" + log "" + log "Next steps:" + log " 1. Review the generated files" + log " 2. Run 'swift build' to verify compilation" + log " 3. Commit the generated files to the repository" + log "" + log "Note: If the code generator did not produce the expected output," + log "you may need to adjust the model paths or write the client files" + log "manually based on the Soto client patterns." + log "" +} + +main "$@" diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh new file mode 100755 index 000000000..a94311d22 --- /dev/null +++ b/scripts/integration-test.sh @@ -0,0 +1,385 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright SwiftAWSLambdaRuntime project authors +## Copyright (c) Amazon.com, Inc. or its affiliates. +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +# ============================================================================= +# integration-test.sh +# +# End-to-end integration test for the Lambda v4 plugin system. +# Exercises the full lifecycle: scaffold → build → deploy → validate → delete. +# +# Prerequisites: +# - AWS credentials configured (via aws configure or environment variables) +# - Docker installed and running +# - Swift toolchain installed +# - curl with --aws-sigv4 support (curl 7.75+) +# +# Usage: +# ./scripts/integration-test.sh +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +FUNCTION_NAME="swift-lambda-e2e-test-$(date +%s)" +AWS_REGION="us-east-1" +CLEANUP_NEEDED=false +WORK_DIR="" +FIXED_WORK_DIR="" + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +# --------------------------------------------------------------------------- +# Cleanup (guaranteed via trap) +# --------------------------------------------------------------------------- + +cleanup() { + local exit_code=$? + + if [ "$CLEANUP_NEEDED" = true ]; then + log "Cleaning up AWS resources for function: ${FUNCTION_NAME}..." + ( + cd "${WORK_DIR}" && \ + swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --region "${AWS_REGION}" --delete --products "${FUNCTION_NAME}" 2>&1 + ) || log "Warning: cleanup of AWS resources may have been incomplete." + fi + + if [ -n "${WORK_DIR}" ] && [ -d "${WORK_DIR}" ] && [ -z "${FIXED_WORK_DIR}" ]; then + log "Removing temporary directory: ${WORK_DIR}" + rm -rf "${WORK_DIR}" + fi + + if [ $exit_code -ne 0 ]; then + error "Integration test FAILED (exit code: ${exit_code})" + fi + + exit $exit_code +} + +trap cleanup EXIT + +# --------------------------------------------------------------------------- +# Prerequisites check +# --------------------------------------------------------------------------- + +check_prerequisites() { + log "Checking prerequisites..." + + if ! command -v swift &> /dev/null; then + fatal "Swift toolchain not found. Please install Swift." + fi + + if ! command -v docker &> /dev/null; then + fatal "Docker not found. Please install Docker." + fi + + if ! command -v curl &> /dev/null; then + fatal "curl not found. Please install curl." + fi + + if ! command -v aws &> /dev/null; then + fatal "AWS CLI not found. Please install and configure the AWS CLI." + fi + + # Verify AWS credentials are available + if ! aws sts get-caller-identity &> /dev/null; then + fatal "AWS credentials not configured or invalid. Run 'aws configure' or set environment variables." + fi + + log "All prerequisites satisfied." +} + +# --------------------------------------------------------------------------- +# Step 1: Create temporary project directory and initialize Swift package +# --------------------------------------------------------------------------- + +scaffold_project() { + log "Step 1: Creating temporary project directory..." + + if [ -n "${FIXED_WORK_DIR}" ]; then + WORK_DIR="${FIXED_WORK_DIR}" + mkdir -p "${WORK_DIR}" + log " Using fixed working directory: ${WORK_DIR}" + + # If Package.swift already exists, skip scaffolding + if [ -f "${WORK_DIR}/Package.swift" ]; then + log " Package.swift already exists, skipping scaffold." + cd "${WORK_DIR}" + return + fi + else + WORK_DIR=$(mktemp -d -t "swift-lambda-e2e-XXXXXX") + log " Working directory: ${WORK_DIR}" + fi + + cd "${WORK_DIR}" + + # Initialize a Swift package with the function name as the executable target + swift package init --type executable --name "${FUNCTION_NAME}" + + # Add macOS 15 platform requirement (needed by AWSLambdaRuntime) + sed -i '' 's/name: "'"${FUNCTION_NAME}"'",/name: "'"${FUNCTION_NAME}"'",\n platforms: [.macOS(.v15)],/' Package.swift + + # Add the lambda runtime dependency + swift package add-dependency https://github.com/swift-server/swift-aws-lambda-runtime.git --branch sebsto/new-plugins + swift package add-target-dependency AWSLambdaRuntime "${FUNCTION_NAME}" --package swift-aws-lambda-runtime + + # Also add AWSLambdaEvents for the URL template + swift package add-dependency https://github.com/swift-server/swift-aws-lambda-events.git --branch main + swift package add-target-dependency AWSLambdaEvents "${FUNCTION_NAME}" --package swift-aws-lambda-events + + log " Swift package initialized." +} + +# --------------------------------------------------------------------------- +# Step 2: Scaffold the Lambda function using lambda-init --with-url +# --------------------------------------------------------------------------- + +scaffold_function() { + log "Step 2: Scaffolding Lambda function with URL template..." + + cd "${WORK_DIR}" + + # Skip if already scaffolded (for --work-dir reuse) + if [ -n "${FIXED_WORK_DIR}" ] && grep -q "LambdaRuntime" Sources/main.swift 2>/dev/null; then + log " Function already scaffolded, skipping." + return + fi + + swift package --allow-writing-to-package-directory lambda-init --with-url + + log " Function scaffolded with URL template." +} + +# --------------------------------------------------------------------------- +# Step 3: Build and package the Lambda function +# --------------------------------------------------------------------------- + +build_function() { + log "Step 3: Building and packaging the Lambda function..." + + cd "${WORK_DIR}" + + # Skip build if archive already exists (for --work-dir reuse) + if [ -n "${FIXED_WORK_DIR}" ] && [ -d ".build/plugins/AWSLambdaBuilder/outputs" ]; then + log " Build artifacts found, skipping build." + return + fi + + swift package --allow-network-connections docker lambda-build --products "${FUNCTION_NAME}" + + log " Build and packaging complete." +} + +# --------------------------------------------------------------------------- +# Step 4: Deploy the Lambda function with Function URL +# --------------------------------------------------------------------------- + +deploy_function() { + log "Step 4: Deploying Lambda function with Function URL..." + + cd "${WORK_DIR}" + + # Capture deploy output to extract the Function URL + DEPLOY_OUTPUT=$(swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --region "${AWS_REGION}" --with-url --products "${FUNCTION_NAME}" 2>&1) || { + error "Deployment failed." + echo "${DEPLOY_OUTPUT}" >&2 + exit 1 + } + + # Mark cleanup as needed now that resources are deployed + CLEANUP_NEEDED=true + + echo "${DEPLOY_OUTPUT}" >&2 + + log " Deployment complete." +} + +# --------------------------------------------------------------------------- +# Step 5: Extract Function URL from deploy output +# --------------------------------------------------------------------------- + +extract_function_url() { + log "Step 5: Extracting Function URL from deploy output..." + + # The deploy plugin outputs the Function URL — extract it + FUNCTION_URL=$(echo "${DEPLOY_OUTPUT}" | grep -oE 'https://[a-z0-9]+\.lambda-url\.[a-z0-9-]+\.on\.aws/?' | head -1) + + if [ -z "${FUNCTION_URL}" ]; then + fatal "Could not extract Function URL from deploy output." + fi + + log " Function URL: ${FUNCTION_URL}" +} + +# --------------------------------------------------------------------------- +# Step 6: Validate the deployed function via curl with AWS SigV4 +# --------------------------------------------------------------------------- + +validate_function() { + log "Step 6: Validating deployed function via Function URL..." + + # Use the hardcoded region for SigV4 signing + local region="${AWS_REGION}" + log " Region for SigV4 signing: ${region}" + + # Resolve AWS credentials for curl (supports SSO, assumed roles, config files, etc.) + log " Resolving AWS credentials for curl..." + eval "$(aws configure export-credentials --format env-no-export 2>/dev/null)" || \ + fatal "Could not resolve AWS credentials. Ensure 'aws configure export-credentials' works." + + local access_key_id="${AWS_ACCESS_KEY_ID:-}" + local secret_access_key="${AWS_SECRET_ACCESS_KEY:-}" + local session_token="${AWS_SESSION_TOKEN:-}" + + if [ -z "${access_key_id}" ] || [ -z "${secret_access_key}" ]; then + fatal "Could not resolve AWS credentials for curl signing." + fi + + log " AWS_ACCESS_KEY_ID: ${access_key_id:0:8}..." + log " AWS_SESSION_TOKEN: ${session_token:+present}" + + # Wait for the function to become active (cold start may take a moment) + log " Waiting for function to become active..." + local max_retries=30 + local retry_count=0 + local response="" + + while [ $retry_count -lt $max_retries ]; do + # Use curl with AWS SigV4 to call the Function URL + response=$(curl --silent --show-error --max-time 60 \ + --aws-sigv4 "aws:amz:${region}:lambda" \ + --user "${access_key_id}:${secret_access_key}" \ + ${session_token:+-H "x-amz-security-token: ${session_token}"} \ + "${FUNCTION_URL}?name=World" 2>&1) || true + + # Check if we got the expected successful response + if echo "${response}" | grep -q '"Hello'; then + break + fi + + retry_count=$((retry_count + 1)) + if [ $retry_count -lt $max_retries ]; then + log " Attempt ${retry_count}/${max_retries} - waiting 10 seconds..." + sleep 10 + fi + done + + if [ $retry_count -ge $max_retries ]; then + error "Function did not return a valid response after ${max_retries} attempts." + error "Last response: ${response}" + exit 1 + fi + + log " Response received: ${response}" + + RESPONSE_BODY="${response}" +} + +# --------------------------------------------------------------------------- +# Step 7: Verify response matches expected output +# --------------------------------------------------------------------------- + +verify_response() { + log "Step 7: Verifying response body..." + + local expected_message="Hello World" + + if echo "${RESPONSE_BODY}" | grep -q "${expected_message}"; then + log " Response verification PASSED: contains '${expected_message}'" + else + error "Response verification FAILED." + error " Expected response to contain: '${expected_message}'" + error " Actual response: '${RESPONSE_BODY}'" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Step 8: Delete the deployed function and associated resources +# --------------------------------------------------------------------------- + +delete_function() { + log "Step 8: Deleting Lambda function and associated resources..." + + cd "${WORK_DIR}" + swift package --allow-network-connections all:443 \ + lambda-deploy --allow-writing-to-package-directory \ + --region "${AWS_REGION}" --delete --products "${FUNCTION_NAME}" + + CLEANUP_NEEDED=false + + log " Function and resources deleted." +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --work-dir) + FIXED_WORK_DIR="$2" + shift 2 + ;; + *) + fatal "Unknown argument: $1. Usage: $0 [--work-dir ]" + ;; + esac + done + + log "==========================================" + log "Lambda Plugin End-to-End Integration Test" + log "==========================================" + log "" + log "Function name: ${FUNCTION_NAME}" + log "Region: ${AWS_REGION}" + if [ -n "${FIXED_WORK_DIR}" ]; then + log "Fixed work dir: ${FIXED_WORK_DIR} (temp dir will NOT be deleted)" + fi + log "" + + check_prerequisites + scaffold_project + scaffold_function + build_function + deploy_function + extract_function_url + validate_function + verify_response + delete_function + + log "" + log "==========================================" + log "Integration test PASSED" + log "==========================================" +} + +main "$@"