diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..2a064dd --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,4 @@ +FROM swift:6.2.0 + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends make git \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e79ca9c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker-compose +{ + "name": "Docker from Docker Compose", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Use this environment variable if you need to bind mount your local source code into a new container. + "remoteEnv": { + "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" + }, + + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "version": "latest", + "enableNonRootDocker": "true", + "moby": "true" + }, + "ghcr.io/devcontainers/features/aws-cli:1": {} + }, + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "swiftlang.swift-vscode" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "docker --version", + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "vscode" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..67f8854 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + # Forwards the local Docker socket to the container. + - /var/run/docker.sock:/var/run/docker-host.sock + # Update this to wherever you want VS Code to mount the folder of your project + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + entrypoint: /usr/local/share/docker-init.sh + depends_on: + - localstack + environment: + - LOCALSTACK_ENDPOINT=http://localstack:4566 + - AWS_ACCESS_KEY_ID=test + - AWS_SECRET_ACCESS_KEY=test + - AWS_REGION=us-east-1 + command: sleep infinity + + # Uncomment the next four lines if you will use a ptrace-based debuggers like C++, Go, and Rust. + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + localstack: + image: localstack/localstack \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json new file mode 100644 index 0000000..932a8e1 --- /dev/null +++ b/.sourcekit-lsp/config.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/release/6.1/config.schema.json" +} \ No newline at end of file diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..4ac4fde --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2.0 \ No newline at end of file diff --git a/Makefile b/Makefile index 09bf950..2a29098 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,11 @@ localstack: docker run -it --rm -p "4566:4566" localstack/localstack local_setup_dynamo_db: - aws --endpoint-url=http://localhost:4566 dynamodb create-table \ + aws --endpoint-url=http://localstack:4566 dynamodb create-table \ --table-name Breeze \ --attribute-definitions AttributeName=itemKey,AttributeType=S \ --key-schema AttributeName=itemKey,KeyType=HASH \ - --billing-mode PAY_PER_REQUEST + --billing-mode PAY_PER_REQUEST \ --region us-east-1 local_invoke_demo_app: diff --git a/Package.swift b/Package.swift index 1f3bceb..2c13282 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 import PackageDescription @@ -26,8 +26,8 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.5.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime", from: "2.2.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "0.5.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"), .package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"), diff --git a/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift b/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift index 819da6d..e7fc2b7 100644 --- a/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift +++ b/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift @@ -20,19 +20,19 @@ import Logging /// Defines the interface for a Breeze DynamoDB service. /// /// Provides methods to access the database manager and to gracefully shutdown the service. -public protocol BreezeDynamoDBServing: Actor { - func dbManager() async -> BreezeDynamoDBManaging - func gracefulShutdown() throws +public protocol BreezeDynamoDBServing: Service { + var dbManager: BreezeDynamoDBManaging { get } + func onGracefulShutdown() async throws + func syncShutdown() throws } /// Provides methods to access the DynamoDB database manager and to gracefully shutdown the service. -public actor BreezeDynamoDBService: BreezeDynamoDBServing { +public struct BreezeDynamoDBService: BreezeDynamoDBServing { - private let dbManager: BreezeDynamoDBManaging + public let dbManager: BreezeDynamoDBManaging private let logger: Logger private let awsClient: AWSClient private let httpClient: HTTPClient - private var isShutdown = false /// Initializes the BreezeDynamoDBService with the provided configuration. /// - Parameters: @@ -46,11 +46,14 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { httpConfig: BreezeHTTPClientConfig, logger: Logger, DBManagingType: BreezeDynamoDBManaging.Type = BreezeDynamoDBManager.self - ) async { + ) { logger.info("Init DynamoDBService with config...") logger.info("region: \(config.region)") logger.info("tableName: \(config.tableName)") logger.info("keyName: \(config.keyName)") + if config.endpoint != nil { + logger.info("endpoint: \(config.endpoint!)") + } self.logger = logger let timeout = HTTPClient.Configuration.Timeout( @@ -76,10 +79,9 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { logger.info("DBManager is ready.") } - /// Returns the BreezeDynamoDBManaging instance. - public func dbManager() async -> BreezeDynamoDBManaging { - logger.info("Starting DynamoDBService...") - return self.dbManager + public func run() async throws { + try await gracefulShutdown() + try await onGracefulShutdown() } /// Gracefully shutdown the service and its components. @@ -89,21 +91,19 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { /// It also logs the shutdown process. /// This method is idempotent; /// - Important: This method must be called at leat once to ensure that resources are released properly. If the method is not called, it will lead to a crash. - public func gracefulShutdown() throws { - guard !isShutdown else { return } - isShutdown = true + public func onGracefulShutdown() async throws { logger.info("Stopping DynamoDBService...") - try awsClient.syncShutdown() + try await awsClient.shutdown() logger.info("DynamoDBService is stopped.") logger.info("Stopping HTTPClient...") - try httpClient.syncShutdown() + try await httpClient.shutdown() logger.info("HTTPClient is stopped.") } - deinit { - guard !isShutdown else { return } - try? awsClient.syncShutdown() - try? httpClient.syncShutdown() + /// Sync shutdown + public func syncShutdown() throws { + try awsClient.syncShutdown() + try httpClient.syncShutdown() } } diff --git a/Sources/BreezeLambdaAPI/BreezeAPIConfiguration.swift b/Sources/BreezeLambdaAPI/BreezeAPIConfiguration.swift index f4aa1a0..21b1a79 100644 --- a/Sources/BreezeLambdaAPI/BreezeAPIConfiguration.swift +++ b/Sources/BreezeLambdaAPI/BreezeAPIConfiguration.swift @@ -104,10 +104,10 @@ public struct BreezeAPIConfiguration: APIConfiguring { /// This method is used to retrieve the name of the primary key in the DynamoDB table that will be used by the Breeze Lambda API. /// - Important: The key name is essential for identifying items in the DynamoDB table. func keyName() throws -> String { - guard let tableName = Lambda.env("DYNAMO_DB_KEY") else { + guard let keyName = Lambda.env("DYNAMO_DB_KEY") else { throw BreezeLambdaAPIError.keyNameNotFound } - return tableName + return keyName } /// Returns the endpoint for the Breeze Lambda API. diff --git a/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift b/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift index a13c55f..67f7df5 100644 --- a/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift +++ b/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift @@ -43,6 +43,7 @@ public actor BreezeLambdaAPI: Service { let timeout: TimeAmount private let serviceGroup: ServiceGroup private let apiConfig: any APIConfiguring + private let dynamoDBService: BreezeDynamoDBService /// Initializes the BreezeLambdaAPI with the provided API configuration. /// - Parameter apiConfig: An object conforming to `APIConfiguring` that provides the necessary configuration for the Breeze API. @@ -63,19 +64,18 @@ public actor BreezeLambdaAPI: Service { logger: logger ) let operation = try apiConfig.operation() - let dynamoDBService = await BreezeDynamoDBService( + self.dynamoDBService = BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger ) - let breezeLambdaService = BreezeLambdaService( - dynamoDBService: dynamoDBService, - operation: operation, - logger: logger - ) + let dbManager = dynamoDBService.dbManager + let breezeApi = BreezeLambdaHandler(dbManager: dbManager, operation: operation) + let runtime = LambdaRuntime(body: breezeApi.handle) self.serviceGroup = ServiceGroup( - services: [breezeLambdaService], - gracefulShutdownSignals: [.sigterm, .sigint], + services: [runtime, dynamoDBService], + gracefulShutdownSignals: [.sigint], + cancellationSignals: [.sigterm], logger: logger ) } catch { @@ -90,7 +90,13 @@ public actor BreezeLambdaAPI: Service { /// The internal ServiceGroup will handle the lifecycle of the BreezeLambdaAPI, including starting and stopping the service gracefully. public func run() async throws { logger.info("Starting BreezeLambdaAPI...") - try await serviceGroup.run() + do { + try await serviceGroup.run() + } catch { + try dynamoDBService.syncShutdown() + logger.error("BreezeLambdaAPI failed with error: \(error.localizedDescription)") + throw error + } logger.info("BreezeLambdaAPI is stopped successfully") } } diff --git a/Sources/BreezeLambdaAPI/BreezeLambdaService.swift b/Sources/BreezeLambdaAPI/BreezeLambdaService.swift deleted file mode 100644 index 94b5c5d..0000000 --- a/Sources/BreezeLambdaAPI/BreezeLambdaService.swift +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2024 (c) Andrea Scuderi - https://github.com/swift-serverless -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import ServiceLifecycle -import AsyncHTTPClient -import NIOCore -import BreezeDynamoDBService -import AWSLambdaRuntime -import AWSLambdaEvents -import Logging -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -/// Service for processing AWS API Gateway events with BreezeCodable models. -/// -/// `BreezeLambdaService` is a key component in the serverless architecture that: -/// - Acts as a bridge between AWS Lambda runtime and DynamoDB operations -/// - Processes incoming API Gateway events through a type-safe interface -/// - Manages the lifecycle of AWS Lambda handlers for BreezeCodable models -/// - Coordinates graceful shutdown procedures to ensure clean resource release -/// -/// The service leverages Swift concurrency features through the actor model to ensure -/// thread-safe access to shared resources while processing multiple Lambda invocations. -/// It delegates the actual processing of events to a specialized `BreezeLambdaHandler` -/// which performs the database operations via the injected `BreezeDynamoDBService`. -/// -/// This service is designed to be initialized and run as part of a `ServiceGroup` -/// within the AWS Lambda execution environment. -actor BreezeLambdaService: Service { - - /// Database service that provides access to the underlying DynamoDB operations. - /// - /// This service is responsible for all database interactions and connection management. - let dynamoDBService: BreezeDynamoDBServing - - /// Operation type that determines the behavior of this service instance. - /// - /// Defines whether this Lambda will perform create, read, update, delete, or list operation - let operation: BreezeOperation - - /// Logger instance for tracking service lifecycle events and errors. - /// - /// Used throughout the service to provide consistent logging patterns. - let logger: Logger - - /// Initializes a new instance of `BreezeLambdaService`. - /// - Parameters: - /// - dynamoDBService: Service providing DynamoDB operations and connection management - /// - operation: The specific CRUD operation this Lambda instance will perform - /// - logger: Logger instance for service monitoring and debugging - init(dynamoDBService: BreezeDynamoDBServing, operation: BreezeOperation, logger: Logger) { - self.dynamoDBService = dynamoDBService - self.operation = operation - self.logger = logger - } - - /// Handler instance that processes business logic for the configured operation. - /// - /// Lazily initialized during the `run()` method to ensure proper service startup sequence. - var breezeApi: BreezeLambdaHandler? - - /// Handler method that processes incoming AWS Lambda events. - func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { - guard let breezeApi else { throw BreezeLambdaAPIError.invalidHandler } - return try await breezeApi.handle(event, context: context) - } - - /// Runs the service allowing graceful shutdown. - /// - /// - Throws: An error if the service fails to initialize or run. - func run() async throws { - let dbManager = await dynamoDBService.dbManager() - let breezeApi = BreezeLambdaHandler(dbManager: dbManager, operation: self.operation) - self.breezeApi = breezeApi - logger.info("Starting BreezeLambdaService...") - let runtime = LambdaRuntime(body: handler) - try await runTaskWithCancellationOnGracefulShutdown { - do { - try await runtime.run() - } catch { - self.logger.error("\(error.localizedDescription)") - throw error - } - } onGracefulShutdown: { - self.logger.info("Gracefully stoping BreezeLambdaService ...") - try await self.dynamoDBService.gracefulShutdown() - self.logger.info("BreezeLambdaService is stopped.") - } - } - - /// Runs a task with cancellation on graceful shutdown. - /// - /// - Note: It's required to allow a full process shutdown without leaving tasks hanging. - private func runTaskWithCancellationOnGracefulShutdown( - operation: @escaping @Sendable () async throws -> Void, - onGracefulShutdown: () async throws -> Void - ) async throws { - let (cancelOrGracefulShutdown, cancelOrGracefulShutdownContinuation) = AsyncStream.makeStream() - let task = Task { - try await withTaskCancellationOrGracefulShutdownHandler { - try await operation() - } onCancelOrGracefulShutdown: { - cancelOrGracefulShutdownContinuation.yield() - cancelOrGracefulShutdownContinuation.finish() - } - } - for await _ in cancelOrGracefulShutdown { - try await onGracefulShutdown() - task.cancel() - } - } -} diff --git a/Sources/BreezeLambdaItemAPI/BreezeLambdaItemAPI.swift b/Sources/BreezeLambdaItemAPI/BreezeLambdaItemAPI.swift index 4fcf9e7..a4a352f 100644 --- a/Sources/BreezeLambdaItemAPI/BreezeLambdaItemAPI.swift +++ b/Sources/BreezeLambdaItemAPI/BreezeLambdaItemAPI.swift @@ -54,7 +54,7 @@ struct APIConfiguration: APIConfiguring { /// You can change the region, table name, key name, and endpoint as needed for your application. /// Remove the endpoint for production use. func getConfig() throws -> BreezeDynamoDBConfig { - BreezeDynamoDBConfig(region: .useast1, tableName: "Breeze", keyName: "itemKey", endpoint: "http://127.0.0.1:4566") + BreezeDynamoDBConfig(region: .useast1, tableName: "Breeze", keyName: "itemKey", endpoint: "http://localstack:4566") } } diff --git a/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift b/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift index 1c0509c..1cec714 100644 --- a/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift +++ b/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift @@ -23,16 +23,19 @@ struct BreezeDynamoDBServiceTests { @Test func testInitPrepareBreezeDynamoDBManager() async throws { let sut = await makeBreezeDynamoDBConfig() - let manager = await sut.dbManager() + let manager = +sut.dbManager #expect(manager is BreezeDynamoDBManager, "Expected BreezeDynamoDBManager instance") - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() } @Test func testGracefulShutdownCanBeCalledMultipleTimes() async throws { let sut = await makeBreezeDynamoDBConfig() - try await sut.gracefulShutdown() - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() + await #expect(throws: Error.self) { + try await sut.onGracefulShutdown() + } } @Test @@ -44,15 +47,15 @@ struct BreezeDynamoDBServiceTests { ) let logger = Logger(label: "BreezeDynamoDBServiceTests") let httpConfig = BreezeHTTPClientConfig(timeout: .seconds(10), logger: logger) - let sut = await BreezeDynamoDBService( + let sut = BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger, DBManagingType: BreezeDynamoDBManagerMock.self ) - let manager = await sut.dbManager() + let manager = sut.dbManager #expect(manager is BreezeDynamoDBManagerMock, "Expected BreezeDynamoDBManager instance") - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() } private func makeBreezeDynamoDBConfig() async -> BreezeDynamoDBService { @@ -63,7 +66,7 @@ struct BreezeDynamoDBServiceTests { ) let logger = Logger(label: "BreezeDynamoDBServiceTests") let httpConfig = BreezeHTTPClientConfig(timeout: .seconds(10), logger: logger) - return await BreezeDynamoDBService( + return BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger, diff --git a/Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift b/Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift similarity index 75% rename from Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift rename to Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift index bd55e44..a3f27ac 100644 --- a/Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift +++ b/Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift @@ -26,54 +26,62 @@ struct APIConfiguration: APIConfiguring { .list } func getConfig() throws -> BreezeDynamoDBConfig { - BreezeDynamoDBConfig(region: .useast1, tableName: "Breeze", keyName: "itemKey", endpoint: "http://127.0.0.1:4566") + BreezeDynamoDBConfig(region: .useast1, tableName: "Breeze", keyName: "itemKey", endpoint: "http://localstack:4566") } } -@Suite -struct BreezeLambdaAPIServiceTests { +@Suite(.serialized) +struct BreezeLambdaAPITests { let logger = Logger(label: "BreezeHTTPClientServiceTests") @Test - func test_breezeLambdaAPIService_whenValidEnvironment() async throws { + func test_breezeLambdaAPI_whenValidEnvironment() async throws { + do { try await testGracefulShutdown { gracefulShutdownTestTrigger in - let (gracefulStream, continuation) = AsyncStream.makeStream() - try await withThrowingTaskGroup(of: Void.self) { group in let sut = try await BreezeLambdaAPI(apiConfig: APIConfiguration()) group.addTask { - try await Task.sleep(nanoseconds: 1_000_000_000) - gracefulShutdownTestTrigger.triggerGracefulShutdown() - } - group.addTask { - try await withGracefulShutdownHandler { + try await withGracefulShutdownHandler{ try await sut.run() - print("BreezeLambdaAPIService started successfully") } onGracefulShutdown: { logger.info("On Graceful Shutdown") continuation.yield() - continuation.finish() } } + group.addTask { + try await Task.sleep(nanoseconds: 1_000_000_000) + gracefulShutdownTestTrigger.triggerGracefulShutdown() + } for await _ in gracefulStream { + continuation.finish() logger.info("Graceful shutdown stream received") group.cancelAll() } } } + } catch { + logger.error("Error during test: \(error)") + throw error + } } @Test - func test_breezeLambdaAPIService_whenInvalidEnvironment() async throws { + func test_breezeLambdaAPI_whenInvalidEnvironment() async throws { await #expect(throws: BreezeLambdaAPIError.self) { + let (errorStream, continuation) = AsyncStream.makeStream() try await testGracefulShutdown { gracefulShutdownTestTrigger in try await withThrowingTaskGroup(of: Void.self) { group in let sut = try await BreezeLambdaAPI() group.addTask { try await withGracefulShutdownHandler { - try await sut.run() + do { + try await sut.run() + } catch { + continuation.yield(error) + continuation.finish() + } } onGracefulShutdown: { logger.info("Performing onGracefulShutdown") } @@ -82,7 +90,10 @@ struct BreezeLambdaAPIServiceTests { try await Task.sleep(nanoseconds: 1_000_000_000) gracefulShutdownTestTrigger.triggerGracefulShutdown() } - group.cancelAll() + for await _ in errorStream { + logger.info("Error stream received") + group.cancelAll() + } } } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 0d1b9a8..38f85dd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ FROM swift:6.2.0-amazonlinux2 as builder RUN yum -y update && \ - yum -y install git make + yum -y install git make \ No newline at end of file